repositories.rs
use std::fmt::Write;
use std::path::PathBuf;
use crate::models::{pijul::changestores::Changestore, users::User, projects::Project};
use crate::database::Database;
use libpijul::pristine::sanakirja::Pristine;
use libpijul::TxnT;
use libpijul::TxnTExt;
use libpijul::{Base32, Hash};
use libpijul::{MutTxnT, MutTxnTExt};
use anyhow::bail;
pub struct Repository<'r> {
/// full_path is the fully qualified path on disk
full_path: PathBuf,
project: &'r Project,
}
const DEFAULT_CHANNEL: &str = "main";
const PRISTINE_DIR: &str = "pristine";
impl <'r>Repository<'r> {
fn pristine_dir(&self) -> PathBuf {
self.full_path.join(PRISTINE_DIR)
}
fn pristine(&self) -> Result<Pristine, anyhow::Error> {
// TODO error should be logged, or handled somehow, not like this
match Pristine::new(self.pristine_dir().join("db")) {
Ok(p) => Ok(p),
Err(e) => bail!("Failed to open pristine: {}", e),
}
}
pub fn changestore(&self) -> Changestore {
Changestore::new(self.full_path.clone())
}
// Checks if the change is on disk, and if we can deserialize
pub fn valid_change(&self, hash: Hash) -> bool {
libpijul::change::Change::deserialize(
&self.changestore().change_file(hash).to_string_lossy(),
Some(&hash),
)
.is_ok()
}
// TODO maybe make a proper channel struct etc later
pub fn apply_change_to_channel(&self, chan: &str, change: Hash) -> Result<(), anyhow::Error> {
let mut txn = self.pristine()?.mut_txn_begin()?;
let chan = if let Some(chan) = txn.load_channel(chan)? {
chan
} else {
bail!("channel not found")
};
{
let mut write_channel = chan.write();
txn.apply_change(
&libpijul::changestore::filesystem::FileSystem::from_changes(
self.changestore().dir(),
),
&mut write_channel,
&change,
)?;
}
txn.commit()?;
Ok(())
}
fn new(project: &Project, full_path: PathBuf) -> Repository {
Repository {
project: project,
full_path: full_path.join(libpijul::DOT_DIR),
}
}
/// init returns an error if there's already something on disk
fn init(&self) -> Result<(), anyhow::Error> {
if std::fs::metadata(self.pristine_dir()).is_err() {
std::fs::create_dir_all(self.pristine_dir())?;
} else {
bail!("Already a repository on disk")
}
// TODO why the f does libpijul do this? It's properly weird even small
// actions can just create their own txn
let mut txn = self.pristine()?.mut_txn_begin()?;
txn.open_or_create_channel(DEFAULT_CHANNEL)?;
txn.set_current_channel(DEFAULT_CHANNEL)?;
txn.commit()?;
Ok(())
}
/// Either initiate a Pijul repository, or open the one already present on
/// disk. Fails if the pristine cannot be opened correctly.
pub fn init_or_open(project: &'r Project, root: &PathBuf) -> Result<Self, anyhow::Error> {
let repo = Self::new(project, root.join(project.repo_path()));
// If the pristine can't be construsted, assume nothing is on disk
// TODO make a proper is_valid();
match repo.pristine() {
Ok(_) => Ok(repo),
Err(e) => {
println!("{}", e);
repo.init()?;
Ok(repo)
}
}
}
pub fn channel_remote_id(&self, channel: String, _id: Option<String>) -> Option<String> {
let txn = match self.pristine().ok()?.mut_txn_begin() {
Err(_) => return None,
Ok(txn) => txn,
};
// TODO validate the txn needs closing?
//
// Or does the drop function do that?
let chan = match txn.load_channel(&channel) {
Ok(c) => c,
Err(_) => None,
};
match chan {
Some(c) => Some(c.read().id.to_string()),
None => {
println!("No chan found");
None
}
}
}
// TODO figure out how to stream the response, instead of allocating all now
pub fn changelist(&self, channel: String, from: u64) -> Result<String, anyhow::Error> {
let txn = self.pristine()?.mut_txn_begin()?;
let chan = match txn.load_channel(&channel)? {
Some(c) => c,
None => bail!("failed to read channel transaction"),
};
let mut out: String = "".to_string();
for change in txn.log(&chan.read(), from)? {
let (offset, (hash, merkle)) = change?;
let h: Hash = hash.into();
let m: libpijul::Merkle = merkle.into();
write!(out, "{}.{}.{}\n", offset, h.to_base32(), m.to_base32())?;
}
Ok(out)
}
pub async fn owner(&self, db: &Database) -> anyhow::Result<User> {
self.project.owner(db).await
}
}