use clap::{SubCommand, ArgMatches, Arg};
use commands::{StaticSubcommand, default_explain};
use libpijul::Repository;
use libpijul::fs_representation::{pristine_dir, find_repo_root, PIJUL_DIR_NAME};
use libpijul::patch::Record;
use libpijul::MutTxn;
use error::{Error, ErrorKind};
use rand;
use ignore::{WalkBuilder};
use super::{get_current_branch, get_wd};
use std::path::{Path, PathBuf};
use std::env;
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)
"#;
pub fn invocation() -> StaticSubcommand {
SubCommand::with_name("status")
.about("Show working tree status")
.arg(Arg::with_name("short")
.long("short")
.short("s")
.help("Output in short format"))
}
pub struct Params {
pub short: bool,
}
pub fn explain(r: Result<(), Error>) {
default_explain(r)
}
pub fn run(arg_matches: &ArgMatches) -> Result<(), Error> {
let params = Params {
short: arg_matches.is_present("short")
};
let wd = get_wd(None)?;
let repo_root = if let Some(root) = find_repo_root(&wd) {
root
} else {
return Err(ErrorKind::NotInARepository.into())
};
let pristine_dir = pristine_dir(&repo_root);
let repo = Repository::open(&pristine_dir, Some(409600))?;
let current_branch = get_current_branch(&repo_root)?;
let (unrecorded, untracked) = {
let mut txn = repo.mut_txn_begin(rand::thread_rng())?;
let unrecorded = unrecorded_changes(&mut txn, &repo_root, ¤t_branch)?;
let untracked = untracked_files(&txn, &repo_root)?;
(unrecorded, untracked)
};
let cwd = env::current_dir()?;
if params.short {
print_shortstatus(&cwd, unrecorded, untracked);
} else {
print_longstatus(¤t_branch, &cwd, unrecorded, untracked);
}
Ok(())
}
fn print_longstatus(branch: &String,
cwd: &Path,
changed: Vec<(Rc<PathBuf>, ChangeType)>,
untracked: Vec<PathBuf>) {
println!("On branch {}", branch);
if changed.is_empty() && untracked.is_empty() {
println!("Nothing to record, working tree clean");
}
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>) {
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)));
}
}
}