pijul 1.0.0-alpha

The sound distributed version control system.
use crate::repository::*;
use crate::Error;
use chrono::Utc;
use libpijul::change::*;
use libpijul::changestore::*;
use libpijul::pristine::{Base32, ChannelRef, MutTxnT, TxnT};
use libpijul::{MutTxnTExt, TxnTExt};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use thrussh_keys::PublicKeyBase64;

#[derive(Clap, Debug)]
pub struct Record {
    #[clap(short = 'a', long = "all")]
    pub all: bool,
    #[clap(short = 'm', long = "message")]
    pub message: Option<String>,
    #[clap(long = "author")]
    pub author: Option<String>,
    #[clap(long = "channel")]
    pub channel: Option<String>,
    #[clap(long = "repository")]
    pub repo_path: Option<PathBuf>,
    #[clap(long = "timestamp")]
    pub timestamp: Option<i64>,
    #[clap(short = 'S')]
    pub sign: bool,
    #[clap(long = "stdin")]
    pub stdin: bool,
    #[clap(long = "tag")]
    pub tag: bool,
    #[clap(long = "amend")]
    pub amend: Option<String>,
    pub prefixes: Vec<PathBuf>,
}

impl Record {
    pub async fn run(self) -> Result<(), anyhow::Error> {
        let mut repo = Repository::find_root(self.repo_path.clone())?;
        let mut txn = repo.pristine.mut_txn_begin();
        let mut channel =
            txn.open_or_create_channel(repo.config.get_current_channel(self.channel.as_ref()))?;
        let sign = self.sign;

        let header = if let Some(ref amend) = self.amend {
            let (h, _) = txn.hash_from_prefix(amend)?;
            let header = repo.changes.get_header(&h)?;
            txn.unrecord(&repo.changes, &mut channel, &h)?;
            header
        } else {
            self.header()
        };
        let result = self.record(
            &mut txn,
            &mut channel,
            &mut repo.working_copy,
            &repo.changes,
            &repo.path,
            header,
        )?;
        if let Some((mut change, updates, hash)) = result {
            let hash = hash.unwrap();
            if sign {
                let mut key_path = dirs::home_dir().unwrap().join(".ssh");
                if let Some((pk, signature)) = sign_hash(&mut key_path, hash).await? {
                    let sig = toml::Value::try_from(vec![Signature {
                        public_key: pk,
                        timestamp: change.header.timestamp,
                        signature: signature,
                    }])?;
                    let mut toml = toml::map::Map::new();
                    toml.insert("signatures".to_string(), sig);
                    change.unhashed = Some(toml.into());
                    let hash2 = repo.changes.save_change(&change).unwrap();
                    assert_eq!(hash2, hash);
                }
            }
            txn.apply_local_change(&mut channel, &change, hash, &updates)?;
            println!("Hash: {}", hash.to_base32());
            txn.commit()?;
        } else {
            eprintln!("Nothing to record");
        }
        Ok(())
    }

    fn header(&self) -> ChangeHeader {
        let authors = if let Some(ref a) = self.author {
            vec![libpijul::change::Author {
                name: a.clone(),
                full_name: None,
                email: None,
            }]
        } else if let Ok(global) = crate::config::Global::load() {
            vec![global.author]
        } else {
            Vec::new()
        };
        ChangeHeader {
            message: self.message.clone().unwrap_or(String::new()),
            authors,
            description: None,
            timestamp: if let Some(t) = self.timestamp {
                chrono::DateTime::from_utc(chrono::NaiveDateTime::from_timestamp(t, 0), chrono::Utc)
            } else {
                Utc::now()
            },
        }
    }

    fn fill_relative_prefixes(&mut self) -> Result<(), anyhow::Error> {
        let cwd = std::env::current_dir()?;
        for p in self.prefixes.iter_mut() {
            if p.is_relative() {
                *p = cwd.join(&p);
            }
        }
        Ok(())
    }

