use std::cmp::Ordering;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fmt;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;
use chrono::DateTime;
use chrono::Utc;
use serde::Deserialize;
use libpijul::change::Author;
use libpijul::changestore::ChangeStore;
use libpijul::pristine::sanakirja::Pristine;
use libpijul::pristine::sanakirja::Txn;
use libpijul::pristine::ChannelRef;
use libpijul::pristine::InodeMetadata;
use libpijul::working_copy::WorkingCopy;
use libpijul::working_copy::WorkingCopyRead;
use libpijul::DepsTxnT;
use libpijul::GraphTxnT;
use libpijul::Hash;
use libpijul::MutTxnT;
use libpijul::MutTxnTExt;
use libpijul::TxnT;
use libpijul::TxnTExt;
use crate::changestore::EagerChangeStore;
pub struct Repository {
path: String,
pristine: Pristine,
change_store: EagerChangeStore,
identities: HashMap<String, String>,
}
pub struct Change {
pub hash: libpijul::Hash,
pub state: libpijul::Merkle,
pub message: String,
pub description: Option<String>,
pub timestamp: DateTime<Utc>,
pub authors: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct NoSuchChannelError {
channel_name: String,
}
impl fmt::Display for NoSuchChannelError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "no such channel: {}", self.channel_name)
}
}
impl Error for NoSuchChannelError {}
#[derive(Debug, Clone)]
pub struct StateMismatch {
got: libpijul::Merkle,
want: libpijul::Merkle,
}
impl fmt::Display for StateMismatch {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"state mismatch: got {:?}, want {:?}",
self.got, self.want
)
}
}
impl Error for StateMismatch {}
#[derive(Deserialize)]
struct IdentityKey {
key: String,
}
#[derive(Deserialize)]
struct Identity {
display_name: String,
email: String,
public_key: IdentityKey,
}
impl Repository {
pub fn load(path: &str) -> Result<Repository, Box<dyn Error>> {
let repo_path = Path::new(path);
let pristine_path = repo_path.join(".pijul/pristine/db");
let pristine = Pristine::new(pristine_path)?;
let change_store = EagerChangeStore::from_root(repo_path);
let mut identities: HashMap<String, String> = HashMap::new();
Self::load_identities(&mut identities, &repo_path.join(".pijul/identities")).unwrap_or(());
if let Ok(home) = env::var("HOME") {
Self::load_identities(
&mut identities,
&Path::new(&home).join(".config/pijul/identities"),
)
.unwrap_or(());
}
Ok(Repository {
path: path.to_string(),
change_store,
pristine,
identities,
})
}
fn load_identities(
identities: &mut HashMap<String, String>,
dir: &Path,
) -> Result<(), Box<dyn Error>> {
let files = fs::read_dir(dir)?;
for file in files {
if let Ok(content) = fs::read_to_string(dir.join(file?.path()).join("identity.toml")) {
let id: Identity = toml::from_str(&content)?;
identities.insert(
id.public_key.key,
format!("{} <{}>", id.display_name, id.email),
);
}
}
Ok(())
}
fn load_channel(
&mut self,
txn: &Txn,
channel_name: &str,
) -> Result<ChannelRef<Txn>, Box<dyn Error>> {
match txn.load_channel(channel_name) {
Ok(opt) => match opt {
Some(c) => Ok(c),
None => Err(Box::new(NoSuchChannelError {
channel_name: channel_name.to_string(),
})),
},
Err(err) => Err(err.into()),
}
}
fn author_string(&self, a: &Author) -> String {
if let Some(key) = a.0.get("key") {
return self
.identities
.get(key)
.map_or_else(|| format!("{} <>", key), Into::into);
}
if let Some(name) = a.0.get("name") {
return format!(
"{} <{}>",
name,
a.0.get("email").map(|e| e.as_str()).unwrap_or("")
);
}
if let Some(email) = a.0.get("email") {
return format!("<{}>", email);
}
"<>".into()
}
pub fn log(&mut self, channel_name: &str) -> Result<Vec<Change>, Box<dyn Error>> {
let txn = self.pristine.txn_begin()?;
let channel = self.load_channel(&txn, channel_name)?;
let log = txn.log(&*channel.read(), 0)?;
let mut changes: Vec<Change> = Vec::new();
for pr in log {
let (_, (h, mrk)) = pr?;
let hash: libpijul::Hash = h.into();
let header = self.change_store.get_header(&h.into())?;
let authors: Vec<String> = header
.authors
.iter()
.map(|a| self.author_string(a))
.collect();
changes.push(Change {
hash,
state: mrk.into(),
message: header.message,
description: header.description,
timestamp: header.timestamp,
authors,
});
}
Ok(changes)
}
pub fn new_sandbox(&mut self) -> Result<Sandbox, Box<dyn Error>> {
let change_store = EagerChangeStore::from_root(Path::new(&self.path));
let txn = self.pristine.arc_txn_begin()?;
let channel = txn.write().open_or_create_channel("pijul-export-sandbox")?;
Ok(Sandbox {
change_store,
txn,
channel,
})
}
}
pub struct Sandbox {
change_store: EagerChangeStore,
txn: libpijul::pristine::ArcTxn<libpijul::pristine::sanakirja::MutTxn<()>>,
channel: libpijul::pristine::ChannelRef<libpijul::pristine::sanakirja::MutTxn<()>>,
}
impl Sandbox {
pub fn add_change(&mut self, change: &Change) -> Result<(), Box<dyn Error>> {
let (_, new_state) = self.txn.write().apply_change(
&self.change_store,
&mut *self.channel.write(),
&change.hash,
)?;
if new_state == change.state {
Ok(())
} else {
Err(Box::new(StateMismatch {
got: new_state,
want: change.state,
}))
}
}
pub fn get_files(&mut self, change: Hash) -> Result<FileSet, Box<dyn Error>> {
let fs = FileSet {
operations: Arc::new(Mutex::new(Vec::new())),
};
let mut touched_paths = BTreeSet::new();
let txn = self.txn.read();
if let Some(int) = txn.get_internal(&change.into())? {
for inode in txn.iter_rev_touched(int)? {
let (int_, inode) = inode?;
match int_.cmp(int) {
Ordering::Less => continue,
Ordering::Greater => break,
Ordering::Equal => {
if let Some((path, _)) = libpijul::fs::find_path(
&self.change_store,
&*txn,
&*self.channel.read(),
false,
*inode,
)? {
touched_paths.insert(path);
} else {
touched_paths.clear();
break;
}
}
}
}
}
if touched_paths.contains("") {
touched_paths.clear();
}
if touched_paths.is_empty() {
touched_paths.insert(String::from(""));
}
std::mem::drop(txn);
let mut last: Option<&str> = None;
for path in touched_paths.iter() {
if let Some(last_path) = last {
if last_path.len() < path.len() {
let (pre_last, post_last) = path.split_at(last_path.len());
if pre_last == last_path && post_last.starts_with('/') {
continue;
}
}
}
libpijul::output::output_repository_no_pending(
&fs,
&self.change_store,
&self.txn,
&self.channel,
path,
false,
None,
1,
0,
)?;
last = Some(path);
}
Ok(fs)
}
}
#[derive(Clone)]
pub struct FileWriter {
pub name: String,
pub content: Arc<Mutex<Vec<u8>>>,
}
impl std::io::Write for FileWriter {
fn write(&mut self, b: &[u8]) -> Result<usize, std::io::Error> {
match self.content.lock() {
Ok(mut c) => c.write(b),
Err(e) => panic!("{}", e),
}
}
fn flush(&mut self) -> Result<(), std::io::Error> {
Ok(())
}
}
pub enum FileOp {
Modify { fw: FileWriter },
Delete { path: String },
Rename { old: String, new: String },
}
#[derive(Clone)]
pub struct FileSet {
pub operations: Arc<Mutex<Vec<FileOp>>>,
}
#[derive(Debug, Clone)]
pub enum FileSetError {
ReadNotImplemented,
}
impl Error for FileSetError {}
impl fmt::Display for FileSetError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
FileSetError::ReadNotImplemented => write!(f, "not implemented: WorkingCopyRead"),
}
}
}
impl WorkingCopyRead for FileSet {
type Error = FileSetError;
fn file_metadata(&self, _file: &str) -> Result<InodeMetadata, Self::Error> {
Err(FileSetError::ReadNotImplemented)
}
fn read_file(&self, _file: &str, _buffer: &mut Vec<u8>) -> Result<(), Self::Error> {
Err(FileSetError::ReadNotImplemented)
}
fn modified_time(&self, _file: &str) -> Result<std::time::SystemTime, Self::Error> {
Err(FileSetError::ReadNotImplemented)
}
}
impl WorkingCopy for FileSet {
type Writer = FileWriter;
fn create_dir_all(&self, _file: &str) -> Result<(), Self::Error> {
Ok(())
}
fn remove_path(&self, path: &str, _rec: bool) -> Result<(), Self::Error> {
self.operations.lock().unwrap().push(FileOp::Delete {
path: path.to_owned(),
});
Ok(())
}
fn rename(&self, old: &str, new: &str) -> Result<(), Self::Error> {
self.operations.lock().unwrap().push(FileOp::Rename {
old: old.to_owned(),
new: new.to_owned(),
});
Ok(())
}
fn set_permissions(&self, _file: &str, _permissions: u16) -> Result<(), Self::Error> {
Ok(())
}
fn write_file(&self, file: &str, _: libpijul::Inode) -> Result<Self::Writer, Self::Error> {
let f = FileWriter {
name: file.to_string(),
content: Arc::new(Mutex::new(Vec::new())),
};
self.operations
.lock()
.unwrap()
.push(FileOp::Modify { fw: f.clone() });
Ok(f)
}
}