pijul 1.0.0-alpha.9

The sound distributed version control system.
use std::collections::HashSet;
use std::io::Write;
use std::path::PathBuf;

use clap::Clap;
use lazy_static::lazy_static;
use libpijul::changestore::ChangeStore;
use libpijul::{MutTxnT, MutTxnTExt, TxnT, TxnTExt};
use log::debug;
use regex::Regex;

use crate::repository::Repository;

#[derive(Clap, Debug)]
pub struct Remote {
    #[clap(subcommand)]
    subcmd: Option<SubRemote>,
    /// Set the repository where this command should run. Defaults to the first ancestor of the current directory that contains a `.pijul` directory.
    #[clap(long = "repository")]
    repo_path: Option<PathBuf>,
}

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

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

#[derive(Clap, Debug)]
pub struct Push {
    /// Path to the repository. Uses the current repository if the argument is omitted
    #[clap(long = "repository")]
    repo_path: Option<PathBuf>,
    /// Push from this channel instead of the default channel
    #[clap(long = "channel")]
    channel: Option<String>,
    /// Push all changes
    #[clap(long = "all", short = 'a', conflicts_with = "changes")]
    all: bool,
    /// Do not check certificates (HTTPS remotes only, this option might be dangerous)
    #[clap(short = 'k')]
    no_cert_check: bool,
    /// Push changes only relating to these paths
    #[clap(long = "path")]
    path: Option<String>,
    /// Push to this remote
    to: Option<String>,
    /// Push to this remote channel instead of the remote's default channel
    #[clap(long = "to-channel")]
    to_channel: Option<String>,
    /// Push only these changes
    #[clap(last = true)]
    changes: Vec<String>,
}

#[derive(Clap, Debug)]
pub struct Pull {
    /// Set the repository where this command should run. Defaults to the first ancestor of the current directory that contains a `.pijul` directory.
    #[clap(long = "repository")]
    repo_path: Option<PathBuf>,
    /// Pull into this channel instead of the current channel
    #[clap(long = "channel")]
    channel: Option<String>,
    /// Push all changes
    #[clap(long = "all", short = 'a', conflicts_with = "changes")]
    all: bool,
    /// Do not check certificates (HTTPS remotes only, this option might be dangerous)
    #[clap(short = 'k')]
    no_cert_check: bool,
    /// Download full changes, even when not necessory
    #[clap(long = "full")]
    full: bool, // This can't be symmetric with push
    /// Only pull to these paths
    #[clap(long = "path")]
    path: Option<String>,
    /// Pull from this remote
    from: Option<String>,
    /// Pull from this remote channel
    #[clap(long = "from-channel")]
    from_channel: Option<String>,
    /// Pull changes from the local repository, not necessarily from a channel
    #[clap(last = true)]
    changes: Vec<String>, // For local changes only, can't be symmetric.
}

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

