pijul 0.6.0

A patch-based distributed version control system, easy to use and fast. Command-line interface.
use clap::{SubCommand, ArgMatches, Arg};

use commands::{StaticSubcommand, default_explain};
use error::Error;
use std::path::Path;
use std::fs::File;

use libpijul::fs_representation::find_repo_root;
use libpijul::patch::Patch;
use libpijul::{Hash, DEFAULT_BRANCH, ApplyTimestamp, PatchId};
use commands::remote;
use commands::ask::{ask_patches, Command};
use commands::get_wd;
use std::io::BufReader;
use super::get_current_branch;
use meta::{Meta, Repository, DEFAULT_REMOTE};

pub fn invocation() -> StaticSubcommand {
    return SubCommand::with_name("pull")
        .about("pull from a remote repository")
        .arg(Arg::with_name("remote").help("Repository from which to pull."))
        .arg(Arg::with_name("repository").help("Local repository."))
        .arg(Arg::with_name("remote_branch")
            .long("from-branch")
            .help("The branch to pull from")
            .takes_value(true))
        .arg(Arg::with_name("local_branch")
            .long("to-branch")
            .help("The branch to pull into")
            .takes_value(true))
        .arg(Arg::with_name("all")
             .short("a")
            .long("all")
            .help("Answer 'y' to all questions")
            .takes_value(false))
        .arg(Arg::with_name("set-default")
             .long("set-default")
             .help("Used with --set-remote, sets this remote as the default pull remote.")
        )
        .arg(Arg::with_name("set-remote")
             .long("set-remote")
             .takes_value(true)
        )
        .arg(Arg::with_name("port")
            .short("p")
            .long("port")
            .help("Port of the remote ssh server.")
            .takes_value(true)
            .validator(|val| {
                let x: Result<u16, _> = val.parse();
                match x {
                    Ok(_) => Ok(()),
                    Err(_) => Err(val),
                }
            }));
}

#[derive(Debug)]
pub struct Params<'a> {
    pub repository: Option<&'a Path>,
    pub remote_id: Option<&'a str>,
    pub set_remote: Option<&'a str>,
    pub yes_to_all: bool,
    pub set_default: bool,
    pub port: Option<u16>,
    pub local_branch: Option<&'a str>,
    pub remote_branch: &'a str,
}

pub fn parse_args<'a>(args: &'a ArgMatches) -> Params<'a> {
    Params {
        repository: args.value_of("repository").map(Path::new),
        remote_id: args.value_of("remote"),
        set_remote: args.value_of("set-remote"),
        yes_to_all: args.is_present("all"),
        set_default: args.is_present("set-default"),
        port: args.value_of("port").and_then(|x| Some(x.parse().unwrap())),
        local_branch: args.value_of("local_branch"),
        remote_branch: args.value_of("remote_branch").unwrap_or(DEFAULT_BRANCH),
    }
}

fn fetch_pullable_patches(session: &mut remote::Session,
                          pullable: &[(Hash, ApplyTimestamp)],
                          r: &Path)
                          -> Result<Vec<(Hash, Option<PatchId>, Patch)>, Error> {
    let mut patches = Vec::new();
    for &(ref i, _) in pullable {
        let (hash, _, patch) = {
            let filename = try!(session.download_patch(r, i));
            debug!("filename {:?}", filename);
            let file = try!(File::open(&filename));
            let mut file = BufReader::new(file);
            Patch::from_reader_compressed(&mut file)?
        };
        assert_eq!(&hash, i);
        patches.push((hash, None, patch));
    }
    Ok(patches)
}

pub fn select_patches(interactive: bool,
                      session: &mut remote::Session,
                      remote_branch: &str,
                      local_branch: &str,
                      r: &Path)
                      -> Result<Vec<(Hash, ApplyTimestamp)>, Error> {
    let pullable = try!(session.pullable_patches(remote_branch, local_branch, r));
    let mut pullable:Vec<_> = pullable.iter().collect();
    pullable.sort_by(|&(_, a), &(_, b)| a.cmp(&b));
    if interactive {
        let selected = {
            let patches = try!(fetch_pullable_patches(session, &pullable, r));
            try!(ask_patches(Command::Pull, &patches[..]))
        };
        Ok(pullable.into_iter()
           .filter(|&(ref h, _)| selected.contains(h))
           .collect())
    } else {
        Ok(pullable)
    }
}

pub fn run(args: &Params) -> Result<(), Error> {
    debug!("pull args {:?}", args);
    let wd = try!(get_wd(args.repository));
    match find_repo_root(&wd) {
        None => return Err(Error::NotInARepository),
        Some(ref r) => {
            let mut meta = Meta::load(r).unwrap_or(Meta::new());
            {
                let remote = meta.pull(args.remote_id, args.port, Some(r))?;
                let mut session = remote.session()?;

                let local_branch = if let Some(b) = args.local_branch {
                    b.to_string()
                } else if let Ok(b) = get_current_branch(r) {
                    b
                } else {
                    DEFAULT_BRANCH.to_string()
                };

                let pullable = select_patches(!args.yes_to_all,
                                              &mut session,
                                              args.remote_branch,
                                              &local_branch,
                                              r)?;

                // Pulling and applying
                info!("Pulling patch {:?}", pullable);
                session.pull(r, &local_branch, &pullable)?;
            }
            info!("Saving meta");
            let set_remote = if args.set_default && args.set_remote.is_none() {
                Some(DEFAULT_REMOTE)
            } else {
                args.set_remote
            };
            if let (Some(set_remote), Some(remote_id)) = (set_remote, args.remote_id) {
                let mut repo = Repository::default();
                repo.address = remote_id.to_string();
                repo.port = args.port;

                meta.remote.insert(set_remote.to_string(), repo);
                if args.set_default {
                    meta.pull = Some(set_remote.to_string());
                }
                meta.save(r)?;
            }
            Ok(())
        }
    }
}

pub fn explain(res: Result<(), Error>) {
    default_explain(res)
}