pijul 1.0.0-alpha

The sound distributed version control system.
use crate::repository::Repository;
use libpijul::changestore::ChangeStore;
use libpijul::pristine::{MutTxnT, TxnT};
use libpijul::{MutTxnTExt, TxnTExt};
use regex::Regex;
use std::collections::HashSet;
use std::path::PathBuf;

#[derive(Clap, Debug)]
pub struct Remote {
    #[clap(subcommand)]
    subcmd: Option<SubRemote>,
    #[clap(long = "repository")]
    repo_path: Option<PathBuf>,
}

#[derive(Clap, Debug)]
pub enum SubRemote {
    #[clap(name = "delete")]
    Delete { remote: String },
    #[clap(name = "list")]
    List,
}

impl Remote {
    pub fn run(self) -> Result<(), anyhow::Error> {
        let repo = Repository::find_root(self.repo_path)?;
        debug!("{:?}", repo.config);
        match self.subcmd {
            None | Some(SubRemote::List) => {
                let txn = repo.pristine.txn_begin()?;
                for r in txn.iter_remotes("") {
                    println!("  {}", r.name());
                }
            }
            Some(SubRemote::Delete { remote }) => {
                let mut txn = repo.pristine.mut_txn_begin();
                if !txn.drop_named_remote(&remote)? {
                    eprintln!("Remote not found: {:?}", remote)
                } else {
                    txn.commit()?;
                }
            }
        }
        Ok(())
    }
}

#[derive(Clap, Debug)]
pub struct Push {
    #[clap(long = "repository")]
    repo_path: Option<PathBuf>,
    #[clap(long = "channel")]
    channel: Option<String>,
    #[clap(short = 'a')]
    all: bool,
    #[clap(short = 'k', about="Do not check certificates")]
    no_cert_check: bool,

    #[clap(last = true)]
    changes: Vec<String>,
    #[clap(long = "path")]
    path: Option<String>,

    to: Option<String>,
    #[clap(long = "to-channel")]
    to_channel: Option<String>,
}

#[derive(Clap, Debug)]
pub struct Pull {
    #[clap(long = "repository")]
    repo_path: Option<PathBuf>,
    #[clap(long = "channel")]
    channel: Option<String>,
    #[clap(long = "all", short = 'a')]
    all: bool,
    #[clap(short = 'k', about="Do not check certificates")]
    no_cert_check: bool,

    #[clap(long = "full", about="Download full changes, even when not necessary")]
    full: bool, // This can't be symmetric with push
    #[clap(last = true, about = "Pull changes from the local repository, not necessarily from a channel")]
    changes: Vec<String>, // For local changes only, can't be symmetric.

    #[clap(long = "path")]
    path: Option<String>,

    from: Option<String>,
    #[clap(long = "from-channel")]
    from_channel: Option<String>,
}

