use crate::meta::git_file_status::GitFileStatus;
use std::path::{Path, PathBuf};
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub enum GitStatus {
#[default]
Default,
Unmodified,
Ignored,
NewInIndex,
NewInWorkdir,
Typechange,
Deleted,
Renamed,
Modified,
Conflicted,
}
pub struct GitCache {
#[cfg(not(feature = "no-git"))]
statuses: Vec<(PathBuf, git2::Status)>,
}
#[cfg(feature = "no-git")]
impl GitCache {
pub fn new(_: &Path) -> Self {
Self {}
}
pub fn get(&self, _filepath: &PathBuf, _is_directory: bool) -> Option<GitFileStatus> {
None
}
}
#[cfg(not(feature = "no-git"))]
impl GitCache {
pub fn new(path: &Path) -> GitCache {
let repo = match git2::Repository::discover(path) {
Ok(r) => r,
Err(_e) => {
return Self::empty();
}
};
if let Some(workdir) = repo.workdir().and_then(|x| std::fs::canonicalize(x).ok()) {
let mut statuses = Vec::new();
match repo.statuses(None) {
Ok(status_list) => {
for status_entry in status_list.iter() {
let str_path = status_entry.path().unwrap();
let path: PathBuf =
str_path.split('/').collect::<Vec<_>>().iter().collect();
let path = workdir.join(path);
let elem = (path, status_entry.status());
statuses.push(elem);
}
}
Err(err) => {
crate::print_error!(
"Cannot retrieve Git statuses for directory {:?}: {}",
workdir,
err
);
}
}
GitCache { statuses }
} else {
Self::empty()
}
}
pub fn empty() -> Self {
GitCache {
statuses: Vec::new(),
}
}
pub fn get(&self, filepath: &PathBuf, is_directory: bool) -> Option<GitFileStatus> {
match std::fs::canonicalize(filepath) {
Ok(filename) => Some(self.inner_get(&filename, is_directory)),
Err(err) => {
if err.kind() != std::io::ErrorKind::NotFound {
crate::print_error!("Cannot get git status for {:?}: {}", filepath, err);
}
None
}
}
}
fn inner_get(&self, filepath: &PathBuf, is_directory: bool) -> GitFileStatus {
if is_directory {
self.statuses
.iter()
.filter(|&x| x.0.starts_with(filepath))
.map(|x| GitFileStatus::new(x.1))
.fold(GitFileStatus::default(), |acc, x| GitFileStatus {
index: std::cmp::max(acc.index, x.index),
workdir: std::cmp::max(acc.workdir, x.workdir),
})
} else {
self.statuses
.iter()
.find(|&x| filepath == &x.0)
.map(|e| GitFileStatus::new(e.1))
.unwrap_or_default()
}
}
}
#[cfg(not(feature = "no-git"))]
#[cfg(test)]
mod tests {
use super::*;
use assert_fs::prelude::*;
use assert_fs::TempDir;
use git2::build::CheckoutBuilder;
use git2::{CherrypickOptions, Index, Oid, Repository, RepositoryInitOptions};
use std::collections::HashMap;
use std::fs::remove_file;
#[allow(unused)]
use std::process::Command;
#[test]
fn compare_git_status() {
assert!(GitStatus::Unmodified < GitStatus::Conflicted);
}
macro_rules! t {
($e:expr) => {
match $e {
Ok(e) => e,
Err(e) => panic!("{} failed with {}", stringify!($e), e),
}
};
}
fn repo_init() -> (TempDir, Repository) {
let td = t!(TempDir::new());
let mut opts = RepositoryInitOptions::new();
opts.initial_head("master");
let repo = Repository::init_opts(td.path(), &opts).unwrap();
{
let mut config = t!(repo.config());
t!(config.set_str("user.name", "name"));
t!(config.set_str("user.email", "email"));
let mut index = t!(repo.index());
let id = t!(index.write_tree());
let tree = t!(repo.find_tree(id));
let sig = t!(repo.signature());
t!(repo.commit(Some("HEAD"), &sig, &sig, "initial commit", &tree, &[]));
}
(td, repo)
}
fn commit(repo: &Repository, index: &mut Index, msg: &str) -> (Oid, Oid) {
let tree_id = t!(index.write_tree());
let tree = t!(repo.find_tree(tree_id));
let sig = t!(repo.signature());
let head_id = t!(repo.refname_to_id("HEAD"));
let parent = t!(repo.find_commit(head_id));
let commit = t!(repo.commit(Some("HEAD"), &sig, &sig, msg, &tree, &[&parent]));
(commit, tree_id)
}
fn check_cache(root: &Path, statuses: &HashMap<&PathBuf, GitFileStatus>, msg: &str) {
let cache = GitCache::new(root);
for (&path, status) in statuses.iter() {
if let Ok(filename) = std::fs::canonicalize(&root.join(path)) {
let is_directory = filename.is_dir();
assert_eq!(
&cache.inner_get(&filename, is_directory),
status,
"Invalid status for file {} at stage {}",
filename.to_string_lossy(),
msg
);
}
}
}
#[test]
fn test_git_workflow() {
let (root, repo) = repo_init();
let mut index = repo.index().unwrap();
let mut expected_statuses = HashMap::new();
check_cache(root.path(), &expected_statuses, "initialization");
let f0 = PathBuf::from(".gitignore");
root.child(&f0).write_str("*.bak").unwrap();
expected_statuses.insert(
&f0,
GitFileStatus {
index: GitStatus::Unmodified,
workdir: GitStatus::NewInWorkdir,
},
);
let _success = Command::new("git")
.current_dir(root.path())
.arg("status")
.status()
.expect("Git status failed")
.success();
check_cache(root.path(), &expected_statuses, "new .gitignore");
index.add_path(f0.as_path()).unwrap();
check_cache(root.path(), &expected_statuses, "unstaged .gitignore");
index.write().unwrap();
*expected_statuses.get_mut(&f0).unwrap() = GitFileStatus {
index: GitStatus::NewInIndex,
workdir: GitStatus::Unmodified,
};
check_cache(root.path(), &expected_statuses, "staged .gitignore");
commit(&repo, &mut index, "Add gitignore");
*expected_statuses.get_mut(&f0).unwrap() = GitFileStatus {
index: GitStatus::Default,
workdir: GitStatus::Default,
};
check_cache(root.path(), &expected_statuses, "Committed .gitignore");
let d1 = PathBuf::from("d1");
let f1 = d1.join("f1");
root.child(&f1).touch().unwrap();
let f2 = d1.join("f2.bak");
root.child(&f2).touch().unwrap();
expected_statuses.insert(
&d1,
GitFileStatus {
index: GitStatus::Unmodified,
workdir: GitStatus::NewInWorkdir,
},
);
expected_statuses.insert(
&f1,
GitFileStatus {
index: GitStatus::Unmodified,
workdir: GitStatus::NewInWorkdir,
},
);
expected_statuses.insert(
&f2,
GitFileStatus {
index: GitStatus::Unmodified,
workdir: GitStatus::Ignored,
},
);
check_cache(root.path(), &expected_statuses, "New files");
index.add_path(f1.as_path()).unwrap();
index.write().unwrap();
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
index: GitStatus::NewInIndex,
workdir: GitStatus::Ignored,
};
*expected_statuses.get_mut(&f1).unwrap() = GitFileStatus {
index: GitStatus::NewInIndex,
workdir: GitStatus::Unmodified,
};
check_cache(root.path(), &expected_statuses, "Unstaged new files");
index.add_path(f2.as_path()).unwrap();
index.write().unwrap();
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
index: GitStatus::NewInIndex,
workdir: GitStatus::Unmodified,
};
*expected_statuses.get_mut(&f2).unwrap() = GitFileStatus {
index: GitStatus::NewInIndex,
workdir: GitStatus::Unmodified,
};
check_cache(root.path(), &expected_statuses, "Staged new files");
let (commit1_oid, _) = commit(&repo, &mut index, "Add new files");
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
index: GitStatus::Default,
workdir: GitStatus::Default,
};
*expected_statuses.get_mut(&f1).unwrap() = GitFileStatus {
index: GitStatus::Default,
workdir: GitStatus::Default,
};
*expected_statuses.get_mut(&f2).unwrap() = GitFileStatus {
index: GitStatus::Default,
workdir: GitStatus::Default,
};
check_cache(root.path(), &expected_statuses, "Committed new files");
remove_file(root.child(&f2).path()).unwrap();
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
index: GitStatus::Unmodified,
workdir: GitStatus::Deleted,
};
*expected_statuses.get_mut(&f2).unwrap() = GitFileStatus {
index: GitStatus::Unmodified,
workdir: GitStatus::Deleted,
};
check_cache(root.path(), &expected_statuses, "Remove file");
root.child(&f1).write_str("New content").unwrap();
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
index: GitStatus::Unmodified,
workdir: GitStatus::Modified,
}; *expected_statuses.get_mut(&f1).unwrap() = GitFileStatus {
index: GitStatus::Unmodified,
workdir: GitStatus::Modified,
};
check_cache(root.path(), &expected_statuses, "Change file");
index.remove_path(&f2).unwrap();
index.write().unwrap();
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
index: GitStatus::Deleted,
workdir: GitStatus::Modified,
};
*expected_statuses.get_mut(&f2).unwrap() = GitFileStatus {
index: GitStatus::Deleted,
workdir: GitStatus::Unmodified,
};
check_cache(root.path(), &expected_statuses, "Staged changes");
commit(&repo, &mut index, "Remove backup file");
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
index: GitStatus::Unmodified,
workdir: GitStatus::Modified,
};
*expected_statuses.get_mut(&f2).unwrap() = GitFileStatus {
index: GitStatus::Default,
workdir: GitStatus::Default,
};
check_cache(
root.path(),
&expected_statuses,
"Committed changes (first part)",
);
index.add_path(&f1).unwrap();
index.write().unwrap();
commit(&repo, &mut index, "Save modified file");
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
index: GitStatus::Default,
workdir: GitStatus::Default,
};
*expected_statuses.get_mut(&f1).unwrap() = GitFileStatus {
index: GitStatus::Default,
workdir: GitStatus::Default,
};
check_cache(
root.path(),
&expected_statuses,
"Committed changes (second part)",
);
let branch_commit = repo.find_commit(commit1_oid).unwrap();
let branch = repo
.branch("conflict-branch", &branch_commit, true)
.unwrap();
repo.set_head(format!("refs/heads/{}", branch.name().unwrap().unwrap()).as_str())
.unwrap();
let mut checkout_opts = CheckoutBuilder::new();
checkout_opts.force();
repo.checkout_head(Some(&mut checkout_opts)).unwrap();
root.child(&f1)
.write_str("New conflicting content")
.unwrap();
root.child(&f2)
.write_str("New conflicting content")
.unwrap();
index.add_path(&f1).unwrap();
index.add_path(&f2).unwrap();
index.write().unwrap();
let (commit2_oid, _) = commit(&repo, &mut index, "Save conflicting changes");
check_cache(
root.path(),
&expected_statuses,
"Committed changes in branch",
);
repo.set_head("refs/heads/master").unwrap();
repo.checkout_head(Some(&mut checkout_opts)).unwrap();
let mut cherrypick_opts = CherrypickOptions::new();
let branch_commit = repo.find_commit(commit2_oid).unwrap();
repo.cherrypick(&branch_commit, Some(&mut cherrypick_opts))
.unwrap();
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
index: GitStatus::Unmodified,
workdir: GitStatus::Conflicted,
};
*expected_statuses.get_mut(&f1).unwrap() = GitFileStatus {
index: GitStatus::Unmodified,
workdir: GitStatus::Conflicted,
};
*expected_statuses.get_mut(&f2).unwrap() = GitFileStatus {
index: GitStatus::Unmodified,
workdir: GitStatus::Conflicted,
};
check_cache(
root.path(),
&expected_statuses,
"Conflict between master and branch",
);
}
}