pijul 0.7.2

A patch-based distributed version control system, easy to use and fast. Command-line interface.
use clap::{SubCommand, ArgMatches, Arg};
use chrono;
use commands::{BasicOptions, StaticSubcommand, ask};
use libpijul::{Repository, Hash, InodeUpdate, Patch};
use libpijul::fs_representation::patches_dir;
use std::mem::drop;
use error::{Result, ErrorKind};
use std::io::{stderr, Write};
use std::process::exit;

use std::path::Path;
use meta::{Global, Meta, KeyType, load_global_or_local_key};
use base64::URL_SAFE_NO_PAD;
use super::ask::{ChangesDirection, ask_changes};
use rand;

pub fn invocation() -> StaticSubcommand {
    return SubCommand::with_name("record")
        .about("record changes in the repository")
        .arg(Arg::with_name("repository")
             .long("repository")
             .help("The repository where to record, defaults to the current directory.")
             .takes_value(true)
             .required(false))
        .arg(Arg::with_name("branch")
             .long("branch")
             .help("The branch where to record, defaults to the current branch.")
             .takes_value(true)
             .required(false))
        .arg(Arg::with_name("all")
             .short("a")
             .long("all")
             .help("Answer 'y' to all questions")
             .takes_value(false))
        .arg(Arg::with_name("message")
             .short("m")
             .long("name")
             .help("The name of the patch to record")
             .takes_value(true))
        .arg(Arg::with_name("author")
             .short("A")
             .long("author")
             .help("Author of this patch (multiple occurrences allowed)")
             .takes_value(true))
        .arg(Arg::with_name("prefix")
             .help("Prefix to start from")
             .takes_value(true)
             .multiple(true))
}

pub fn run(args: &ArgMatches) -> Result<Option<Hash>> {
    let opts = BasicOptions::from_args(args)?;
    let yes_to_all = args.is_present("all");
    let patch_name_arg = args.value_of("message");
    let authors_arg = args.values_of("author").map(|x| x.collect::<Vec<_>>());
    let branch_name = opts.branch();

    let (changes, syncs) = {
        // Increase by 100 pages. The most things record can
        // write is one write in the branches table, affecting
        // at most O(log n) blocks.
        let repo = opts.open_and_grow_repo(409600)?;
        let mut txn = repo.mut_txn_begin(rand::thread_rng())?;
        let (changes, syncs) = txn.record(&branch_name, &opts.repo_root, None)?;
        if !yes_to_all {
            let c = try!(ask_changes(&txn, &changes, ChangesDirection::Record));
            let selected = changes.into_iter()
                .enumerate()
                .filter(|&(i, _)| *(c.get(&i).unwrap_or(&false)))
                .map(|(_, x)| x)
                .collect();
            txn.commit()?;
            (selected, syncs)
        } else {
            txn.commit()?;
            (changes, syncs)
        }
    };

    let mut save_meta = false;
    let mut save_global = false;

    let mut global = Global::load().unwrap_or_else(|e| {
        info!("loading global key, error {:?}", e);
        save_global = true;
        Global::new()
    });

    let mut meta = match Meta::load(&opts.repo_root) {
        Ok(m) => m,
        Err(_) => {
            save_meta = true;
            Meta::new()
        }
    };

    if changes.is_empty() {
        println!("Nothing to record");
        Ok(None)
    } else {
        // println!("patch: {:?}",changes);
        let repo = opts.open_repo()?;
        let patch = {
            let txn = repo.txn_begin()?;
            debug!("meta:{:?}", meta);
            let authors: Vec<String> = if let Some(ref authors) = authors_arg {
                let authors: Vec<String> = authors.iter().map(|x| x.to_string()).collect();
                {
                    if meta.authors.len() == 0 {
                        meta.authors = authors.clone();
                        save_meta = true
                    }
                    if global.author.len() == 0 {
                        global.author = authors[0].clone();
                        save_global = true
                    }
                }
                authors
            } else {
                if meta.authors.len() > 0 {
                    meta.authors.clone()
                } else if global.author.len() > 0 {
                    vec![global.author.clone()]
                } else {
                    let authors = try!(ask::ask_authors());
                    save_meta = true;
                    meta.authors = authors.clone();
                    save_global = true;
                    global.author = authors[0].clone();
                    authors
                }
            };

            debug!("authors:{:?}", authors);
            let patch_name = if let Some(ref m) = patch_name_arg {
                m.to_string()
            } else {
                try!(ask::ask_patch_name())
            };
            debug!("patch_name:{:?}", patch_name);
            if save_meta {
                meta.save(&opts.repo_root)?
            }
            if save_global {
                global.save()?
            }
            debug!("new");
            let changes = changes.into_iter().flat_map(|x| x.into_iter()).collect();
            let branch = txn.get_branch(&branch_name).unwrap();
            txn.new_patch(&branch, authors, patch_name, None, chrono::Utc::now(), changes)
        };
        drop(repo);

        let dot_pijul = opts.repo_dir();
        let key = if let Ok(Some(key)) = meta.signing_key() {
            Some(key)
        } else {
            load_global_or_local_key(Some(&dot_pijul), KeyType::Signing).ok()
        };
        debug!("key.is_some(): {:?}", key.is_some());
        let patches_dir = patches_dir(&opts.repo_root);
        let hash = patch.save(&patches_dir, key.as_ref())?;

        let pristine_dir = opts.pristine_dir();
        let mut increase = 409600;
        loop {
            match record_no_resize(&pristine_dir, &opts.repo_root, &branch_name, &hash, &patch,
                                   &syncs, increase) {
                Err(ref e) if e.lacks_space() => { increase *= 2 },
                e => return e
            }
        }
    }
}

fn record_no_resize(pristine_dir: &Path, r: &Path, branch_name: &str, hash: &Hash,
                    patch: &Patch, syncs: &[InodeUpdate], increase: u64)
                    -> Result<Option<Hash>> {

    let size_increase = increase + patch.size_upper_bound() as u64;
    let repo = match Repository::open(&pristine_dir, Some(size_increase)) {
        Ok(repo) => repo,
        Err(x) => {
            return Err(ErrorKind::Repository(x).into())
        }
    };
    let mut txn = try!(repo.mut_txn_begin(rand::thread_rng()));
    // save patch
    debug!("syncs: {:?}", syncs);
    txn.apply_local_patch(&branch_name, r, &hash, &patch, &syncs, false)?;
    txn.commit()?;
    println!("Recorded patch {}", hash.to_base64(URL_SAFE_NO_PAD));
    Ok(Some(hash.clone()))
}

pub fn explain(res: Result<Option<Hash>>) {
    match res {
        Ok(_) => (),
        Err(e) => {
            write!(stderr(), "error: {}", e).unwrap();
            exit(1)
        }
    }
}