    fn record<T: TxnT + TxnTExt + MutTxnTExt, C: ChangeStore>(
        mut self,
        txn: &mut T,
        channel: &mut ChannelRef<T>,
        working_copy: &mut libpijul::working_copy::FileSystem,
        changes: &C,
        repo_path: &Path,
        header: ChangeHeader,
    ) -> Result<
        Option<(
            Change3,
            HashMap<usize, libpijul::InodeUpdate>,
            Option<libpijul::pristine::Hash>,
        )>,
        anyhow::Error,
    > {
        let mut state = libpijul::RecordBuilder::new();
        if self.prefixes.is_empty() {
            txn.record(
                &mut state,
                libpijul::Algorithm::default(),
                channel,
                working_copy,
                changes,
                ""
            )?
        } else {
            self.fill_relative_prefixes()?;
            working_copy.record_prefixes(
                txn,
                channel,
                changes,
                &mut state,
                repo_path,
                &self.prefixes,
            )?;
        }
        let mut rec = state.finish();
        if rec.actions.is_empty() {
            return Ok(None);
        }
        let actions = rec
            .actions
            .into_iter()
            .map(|rec| rec.globalize(txn))
            .collect();
        let change =
            LocalChange3::make_change(txn, channel, actions, rec.contents, header, Vec::new());

        let file_name = |local: &Local, _| -> String { format!("{}:{}", local.path, local.line) };
        debug!("has_binary = {:?}", rec.has_binary_files);
        let mut change = if self.all {
            change
        } else if rec.has_binary_files {
            return Err(Error::RecordBinaryChange.into());
        } else {
            let mut o = Vec::new();
            change.write(changes, None, file_name, true, &mut o)?;

            let mut change =
                std::io::BufReader::new(std::io::Cursor::new(edit::edit_bytes(&o[..])?));
            let change = Change3::read(&mut change, &mut rec.updatables)?;
            if change.changes.is_empty() {
                return Ok(None);
            }
            change
        };

        if change.header.message.trim().is_empty() {
            return Err(Error::NoChangeMessage.into());
        }
        let (dependencies, extra_known) = if self.tag {
            full_dependencies(txn, channel)
        } else {
            dependencies(txn, channel, change.changes.iter())
        };
        change.dependencies = dependencies;
        change.extra_known = extra_known;
        debug!("saving change");
        let hash = changes.save_change(&change).unwrap();
        debug!("saved");
        Ok(Some((change, rec.updatables, Some(hash))))
    }
}

#[derive(Debug, Serialize, Deserialize)]
struct Signature {
    public_key: String,
    timestamp: chrono::DateTime<chrono::Utc>,
    signature: String,
}

async fn sign_hash(
    key_path: &mut PathBuf,
    hash: libpijul::pristine::Hash,
) -> Result<Option<(String, String)>, anyhow::Error> {
    let to_sign = hash.to_bytes();
    match thrussh_keys::agent::client::AgentClient::connect_env().await {
        Ok(agent) => {
            let mut agent = Some(agent);
            for k in &["id_ed25519.pub", "id_rsa.pub"] {
                key_path.push(k);
                if let Ok(key) = thrussh_keys::load_public_key(&key_path) {
                    debug!("key");
                    if let Some(a) = agent.take() {
                        debug!("authenticate future");
                        if let (_, Ok(sig)) = a.sign_request_base64(&key, &to_sign).await {
                            key_path.pop();
                            let key = key.public_key_base64();
                            return Ok(Some((key, sig)));
                        }
                    }
                }
                key_path.pop();
            }
        }
        Err(e) => {
            error!("{:?}", e);
        }
    }
    for k in &["id_ed25519", "id_rsa"] {
        key_path.push(k);
        if let Some(k) = crate::remote::ssh::load_secret_key(&key_path, k) {
            key_path.pop();
            let pk = k.public_key_base64();
            return Ok(Some((pk, k.sign_detached(&to_sign)?.to_base64())));
        } else {
            key_path.pop();
        }
    }
    Ok(None)
}