impl Push {
    pub async fn run(self) -> Result<(), anyhow::Error> {
        let mut stderr = std::io::stderr();
        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(
                Some(&repo.path),
                &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 let Some(ref remote_changes) = remote_changes {
                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)
                    }
                }
            } else if let crate::remote::RemoteRepo::LocalChannel(ref remote_channel) = remote {
                if let Some(channel) = txn.load_channel(remote_channel) {
                    let channel = channel.borrow();
                    let h_int = txn.get_internal(h).unwrap();
                    if txn.get_changeset(&channel.changes, h_int, None).is_none() {
                        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)
                        }
                    }
                }
            }
        }
        to_upload.reverse();
        debug!("to_upload = {:?}", to_upload);

        if to_upload.is_empty() {
            writeln!(stderr, "Nothing to push")?;
            return Ok(());
        }

        let to_upload = if !self.changes.is_empty() {
            let mut u = Vec::new();
            let mut not_found = Vec::new();
            for change in self.changes.iter() {
                match txn.hash_from_prefix(change) {
                    Ok((hash, _)) => {
                        if to_upload.contains(&hash) {
                            u.push(hash);
                        }
                    }
                    Err(_) => {
                        if !not_found.contains(change) {
                            not_found.push(change.to_string());
                        }
                    }
                }
            }

            if !not_found.is_empty() {
                return Err((crate::Error::ChangesNotFound { hashes: not_found }).into());
            }

            check_deps(&repo.changes, &to_upload, &u)?;
            u
        } else if self.all {
            to_upload
        } else {
            let mut o = make_changelist(&repo.changes, &to_upload)?;
            loop {
                let d = parse_changelist(&edit::edit_bytes(&o[..])?);
                let comp = complete_deps(&repo.changes, &to_upload, &d)?;
                if comp.len() == d.len() {
                    break comp;
                }
                o = make_changelist(&repo.changes, &comp)?
            }
        };
        debug!("to_upload = {:?}", to_upload);

        if to_upload.is_empty() {
            writeln!(stderr, "Nothing to push")?;
            return Ok(());
        }

        remote
            .upload_changes(&mut txn, 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(
                Some(&repo.path),
                &remote_name,
                from_channel,
                self.no_cert_check,
            )
            .await?;
        debug!("downloading");

        let to_download = if self.changes.is_empty() {
            let mut paths = if let Some(p) = self.path {
                vec![p]
            } else {
                vec![]
            };
            let remote_changes = remote.update_changelist(&mut txn, &paths).await?;
            debug!("changelist done");
            let mut to_download = Vec::new();
            if let Some(ref remote_changes) = remote_changes {
                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)
                    }
                }
            } else if let crate::remote::RemoteRepo::LocalChannel(ref remote_channel) = remote {
                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
                };
                if let Some(remote_channel) = txn.load_channel(remote_channel) {
                    let remote_channel = remote_channel.borrow();
                    for (_, (h, m)) in txn.reverse_log(&remote_channel, None) {
                        if txn.channel_has_state(&channel, m) {
                            break;
                        }
                        let h_int = txn.get_internal(h).unwrap();
                        if txn
                            .get_changeset(&channel.borrow().changes, h_int, None)
                            .is_none()
                        {
                            if let Some(ref p) = path {
                                if txn.get_touched_files(*p, Some(h_int)).is_some() {
                                    to_download.push(h)
                                }
                            } else {
                                to_download.push(h)
                            }
                        }
                    }
                }
            }
            to_download.reverse();
            to_download
        } else {
            let r: Result<Vec<libpijul::Hash>, anyhow::Error> = self
                .changes
                .iter()
                .map(|h| Ok(txn.hash_from_prefix(h)?.0))
                .collect();
            r?
        };
        if to_download.is_empty() {
            let mut stderr = std::io::stderr();
            writeln!(stderr, "Nothing to pull")?;
            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 {
            let actions = recorded
                .actions
                .into_iter()
                .map(|rec| rec.globalize(&txn))
                .collect();
            let mut pending_change = libpijul::change::Change::make_change(
                &txn,
                &channel,
                actions,
                recorded.contents,
                libpijul::change::ChangeHeader::default(),
                Vec::new(),
            );
            let (dependencies, extra_known) =
                libpijul::change::dependencies(&txn, &channel, pending_change.changes.iter());
            pending_change.dependencies = dependencies;
            pending_change.extra_known = extra_known;
            let hash = repo.changes.save_change(&pending_change).unwrap();
            txn.apply_local_change(&mut channel, &pending_change, hash, &recorded.updatables)?;
            Some(hash)
        };
        remote
            .pull(
                &mut repo,
                &mut txn,
                &mut channel,
                to_download.clone(),
                self.all,
            )
            .await?;
        if !self.all {
            let mut o = make_changelist(&repo.changes, &to_download)?;
            let d = loop {
                let d = parse_changelist(&edit::edit_bytes(&o[..])?);
                let comp = complete_deps(&repo.changes, &to_download, &d)?;
                if comp.len() == d.len() {
                    break comp;
                }
                o = make_changelist(&repo.changes, &comp)?
            };
            let mut ws = libpijul::ApplyWorkspace::new();
            debug!("to_download = {:?}", to_download);
            let progress = indicatif::ProgressBar::new(d.len() as u64);
            progress.set_style(
                indicatif::ProgressStyle::default_spinner()
                    .template("  Applying changes    {wide_bar}  {pos}/{len}"),
            );
            for h in d.iter() {
                txn.apply_change_rec_ws(&repo.changes, &mut channel, *h, &mut ws)?;
                progress.inc(1);
            }
            progress.set_style(
                indicatif::ProgressStyle::default_bar()
                    .template("✓ Applying changes    {wide_bar} {pos}/{len}"),
            );
            progress.finish();
        }
        debug!("completing changes");
        remote
            .complete_changes(&repo, &txn, &mut channel, &to_download, self.full)
            .await?;
        remote.finish().await?;

        let progress = indicatif::ProgressBar::new_spinner();
        progress.set_style(
            indicatif::ProgressStyle::default_spinner().template("{spinner} Outputting repository"),
        );
        progress.enable_steady_tick(100);
        txn.output_repository_no_pending(
            &mut repo.working_copy,
            &repo.changes,
            &mut channel,
            "",
            true,
        )?;
        progress.set_style(
            indicatif::ProgressStyle::default_spinner().template("✓ Outputting repository"),
        );
        progress.finish();

        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::Hash],
) -> Result<Vec<u8>, anyhow::Error> {
    use libpijul::Base32;
    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/pushed.\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 deps = changes.get_dependencies(&p)?;
        if !deps.is_empty() {
            write!(v, "  Dependencies:").unwrap();
            for d in deps {
                write!(v, " {}", d.to_base32()).unwrap();
            }
            writeln!(v).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();
        }
        if let Some(desc) = change.description {
            writeln!(v).unwrap();
            for l in desc.lines() {
                writeln!(v, "    {}", l).unwrap();
            }
        }
    }
    Ok(v)
}

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

fn complete_deps<C: ChangeStore>(
    c: &C,
    original: &[libpijul::Hash],
    now: &[libpijul::Hash],
) -> Result<Vec<libpijul::Hash>, anyhow::Error> {
    let original_: HashSet<_> = original.iter().collect();
    let mut now_ = HashSet::new();
    let mut result = Vec::new();
    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() {
                result.push(d);
                now_.insert(d);
            }
        }
        if now_.insert(*n) {
            result.push(*n)
        }
    }
    Ok(result)
}

fn check_deps<C: ChangeStore>(
    c: &C,
    original: &[libpijul::Hash],
    now: &[libpijul::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(())
}