use grit_lib::config::environment::RepositoryOptions;
use grit_lib::diff::{resolve_diff_arguments, DiffTarget, ResolveDiffArgumentsError};
use grit_lib::objects::ObjectId;
use grit_lib::repo::Repository;
use grit_test_support::git;
use tempfile::TempDir;
fn init_repo(dir: &std::path::Path) {
git(dir, &["init", "-q", "-b", "main", "."]);
git(dir, &["config", "user.email", "u@example.com"]);
git(dir, &["config", "user.name", "U"]);
}
fn commit_all(dir: &std::path::Path, msg: &str) -> ObjectId {
git(dir, &["add", "-A"]);
git(dir, &["commit", "-qm", msg]);
git(dir, &["rev-parse", "HEAD"])
.trim()
.parse()
.expect("oid")
}
fn open_repo(root: &std::path::Path) -> Repository {
Repository::open(
&root.join(".git"),
&RepositoryOptions::empty().with_work_tree(root),
)
.expect("open")
}
#[test]
fn resolve_diff_default_target_is_worktree_vs_head() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("a.txt"), "x\n").expect("write");
commit_all(dir.path(), "c1");
let repo = open_repo(dir.path());
std::fs::write(dir.path().join("a.txt"), "y\n").expect("write");
let args = resolve_diff_arguments(&repo, None, false, false, None, &[], &[]).expect("resolve");
assert_eq!(args.target, DiffTarget::WorktreeVsHead);
assert!(args.pathspecs.is_empty());
}
#[test]
fn resolve_diff_staged_and_unstaged_conflict() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
let repo = open_repo(dir.path());
let err = resolve_diff_arguments(&repo, None, true, true, None, &[], &[]).unwrap_err();
assert_eq!(err, ResolveDiffArgumentsError::StagedAndUnstaged);
}
#[test]
fn files_staged_matches_git_name_only() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("f.txt"), "one\n").expect("write");
commit_all(dir.path(), "c1");
std::fs::write(dir.path().join("f.txt"), "two\n").expect("write");
git(dir.path(), &["add", "f.txt"]);
let repo = open_repo(dir.path());
let mut opts = grit_lib::diff::DiffOptions::default();
opts.word_level_inline = false;
let args = resolve_diff_arguments(&repo, None, true, false, None, &[], &[]).expect("resolve");
let files = repo
.diff()
.files(&args.target, &args.pathspecs, &opts)
.expect("files");
let grit_paths: Vec<_> = files.iter().map(|f| f.path.as_str()).collect();
let git_out = git(dir.path(), &["diff", "--staged", "--name-only"]);
let git_paths: Vec<_> = git_out.lines().filter(|l| !l.is_empty()).collect();
assert_eq!(grit_paths, git_paths);
}
#[test]
fn files_commit_range_matches_git_name_only() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("a.txt"), "1\n").expect("write");
commit_all(dir.path(), "c1");
std::fs::write(dir.path().join("b.txt"), "2\n").expect("write");
let tip = commit_all(dir.path(), "c2");
let base: ObjectId = git(dir.path(), &["rev-parse", "HEAD~1"])
.trim()
.parse()
.expect("oid");
let repo = open_repo(dir.path());
let mut opts = grit_lib::diff::DiffOptions::default();
opts.word_level_inline = false;
let target = DiffTarget::Trees {
old: Some(base),
new: tip,
};
let files = repo.diff().files(&target, &[], &opts).expect("files");
let mut grit_paths: Vec<_> = files.iter().map(|f| f.path.clone()).collect();
grit_paths.sort();
let git_out = git(
dir.path(),
&["diff", "--name-only", &base.to_hex(), &tip.to_hex()],
);
let mut git_paths: Vec<_> = git_out
.lines()
.filter(|l| !l.is_empty())
.map(str::to_owned)
.collect();
git_paths.sort();
assert_eq!(grit_paths, git_paths);
}
fn grit_files(
repo: &Repository,
target: DiffTarget,
pathspecs: &[String],
) -> Vec<grit_lib::diff::FileDiff> {
let mut opts = grit_lib::diff::DiffOptions::default();
opts.word_level_inline = false;
repo.diff().files(&target, pathspecs, &opts).expect("files")
}
fn git_raw(repo: &std::path::Path, args: &[&str]) -> Vec<String> {
let mut cmd_args = vec!["-c", "core.abbrev=40"];
cmd_args.extend(args);
git(repo, &cmd_args)
.lines()
.filter(|l| l.starts_with(':'))
.map(str::to_owned)
.collect()
}
fn grit_raw(repo: &Repository, target: DiffTarget) -> Vec<String> {
use grit_lib::diff::{format_raw, DiffEntry, DiffStatus};
grit_files(repo, target, &[])
.into_iter()
.map(|f| {
let status = f.status;
let path = f.path.clone();
let mut e = DiffEntry {
status,
old_path: f.old_path.clone(),
new_path: if status == DiffStatus::Deleted {
None
} else {
Some(path)
},
old_mode: f.old_mode.clone().unwrap_or_else(|| "000000".into()),
new_mode: f.new_mode.clone().unwrap_or_else(|| "000000".into()),
old_oid: f.old_oid,
new_oid: f.new_oid,
score: None,
};
if status == DiffStatus::Deleted {
e.old_path = Some(f.path);
}
format_raw(&e)
})
.collect()
}
#[test]
fn worktree_vs_head_matches_git_diff_head() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("a.txt"), "v1\n").expect("write");
commit_all(dir.path(), "c1");
std::fs::write(dir.path().join("a.txt"), "v2\n").expect("write");
std::fs::write(dir.path().join("new.txt"), "n\n").expect("write");
let repo = open_repo(dir.path());
let grit = grit_raw(&repo, DiffTarget::WorktreeVsHead);
let git = git_raw(dir.path(), &["diff", "--raw", "-M50", "HEAD"]);
assert_eq!(grit, git);
}
#[test]
fn index_vs_head_matches_git_diff_cached() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("a.txt"), "v1\n").expect("write");
commit_all(dir.path(), "c1");
std::fs::write(dir.path().join("a.txt"), "v2\n").expect("write");
git(dir.path(), &["add", "a.txt"]);
let repo = open_repo(dir.path());
let grit = grit_raw(&repo, DiffTarget::IndexVsHead);
let git = git_raw(dir.path(), &["diff", "--raw", "-M50", "--cached"]);
assert_eq!(grit, git);
}
#[test]
fn worktree_vs_index_matches_git_diff() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("a.txt"), "v1\n").expect("write");
commit_all(dir.path(), "c1");
std::fs::write(dir.path().join("a.txt"), "v2\n").expect("write");
git(dir.path(), &["add", "a.txt"]);
std::fs::write(dir.path().join("a.txt"), "v3\n").expect("write");
let repo = open_repo(dir.path());
let grit = grit_raw(&repo, DiffTarget::WorktreeVsIndex);
let git = git_raw(dir.path(), &["diff", "--raw", "-M50"]);
assert_eq!(grit, git);
}
#[test]
fn commit_matches_git_show() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("a.txt"), "1\n").expect("write");
commit_all(dir.path(), "c1");
std::fs::write(dir.path().join("a.txt"), "2\n").expect("write");
let c2 = commit_all(dir.path(), "c2");
let repo = open_repo(dir.path());
let grit = grit_raw(&repo, DiffTarget::Commit(c2));
let git = git_raw(
dir.path(),
&["show", "--raw", "-M50", "--format=", &c2.to_hex()],
);
assert_eq!(grit, git);
}
#[test]
fn trees_match_git_diff_a_b() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("a.txt"), "1\n").expect("write");
let c1 = commit_all(dir.path(), "c1");
std::fs::write(dir.path().join("b.txt"), "2\n").expect("write");
let c2 = commit_all(dir.path(), "c2");
let repo = open_repo(dir.path());
let target = DiffTarget::Trees {
old: Some(c1),
new: c2,
};
let grit = grit_raw(&repo, target);
let git = git_raw(
dir.path(),
&["diff", "--raw", "-M50", &c1.to_hex(), &c2.to_hex()],
);
assert_eq!(grit, git);
}
#[test]
fn triple_dot_uses_merge_base_like_git() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("base.txt"), "b\n").expect("write");
commit_all(dir.path(), "root");
git(dir.path(), &["switch", "-c", "topic"]);
std::fs::write(dir.path().join("topic.txt"), "t\n").expect("write");
let topic_tip = commit_all(dir.path(), "topic");
git(dir.path(), &["switch", "main"]);
std::fs::write(dir.path().join("main.txt"), "m\n").expect("write");
let main_tip = commit_all(dir.path(), "main");
let repo = open_repo(dir.path());
let args = resolve_diff_arguments(
&repo,
None,
false,
false,
None,
&["main...topic".into()],
&[],
)
.expect("resolve");
let grit = grit_raw(&repo, args.target);
let git = git_raw(
dir.path(),
&[
"diff",
"--raw",
"-M50",
&format!("{}...{}", main_tip.to_hex(), topic_tip.to_hex()),
],
);
assert_eq!(grit, git);
}
#[test]
fn pathspec_filter_matches_git() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("keep.txt"), "k\n").expect("write");
std::fs::write(dir.path().join("skip.txt"), "s\n").expect("write");
commit_all(dir.path(), "c1");
std::fs::write(dir.path().join("keep.txt"), "k2\n").expect("write");
std::fs::write(dir.path().join("skip.txt"), "s2\n").expect("write");
let repo = open_repo(dir.path());
let specs = vec!["keep.txt".to_owned()];
let grit = grit_files(&repo, DiffTarget::WorktreeVsHead, &specs);
let git = git_raw(
dir.path(),
&["diff", "--raw", "-M50", "HEAD", "--", "keep.txt"],
);
assert_eq!(grit.len(), git.len());
assert!(grit.iter().all(|f| f.path == "keep.txt"));
}
#[test]
fn resolve_diff_arguments_too_many_revisions() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("f.txt"), "a\n").expect("write");
commit_all(dir.path(), "a");
std::fs::write(dir.path().join("f.txt"), "b\n").expect("write");
commit_all(dir.path(), "b");
std::fs::write(dir.path().join("f.txt"), "c\n").expect("write");
commit_all(dir.path(), "c");
let repo = open_repo(dir.path());
let err = resolve_diff_arguments(
&repo,
None,
false,
false,
None,
&["HEAD~2".into(), "HEAD~1".into(), "HEAD".into()],
&[],
)
.unwrap_err();
assert_eq!(err, ResolveDiffArgumentsError::TooManyRevisions);
}
#[test]
fn worktree_vs_head_one_row_per_path_with_stale_cache_tree() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("f.txt"), "base\n").expect("write");
commit_all(dir.path(), "base");
std::fs::write(dir.path().join("f.txt"), "staged\n").expect("write");
git(dir.path(), &["add", "f.txt"]);
std::fs::write(dir.path().join("f.txt"), "worktree\n").expect("write");
let repo = open_repo(dir.path());
let mut index = repo.index_handle().load().expect("index");
index.invalidate_cache_tree_for_path(b"f.txt");
repo.index_handle().write(&mut index).expect("write index");
let files = grit_files(&repo, DiffTarget::WorktreeVsHead, &[]);
let grit_paths: Vec<_> = files.iter().map(|f| f.path.as_str()).collect();
assert_eq!(
grit_paths.len(),
grit_paths
.iter()
.collect::<std::collections::HashSet<_>>()
.len(),
"expected at most one diff row per path, got {grit_paths:?}"
);
let git_out = git(dir.path(), &["diff", "--name-only", "HEAD"]);
let git_paths: Vec<_> = git_out.lines().filter(|l| !l.is_empty()).collect();
assert_eq!(grit_paths, git_paths);
}
#[test]
fn worktree_vs_head_skips_assume_unchanged_with_staged_sibling() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
std::fs::write(dir.path().join("a.txt"), "a-v1\n").expect("write");
std::fs::write(dir.path().join("b.txt"), "b-v1\n").expect("write");
commit_all(dir.path(), "base");
git(dir.path(), &["update-index", "--assume-unchanged", "a.txt"]);
std::fs::write(dir.path().join("a.txt"), "a-dirty\n").expect("write");
std::fs::write(dir.path().join("b.txt"), "b-v2\n").expect("write");
git(dir.path(), &["add", "b.txt"]);
let repo = open_repo(dir.path());
let files = grit_files(&repo, DiffTarget::WorktreeVsHead, &[]);
let grit_paths: Vec<_> = files.iter().map(|f| f.path.as_str()).collect();
let git_out = git(dir.path(), &["diff", "--name-only", "HEAD"]);
let git_paths: Vec<_> = git_out.lines().filter(|l| !l.is_empty()).collect();
assert_eq!(grit_paths, git_paths);
assert_eq!(grit_paths, vec!["b.txt"]);
}
#[test]
fn resolve_diff_arguments_against_with_commits() {
let dir = TempDir::new().expect("tempdir");
init_repo(dir.path());
let repo = open_repo(dir.path());
let err = resolve_diff_arguments(
&repo,
None,
false,
false,
Some("HEAD"),
&["HEAD~1".into()],
&[],
)
.unwrap_err();
assert_eq!(err, ResolveDiffArgumentsError::AgainstWithCommits);
}