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 commands::{BasicOptions, StaticSubcommand, default_explain};
use libpijul::fs_representation::PIJUL_DIR_NAME;
use libpijul::patch::Record;
use libpijul::MutTxn;
use error::Error;
use rand;
use ignore::WalkBuilder;

use std::path::{Path, PathBuf};
use std::rc::Rc;

const UNRECORDED_FILES: &'static str = r#"
Changes not yet recorded:
  (use "pijul record ..." to record a new patch)
"#;

const UNTRACKED_FILES: &'static str = r#"
Untracked files:
  (use "pijul add <file>..." to track them)
"#;

const CONFLICTED_FILES: &'static str = r#"
Unresolved conflicts:
  (fix conflicts and record the resolution with "pijul record ...")
"#;

pub fn invocation() -> StaticSubcommand {
    SubCommand::with_name("status")
        .about("Show working tree status")
        .arg(Arg::with_name("repository")
             .long("repository")
             .takes_value(true)
             .help("Local repository."))
        .arg(Arg::with_name("branch")
             .long("branch")
             .help("The branch to output, defaults to the current branch.")
             .takes_value(true)
             .required(false))
        .arg(Arg::with_name("short")
             .long("short")
             .short("s")
             .help("Output in short format"))
}

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

pub fn run(args: &ArgMatches) -> Result<(), Error> {
    let opts = BasicOptions::from_args(args)?;
    let current_branch = opts.branch();
    println!("On branch {}", current_branch);
    let repo = opts.open_and_grow_repo(409600)?;
    let short = args.is_present("short");

    let (unrecorded, untracked, conflicts) = {
        let mut txn = repo.mut_txn_begin(rand::thread_rng())?;
        let unrecorded = unrecorded_changes(&mut txn, &opts.repo_root, &current_branch)?;
        let untracked = untracked_files(&txn, &opts.repo_root)?;
        let conflicts = txn.list_conflict_files(&current_branch, &opts.cwd)?;
        (unrecorded, untracked, conflicts)
    };

    if short {
        print_shortstatus(&opts.cwd, unrecorded, untracked, conflicts);
    } else {
        print_longstatus(&current_branch, &opts.cwd, unrecorded, untracked,
                         conflicts);
    }
    Ok(())
}

fn print_longstatus(branch: &String,
                    cwd: &Path,
                    changed: Vec<(Rc<PathBuf>, ChangeType)>,
                    untracked: Vec<PathBuf>,
                    conflicts: Vec<PathBuf>) {
    println!("On branch {}", branch);
    if changed.is_empty() && untracked.is_empty() && conflicts.is_empty() {
        println!("Nothing to record, working tree clean");
    }

    if !conflicts.is_empty() {
        println!("{}", CONFLICTED_FILES);
        for f in conflicts {
            println!("        {}", relativize(&cwd, f.as_path()).display());
        }
    }

    if !changed.is_empty() {
        println!("{}", UNRECORDED_FILES);
        for (f, t) in changed {
            println!("        {:10} {}", t.long(), relativize(&cwd, f.as_path()).display());
        }
    }

    if !untracked.is_empty() {
        println!("{}", UNTRACKED_FILES);
        for f in untracked {
            println!("        {}", relativize(&cwd, f.as_path()).display());
        }
    }
}

fn print_shortstatus(cwd: &Path,
                     changed: Vec<(Rc<PathBuf>, ChangeType)>,
                     untracked: Vec<PathBuf>,
                     conflicts: Vec<PathBuf>) {
    for f in conflicts {
        println!("C {}", relativize(&cwd, f.as_path()).display());
    }
    for (f, t) in changed {
        println!("{} {}", t.short(), relativize(&cwd, f.as_path()).display());
    }
    for f in untracked {
        println!("? {}", relativize(&cwd, f.as_path()).display());
    }
}

#[derive(Debug)]
enum ChangeType {
    Modified,
    New,
    Del,
//    Mv(Rc<PathBuf>)
}

impl ChangeType {
    fn short(&self) -> &str {
        match *self {
            ChangeType::Modified => "M",
            ChangeType::New => "A",
            ChangeType::Del => "D",
        }
    }

    fn long(&self) -> &str {
        match *self {
            ChangeType::Modified => "modified:",
            ChangeType::New => "new file:",
            ChangeType::Del => "deleted:",
        }
    }
}

