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, ¤t_branch)?;
let untracked = untracked_files(&txn, &opts.repo_root)?;
let conflicts = txn.list_conflict_files(¤t_branch, &opts.cwd)?;
(unrecorded, untracked, conflicts)
};
if short {
print_shortstatus(&opts.cwd, unrecorded, untracked, conflicts);
} else {
print_longstatus(¤t_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,
}
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);
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)
}
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 => {
p.push("..");
for _ in c1 {
p.push("..");
}
p.push(r2.as_os_str());
p.push(c2.as_path());
break;
}
(None, Some(r2)) => {
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)));
}
}
}