use super::*;
use crate::test_support::{age, fixture, git, git_context, git_text, remove, repository};
use std::path::PathBuf;
fn aged_repository(root: &Path) -> PathBuf {
let repo: PathBuf = repository(root, "repo");
std::fs::write(repo.join(".gitignore"), b"ignored/\n").expect("gitignore");
std::fs::write(repo.join("a.txt"), b"alpha\n").expect("a");
std::fs::create_dir(repo.join("dir")).expect("dir");
std::fs::write(repo.join("dir/b.txt"), b"bravo\n").expect("b");
std::os::unix::fs::symlink("a.txt", repo.join("link")).expect("link");
for name in [".gitignore", "a.txt", "dir/b.txt", "link"] {
age(repo.join(name).as_path());
}
git(repo.as_path(), &["add", "--all"]);
git(repo.as_path(), &["commit", "--quiet", "--message=files"]);
return repo;
}
fn check(repo: &Path) -> Result<CleanSource, String> {
let git_dir: PathBuf = repo.join(".git");
return check_clean(&git_context(), repo, git_dir.as_path(), git_dir.as_path());
}
#[test]
fn a_clean_worktree_is_clean_and_names_its_head() {
let root: PathBuf = fixture("clean-ok");
let repo: PathBuf = aged_repository(root.as_path());
std::fs::create_dir(repo.join("ignored")).expect("ignored directory");
std::fs::write(repo.join("ignored/cache"), b"cache\n").expect("ignored file");
let clean: CleanSource = check(repo.as_path()).expect("clean");
assert_eq!(clean.head_commit, git_text(repo.as_path(), &["rev-parse", "HEAD"]).trim());
assert_eq!(to_hex_for_test(clean.head_tree.as_slice()), git_text(repo.as_path(), &["rev-parse", "HEAD^{tree}"]).trim());
assert_eq!(clean.tracked, 4);
remove(root.as_path());
}
fn to_hex_for_test(name: &[u8]) -> String {
return name.iter().map(|byte: &u8| { return format!("{byte:02x}"); }).collect();
}
#[test]
fn each_difference_is_refused_with_its_reason() {
let root: PathBuf = fixture("clean-dirty");
type Make = fn(&Path);
let cases: [(&str, &str, Make); 10] = [
("rewritten content", "a.txt differs from its index entry", |tree| { std::fs::write(tree.join("a.txt"), b"ALPHA\n").expect("write"); }),
("deleted", "dir/b.txt is missing", |tree| { std::fs::remove_file(tree.join("dir/b.txt")).expect("remove"); }),
("executable bit", "a.txt changed its executable bit", |tree| {
std::fs::set_permissions(tree.join("a.txt"), std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("chmod");
}),
("type change", "link changed its type", |tree| {
std::fs::remove_file(tree.join("link")).expect("remove");
std::fs::write(tree.join("link"), b"a.txt").expect("write");
}),
("untracked", "new.txt is untracked and not ignored", |tree| { std::fs::write(tree.join("new.txt"), b"n").expect("write"); }),
("untracked in a directory", "dir/", |tree| { std::fs::write(tree.join("dir/new.txt"), b"n").expect("write"); }),
("staged change", "a.txt is staged with a change", |tree| {
std::fs::write(tree.join("a.txt"), b"staged\n").expect("write");
git(tree, &["add", "a.txt"]);
}),
("staged deletion", "the index and the tree of HEAD hold different numbers", |tree| { git(tree, &["rm", "--quiet", "--cached", "a.txt"]); }),
("merge in progress", "MERGE_HEAD exists", |tree| {
let head: String = git_text(tree, &["rev-parse", "HEAD"]);
std::fs::write(tree.join(".git/MERGE_HEAD"), head.as_bytes()).expect("write");
}),
("held index lock", "index.lock exists", |tree| { std::fs::write(tree.join(".git/index.lock"), b"").expect("write"); }),
];
for (label, expected, make) in cases {
let one: PathBuf = repository(root.as_path(), format!("case-{}", label.replace(' ', "-")).as_str());
std::fs::write(one.join(".gitignore"), b"ignored/\n").expect("gitignore");
std::fs::write(one.join("a.txt"), b"alpha\n").expect("a");
std::fs::create_dir(one.join("dir")).expect("dir");
std::fs::write(one.join("dir/b.txt"), b"bravo\n").expect("b");
std::os::unix::fs::symlink("a.txt", one.join("link")).expect("link");
for name in [".gitignore", "a.txt", "dir/b.txt", "link"] {
age(one.join(name).as_path());
}
git(one.as_path(), &["add", "--all"]);
git(one.as_path(), &["commit", "--quiet", "--message=files"]);
assert!(check(one.as_path()).is_ok(), "{label}: clean before the change");
make(one.as_path());
let reason: String = check(one.as_path()).expect_err(label);
assert!(reason.contains(expected), "{label}: {reason}");
}
remove(root.as_path());
}
#[test]
fn a_racy_entry_is_refused() {
let root: PathBuf = fixture("clean-racy");
let repo: PathBuf = repository(root.as_path(), "repo");
std::fs::write(repo.join("fresh.txt"), b"fresh\n").expect("fresh");
git(repo.as_path(), &["add", "fresh.txt"]);
git(repo.as_path(), &["commit", "--quiet", "--message=fresh"]);
let reason: String = check(repo.as_path()).expect_err("racy");
assert!(reason.contains("as new as the index"), "{reason}");
remove(root.as_path());
}
#[test]
fn a_submodule_entry_is_refused() {
let root: PathBuf = fixture("clean-submodule");
let repo: PathBuf = repository(root.as_path(), "repo");
let head: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
git(repo.as_path(), &["update-index", "--add", "--cacheinfo", format!("160000,{},sub", head.trim()).as_str()]);
git(repo.as_path(), &["commit", "--quiet", "--message=submodule"]);
let reason: String = check(repo.as_path()).expect_err("submodule");
assert!(reason.contains("sub is a submodule"), "{reason}");
remove(root.as_path());
}
#[test]
fn a_linked_worktree_is_checked_against_its_own_git_directory() {
let root: PathBuf = fixture("clean-linked");
let repo: PathBuf = aged_repository(root.as_path());
git(repo.as_path(), &["worktree", "add", "--quiet", "-b", "linked", "../linked"]);
let linked: PathBuf = std::fs::canonicalize(root.join("linked")).expect("canonical");
for name in ["a.txt", "dir/b.txt", ".gitignore", "link"] {
age(linked.join(name).as_path());
}
git(linked.as_path(), &["update-index", "--refresh"]);
let admin: PathBuf = repo.join(".git/worktrees/linked");
let outcome: Result<CleanSource, String> = check_clean(&git_context(), linked.as_path(), admin.as_path(), repo.join(".git").as_path());
assert!(outcome.is_ok(), "{outcome:?}");
std::fs::write(linked.join("untracked.txt"), b"u").expect("write");
assert!(check_clean(&git_context(), linked.as_path(), admin.as_path(), repo.join(".git").as_path()).is_err());
remove(root.as_path());
}
#[test]
fn the_comparison_below_a_handle_refuses_a_link_in_place_of_a_directory() {
let root: PathBuf = fixture("clean-beneath");
let repo: PathBuf = aged_repository(root.as_path());
let clean: CleanSource = check(repo.as_path()).expect("clean");
let entries: Vec<StatEntry> = stored_stats(clean.index_bytes.as_slice(), clean.hash_length).expect("entries");
let held: std::fs::File = crate::fs_beneath::open_root(std::fs::canonicalize(repo.as_path()).expect("canonical").as_path()).expect("handle");
assert_eq!(tree_differs_from_index_beneath(&held, entries.as_slice(), clean.index_seconds), None, "positive control");
std::fs::create_dir(root.join("copy")).expect("copy");
std::fs::copy(repo.join("dir/b.txt"), root.join("copy/b.txt")).expect("copy of the file");
std::fs::rename(repo.join("dir"), root.join("dir.orig")).expect("moved away");
std::os::unix::fs::symlink(root.join("copy"), repo.join("dir")).expect("link in its place");
let reason: String = tree_differs_from_index_beneath(&held, entries.as_slice(), clean.index_seconds).expect("refused");
assert!(reason.contains("dir/b.txt") && reason.contains("link"), "{reason}");
remove(root.as_path());
}
#[test]
fn metadata_of_a_file_in_hand_is_judged_like_its_path() {
let root: PathBuf = fixture("clean-metadata");
let repo: PathBuf = aged_repository(root.as_path());
let clean: CleanSource = check(repo.as_path()).expect("clean");
let entries: Vec<StatEntry> = stored_stats(clean.index_bytes.as_slice(), clean.hash_length).expect("entries");
let entry: &StatEntry = entries.iter().find(|entry: &&StatEntry| { return entry.path == b"a.txt"; }).expect("a.txt");
let held = std::fs::File::open(repo.join("a.txt")).expect("open").metadata().expect("metadata");
assert_eq!(metadata_differs_from_entry(entry, &held, clean.index_seconds), None);
std::fs::write(repo.join("a.txt"), b"ALPHA\n").expect("rewritten");
let changed = std::fs::File::open(repo.join("a.txt")).expect("open").metadata().expect("metadata");
assert!(metadata_differs_from_entry(entry, &changed, clean.index_seconds).expect("changed").contains("a.txt"));
remove(root.as_path());
}