fn unrecorded_changes<T: rand::Rng>(txn: &mut MutTxn<T>,
                                    repo_root: &PathBuf,
                                    branch: &String) -> Result<Vec<(Rc<PathBuf>, ChangeType)>, Error> {

    let (changes, _) = txn.record(branch, repo_root, None)?;

    let mut ret = vec![];
    let mut current_file = None;

    for change in changes.iter() {
        match *change {
            Record::Change { ref file, .. } | Record::Replace { ref file, .. } => {
                if current_file.clone().map_or(true, |f| &f != file) {
                    ret.push((file.clone(), ChangeType::Modified));
                    current_file = Some(file.clone());
                }
            }
            Record::FileAdd { ref name, .. } => {
                let file = Rc::new(PathBuf::from(name.clone()));
                current_file = Some(file.clone());
                ret.push((file.clone(), ChangeType::New));
            }
            Record::FileDel { ref name, .. } => {
                let file = Rc::new(PathBuf::from(name.clone()));
                current_file = Some(file.clone());
                ret.push((file.clone(), ChangeType::Del));
            }
            _ => {}
        }
    }
    Ok(ret)
}

fn untracked_files<T: rand::Rng>(txn: &MutTxn<T>, repo_root: &PathBuf) -> Result<Vec<PathBuf>, Error> {
    let known_files = txn.list_files().unwrap_or_else(|_| vec!());

    let mut w = WalkBuilder::new(repo_root);
    w.git_ignore(false)
        .git_exclude(false)
        .git_global(false);

    // add .pijul/local/ignore
    w.add_ignore(repo_root.join(PIJUL_DIR_NAME).join("local").join("ignore"));

    let mut ret = vec![];
    for f in w.build() {
        if let Ok(f) = f {
            let p = f.path();
            if p == repo_root {
                continue;
            }
            let pb = p.to_path_buf();

            if let Ok(stripped) = p.strip_prefix(repo_root) {
                if known_files.iter().any(|t| *t == stripped) {
                    continue;
                }
            }
            ret.push(pb);
        }
    }
    Ok(ret)
}

/// Produce a PathBuf with the relative path from cwd to the given file
///
/// We assume cwd is a folder and file is a file. We also assume they
/// share a common root (at least the repository root).
///
/// cwd=/a/b/c/e
/// fil=/a/b/c/d/foo.rs
///
/// relativize(cwd, fil) #=> ../d/foo.rs
fn relativize(cwd: &Path, file: &Path) -> PathBuf {
    let mut p = PathBuf::new();
    let mut c1 = cwd.components();
    let mut c2 = file.parent().unwrap().components();
    loop {
        match (c1.next(), c2.next()) {
            (Some(r1), Some(r2)) if r1 != r2 => {
                // paths diverge
                p.push("..");
                for _ in c1 {
                    p.push("..");
                }
                p.push(r2.as_os_str());
                p.push(c2.as_path());
                break;
            }
            (None, Some(r2)) => {
                // Remaining stuff is relative path
                p.push(r2.as_os_str());
                p.push(c2.as_path());
                break;
            }
            (Some(_), None) => {
                p.push("..");
                for _ in c1 {
                    p.push("..");
                }
                break;
            }
            (None, None) => {
                break;
            }
            (Some(_), Some(_)) => {}
        }
    }
    let name = file.file_name().unwrap();
    p.push(name);
    p.set_file_name(name);
    debug!("rel({}, {})={}", cwd.display(), file.display(), p.display());
    p
}


#[cfg(test)]
mod test {
    use super::relativize;
    use std::path::{Path, PathBuf};

    #[test]
    fn test_relativize() {
        let cases = vec![
            ("/a/b/c", "/a/b/c/foo.rs", "foo.rs"),
            ("/a/b/c", "/a/b/c/d/foo.rs", "d/foo.rs"),
            ("/a/b/c/e", "/a/b/c/foo.rs", "../foo.rs"),
            ("/a/b/c/e", "/a/b/c/d/foo.rs", "../d/foo.rs"),
            ("/a/b/c/d/e", "/a/b/c/foo.rs", "../../foo.rs"),
            ("/home/foo/rust/pijul", "/home/foo/rust/pijul/Cargo.lock", "Cargo.lock")
        ];
        for (root, file, expected) in cases {
            assert_eq!(PathBuf::from(expected), relativize(&Path::new(root), &Path::new(file)));
        }
    }
}