lazy_static! {
    static ref CHANNEL: Regex = Regex::new(r#"([^:]*)(:(.*))?"#).unwrap();
}

impl Push {
    pub async fn run(self) -> Result<(), anyhow::Error> {
        let repo = Repository::find_root(self.repo_path)?;
        debug!("{:?}", repo.config);
        let channel_name = repo.config.get_current_channel(self.channel.as_ref());
        let remote_name = if let Some(ref rem) = self.to {
            rem
        } else if let Some(ref def) = repo.config.default_remote {
            def
        } else {
            return Err(crate::Error::MissingRemote.into());
        };
        let mut push_channel = None;
        let remote_channel = if let Some(ref c) = self.to_channel {
            let c = CHANNEL.captures(c).unwrap();
            push_channel = c.get(3).map(|x| x.as_str());
            let c = c.get(1).unwrap().as_str();
            if c.is_empty() {
                channel_name
            } else {
                c
            }
        } else {
            channel_name
        };
        debug!("remote_channel = {:?} {:?}", remote_channel, push_channel);
        let mut remote = repo.remote(&remote_name, remote_channel, self.no_cert_check).await?;
        let mut txn = repo.pristine.mut_txn_begin();
        let mut paths = if let Some(p) = self.path {
            vec![p.to_string()]
        } else {
            vec![]
        };
        let remote_changes = remote.update_changelist(&mut txn, &paths).await?;
        let channel = txn.open_or_create_channel(channel_name)?;

        let path = if let Some(path) = paths.pop() {
            let (p, ambiguous) = txn.follow_oldest_path(&repo.changes, &channel, &path)?;
            if ambiguous {
                return Err((crate::Error::AmbiguousPath { path: path.clone() }).into())
            }
            Some(p)
        } else {
            None
        };

        let mut to_upload = Vec::new();
        for (_, (h, m)) in txn.reverse_log(&channel.borrow(), None) {
            if txn.remote_has_state(&remote_changes, m) {
                break;
            }
            let h_int = txn.get_internal(h).unwrap();
            if !txn.remote_has_change(&remote_changes, h) {
                if let Some(ref p) = path {
                    if txn.get_touched_files(*p, Some(h_int)).is_some() {
                        to_upload.push(h)
                    }
                } else {
                    to_upload.push(h)
                }
            }
        }
        if to_upload.is_empty() {
            return Ok(());
        }

        to_upload.reverse();
        debug!("to_upload = {:?}", to_upload);
        let to_upload = if !self.all {
            let o = make_changelist(&repo.changes, &to_upload)?;
            let u = parse_changelist(&edit::edit_bytes(&o[..])?);
            check_deps(&repo.changes, &to_upload, &u)?;
            u
        } else {
            to_upload
        };
        remote
            .upload_changes(repo.changes_dir.clone(), push_channel, &to_upload)
            .await?;
        txn.commit()?;

        remote.finish().await?;
        Ok(())
    }
}

impl Pull {
    pub async fn run(self) -> Result<(), anyhow::Error> {
        let mut repo = Repository::find_root(self.repo_path)?;
        let mut txn = repo.pristine.mut_txn_begin();
        let channel_name = repo.config.get_current_channel(self.channel.as_ref());
        let mut channel = txn.open_or_create_channel(channel_name)?;
        debug!("{:?}", repo.config);
        let remote_name = if let Some(ref rem) = self.from {
            rem
        } else if let Some(ref def) = repo.config.default_remote {
            def
        } else {
            return Err(crate::Error::MissingRemote.into());
        };
        let from_channel = if let Some(ref c) = self.from_channel {
            c
        } else {
            crate::DEFAULT_CHANNEL
        };
        let mut remote = repo.remote(&remote_name, from_channel, self.no_cert_check).await?;
        debug!("downloading");

        let to_download = if self.changes.is_empty() {
            let paths = if let Some(p) = self.path {
                vec![p.to_string()]
            } else {
                vec![]
            };
            let remote_changes = remote.update_changelist(&mut txn, &paths).await?;
            debug!("changelist done");
            let mut to_download = Vec::new();
            for (_, (h, m)) in txn.iter_remote(&remote_changes.borrow().remote, 0) {
                if txn.channel_has_state(&channel, m) {
                    break;
                } else if txn.get_revchanges(&channel, h).is_none() {
                    to_download.push(h)
                }
            }
            to_download.reverse();
            to_download
        } else {
            let r: Result<Vec<libpijul::pristine::Hash>, anyhow::Error> = self
                .changes
                .iter()
                .map(|h| Ok(txn.hash_from_prefix(h)?.0))
                .collect();
            r?
        };
        if to_download.is_empty() {
            return Ok(());
        }
        debug!("recording");
        let recorded = txn.record_all(
            libpijul::Algorithm::default(),
            &mut channel,
            &mut repo.working_copy,
            &repo.changes,
            "",
        )?;
        let hash = if recorded.actions.is_empty() {
            None
        } else {
            Some(txn.apply_recorded(&mut channel, recorded, &repo.changes)?)
        };
        remote
            .pull(
                &mut repo,
                &mut txn,
                &mut channel,
                to_download.clone(),
                self.all,
            )
            .await?;
        if !self.all {
            let o = make_changelist(&repo.changes, &to_download)?;
            let d = parse_changelist(&edit::edit_bytes(&o[..])?);
            check_deps(&repo.changes, &to_download, &d)?;
            let mut ws = libpijul::ApplyWorkspace::new();
            debug!("to_download = {:?}", to_download);
            for h in d.iter() {
                txn.apply_change_rec_ws(&repo.changes, &mut channel, *h, &mut ws)?;
            }
        }
        debug!("completing changes");
        remote
            .complete_changes(&repo, &txn, &mut channel, &to_download, self.full)
            .await?;
        remote.finish().await?;

        txn.output_repository_no_pending(
            &mut repo.working_copy,
            &repo.changes,
            &mut channel,
            "",
            true,
        )?;

        if let Some(h) = hash {
            txn.unrecord(&repo.changes, &mut channel, &h)?;
            repo.changes.del_change(&h)?;
        }

        txn.commit()?;
        Ok(())
    }
}

/// Make the "changelist", i.e. the list of patches, editable in a
/// text editor.
fn make_changelist<S: ChangeStore>(
    changes: &S,
    pullable: &[libpijul::pristine::Hash],
) -> Result<Vec<u8>, anyhow::Error> {
    use libpijul::pristine::Base32;
    use std::io::Write;
    let mut v = Vec::new();
    writeln!(
        v,
        "# Please select the changes to pull. The lines that contain just a
# valid hash, and no other character (except possibly a newline), will
# be pulled.\n"
    )
    .unwrap();
    let mut first_p = true;
    for p in pullable {
        if !first_p {
            writeln!(v, "").unwrap();
        }
        first_p = false;
        writeln!(v, "{}\n", p.to_base32()).unwrap();
        let change = changes.get_header(&p)?;
        write!(v, "  Author: [").unwrap();
        let mut first = true;
        for a in change.authors.iter() {
            if !first {
                write!(v, ", ").unwrap();
            }
            first = false;
            write!(v, "{}", a).unwrap();
        }
        writeln!(v, "]").unwrap();
        writeln!(v, "  Date: {}\n", change.timestamp).unwrap();
        for l in change.message.lines() {
            writeln!(v, "    {}", l).unwrap();
        }
    }
    Ok(v)
}

fn parse_changelist(o: &[u8]) -> Vec<libpijul::pristine::Hash> {
    use libpijul::pristine::Base32;
    if let Ok(o) = std::str::from_utf8(o) {
        o.lines()
            .filter_map(|l| libpijul::pristine::Hash::from_base32(l.as_bytes()))
            .collect()
    } else {
        Vec::new()
    }
}

fn check_deps<C: ChangeStore>(
    c: &C,
    original: &[libpijul::pristine::Hash],
    now: &[libpijul::pristine::Hash],
) -> Result<(), anyhow::Error> {
    let original_: HashSet<_> = original.iter().collect();
    let now_: HashSet<_> = now.iter().collect();
    for n in now {
        // check that all of `now`'s deps are in now or not in original
        for d in c.get_dependencies(n)? {
            if original_.get(&d).is_some() && now_.get(&d).is_none() {
                return Err((crate::Error::MissingDep { h: *n }).into());
            }
        }
    }
    Ok(())
}