use super::*;
use crate::test_support::{fixture, git, git_output, remove};
use std::os::unix::ffi::OsStringExt;
pub(crate) fn scope(root: &Path) -> ScopeEnvironment {
return ScopeEnvironment { home: Some(root.join("no-home")), no_system: true, global: Some(PathBuf::from("/nonexistent-global-config")), ..ScopeEnvironment::default() };
}
pub(crate) fn mine(root: &Path, repo: &Path, arguments: &[&str]) -> Option<Answer> {
let given: Vec<OsString> = arguments.iter().map(OsString::from).collect();
return answer_with(&Query { arguments: given.as_slice(), directory: Some(repo), variables: &[], input: None }, Vec::new(), &scope(root));
}
fn compare(root: &Path, repo: &Path, arguments: &[&str]) -> bool {
let expected = git_output(repo, arguments);
let Some(found) = mine(root, repo, arguments) else {
return false;
};
assert_eq!(found.stdout, expected.stdout, "{arguments:?}");
assert_eq!(found.stderr, expected.stderr, "{arguments:?}");
assert_eq!(Some(found.code), expected.status.code(), "{arguments:?}");
return true;
}
pub(crate) fn answered(root: &Path, repo: &Path, arguments: &[&str]) {
assert!(compare(root, repo, arguments), "the readers declined {arguments:?}");
}
pub(crate) fn declined(root: &Path, repo: &Path, arguments: &[&str]) {
assert!(mine(root, repo, arguments).is_none(), "the readers answered {arguments:?} but must leave it to Git");
}
pub(crate) fn text(repo: &Path, arguments: &[&str]) -> String {
return String::from_utf8(git_output(repo, arguments).stdout).expect("UTF-8").trim_end().to_string();
}
fn staged_repository(root: &Path, name: &str, format: &str) -> PathBuf {
let repo = root.join(name);
std::fs::create_dir(&repo).expect("dir");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main", &format!("--object-format={format}")]);
std::fs::create_dir_all(repo.join("dir/sub")).expect("dirs");
std::fs::write(repo.join("a.txt"), "one\n").expect("a");
std::fs::write(repo.join("exec.sh"), "#!/bin/sh\n").expect("exec");
std::os::unix::fs::symlink("a.txt", repo.join("link")).expect("link");
std::fs::write(repo.join("dir/b.txt"), "bee\n").expect("b");
std::fs::write(repo.join("dir/sub/c.txt"), "sea\n").expect("c");
std::fs::write(repo.join("sp ace.txt"), "space\n").expect("space");
std::fs::write(repo.join(std::ffi::OsString::from_vec(vec![b'r', 0xff, b'.', b't'])), "raw\n").expect("raw");
git(repo.as_path(), &["add", "--all"]);
git(repo.as_path(), &["commit", "--quiet", "--message=first"]);
std::fs::write(repo.join("a.txt"), "one\ntwo\n").expect("edit");
std::fs::set_permissions(repo.join("exec.sh"), std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("chmod");
std::fs::remove_file(repo.join("link")).expect("rm link");
std::fs::write(repo.join("link"), "now a file\n").expect("file");
std::fs::remove_file(repo.join("dir/b.txt")).expect("rm b");
std::fs::write(repo.join("dir/sub/new.txt"), "new\n").expect("new");
std::fs::write(repo.join("z-added.txt"), "z\n").expect("z");
git(repo.as_path(), &["add", "--all"]);
return repo;
}
fn all_shapes(root: &Path, repo: &Path) {
let head = text(repo, &["rev-parse", "HEAD"]);
let tree = text(repo, &["rev-parse", "HEAD^{tree}"]);
answered(root, repo, &["ls-files", "--stage", "-z", "--full-name"]);
answered(root, repo, &["ls-files", "--stage", "-z"]);
answered(root, repo, &["ls-files", "--unmerged", "-z"]);
answered(root, repo, &["ls-files", "--stage", "-z", "--full-name", "--", ":(top,literal)a.txt"]);
answered(root, repo, &["ls-files", "--stage", "-z", "--full-name", "--", ":(top,literal)dir"]);
answered(root, repo, &["ls-files", "--stage", "-z", "--full-name", "--", "dir/sub", "sp ace.txt"]);
answered(root, repo, &["--no-literal-pathspecs", "ls-files", "--stage", "-z", "--full-name", "--", ":/"]);
for object in [head.as_str(), tree.as_str()] {
answered(root, repo, &["diff-index", "--cached", "--name-only", "-z", "--no-renames", object]);
answered(root, repo, &["diff-index", "--cached", "--raw", "-z", "--no-renames", "--no-abbrev", object]);
for filter in ["A", "D", "M", "T", "AD", "MT"] {
let option = format!("--diff-filter={filter}");
answered(root, repo, &["diff-index", "--cached", "--name-only", &option, "-z", object]);
answered(root, repo, &["diff-index", "--cached", "--raw", "--no-abbrev", "--no-renames", "-z", &option, object, "--", "a.txt"]);
}
answered(root, repo, &["diff-index", "--cached", "--raw", "-z", "--no-renames", "--no-abbrev", object, "--", ":(top,literal)dir/sub/new.txt"]);
answered(root, repo, &["diff-index", "--cached", "--raw", "-z", "--no-renames", "--no-abbrev", object, "--", "dir"]);
}
answered(root, repo, &["diff-tree", "--root", "--no-commit-id", "-r", "-z", "-m", "--no-renames", "--no-abbrev", &head]);
answered(root, repo, &["show", "--no-patch", "--format=%s", &head]);
}
#[test]
fn the_shapes_equal_git_in_every_state() {
let root = fixture("repo-read-query-shapes");
for format in ["sha1", "sha256"] {
let repo = staged_repository(root.as_path(), format, format);
all_shapes(root.as_path(), repo.as_path());
git(repo.as_path(), &["repack", "-a", "-d", "-f", "--quiet"]);
git(repo.as_path(), &["prune-packed"]);
all_shapes(root.as_path(), repo.as_path());
git(repo.as_path(), &["update-index", "--index-version", "4"]);
all_shapes(root.as_path(), repo.as_path());
git(repo.as_path(), &["update-index", "--index-version", "3"]);
all_shapes(root.as_path(), repo.as_path());
}
remove(root.as_path());
}
#[test]
fn subjects_and_root_commits_equal_git() {
let root = fixture("repo-read-query-subjects");
let repo = root.join("repo");
std::fs::create_dir(&repo).expect("dir");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main"]);
for (index, message) in ["plain", "two\nline subject\n\nbody text\n", "\n\nleading blanks\n", " indented \ttail \t\n", "trailing\n\n\n", "é ünïcode\n"].iter().enumerate() {
std::fs::write(repo.join("f.txt"), format!("{index}\n")).expect("write");
git(repo.as_path(), &["add", "--all"]);
git(repo.as_path(), &["commit", "--quiet", "--allow-empty-message", "--cleanup=verbatim", "--message", message]);
let head = text(repo.as_path(), &["rev-parse", "HEAD"]);
answered(root.as_path(), repo.as_path(), &["show", "--no-patch", "--format=%s", &head]);
answered(root.as_path(), repo.as_path(), &["diff-tree", "--root", "--no-commit-id", "-r", "-z", "-m", "--no-renames", "--no-abbrev", &head]);
}
remove(root.as_path());
}
#[test]
fn states_that_need_git_are_declined() {
let root = fixture("repo-read-query-declined");
let repo = staged_repository(root.as_path(), "dir-file", "sha1");
let head = text(repo.as_path(), &["rev-parse", "HEAD"]);
git(repo.as_path(), &["commit", "--quiet", "--message=second"]);
std::fs::remove_file(repo.join("a.txt")).expect("rm");
std::fs::create_dir(repo.join("a.txt")).expect("dir");
std::fs::write(repo.join("a.txt/inside"), "x\n").expect("inside");
git(repo.as_path(), &["add", "--all"]);
let second = text(repo.as_path(), &["rev-parse", "HEAD"]);
declined(root.as_path(), repo.as_path(), &["diff-index", "--cached", "--name-only", "-z", &second]);
assert!(compare(root.as_path(), repo.as_path(), &["ls-files", "--stage", "-z", "--full-name"]), "the listing is unaffected");
let moved = staged_repository(root.as_path(), "rename", "sha1");
git(moved.as_path(), &["commit", "--quiet", "--message=second"]);
git(moved.as_path(), &["mv", "sp ace.txt", "spaced.txt"]);
let tip = text(moved.as_path(), &["rev-parse", "HEAD"]);
declined(root.as_path(), moved.as_path(), &["diff", "--cached", "--name-only", "-z", &tip]);
answered(root.as_path(), moved.as_path(), &["diff-index", "--cached", "--name-only", "-z", &tip]);
std::fs::write(moved.join("later.txt"), "later\n").expect("later");
git(moved.as_path(), &["add", "--intent-to-add", "later.txt"]);
declined(root.as_path(), moved.as_path(), &["diff-index", "--cached", "--name-only", "-z", &tip]);
let conflict = root.join("conflict");
std::fs::create_dir(&conflict).expect("dir");
git(conflict.as_path(), &["init", "--quiet", "--initial-branch=main"]);
std::fs::write(conflict.join("f"), "base\n").expect("f");
git(conflict.as_path(), &["add", "f"]);
git(conflict.as_path(), &["commit", "--quiet", "--message=base"]);
git(conflict.as_path(), &["switch", "--quiet", "--create", "side"]);
std::fs::write(conflict.join("f"), "side\n").expect("f");
git(conflict.as_path(), &["commit", "--quiet", "--all", "--message=side"]);
git(conflict.as_path(), &["switch", "--quiet", "main"]);
std::fs::write(conflict.join("f"), "main\n").expect("f");
git(conflict.as_path(), &["commit", "--quiet", "--all", "--message=main"]);
assert!(!git_output(conflict.as_path(), &["merge", "side"]).status.success());
let conflict_tip = text(conflict.as_path(), &["rev-parse", "HEAD"]);
answered(root.as_path(), conflict.as_path(), &["ls-files", "--unmerged", "-z"]);
answered(root.as_path(), conflict.as_path(), &["ls-files", "--stage", "-z", "--full-name"]);
declined(root.as_path(), conflict.as_path(), &["diff-index", "--cached", "--name-only", "-z", &conflict_tip]);
git(conflict.as_path(), &["checkout", "--quiet", "--theirs", "f"]);
git(conflict.as_path(), &["add", "f"]);
git(conflict.as_path(), &["commit", "--quiet", "--message=merged"]);
let merge = text(conflict.as_path(), &["rev-parse", "HEAD"]);
declined(root.as_path(), conflict.as_path(), &["diff-tree", "--root", "--no-commit-id", "-r", "-z", "-m", "--no-renames", "--no-abbrev", &merge]);
let linked = staged_repository(root.as_path(), "gitlink", "sha1");
git(linked.as_path(), &["commit", "--quiet", "--message=second"]);
let inner = text(linked.as_path(), &["rev-parse", "HEAD"]);
git(linked.as_path(), &["update-index", "--add", "--cacheinfo", &format!("160000,{inner},module")]);
let linked_tip = text(linked.as_path(), &["rev-parse", "HEAD"]);
declined(root.as_path(), linked.as_path(), &["diff-index", "--cached", "--name-only", "-z", &linked_tip]);
git(repo.as_path(), &["config", "diff.orderFile", "order.txt"]);
declined(root.as_path(), repo.as_path(), &["diff-index", "--cached", "--name-only", "-z", &head]);
let clean = staged_repository(root.as_path(), "unknown-shapes", "sha1");
declined(root.as_path(), clean.as_path(), &["ls-files", "--stage", "-z", "--others"]);
declined(root.as_path(), clean.as_path(), &["diff-index", "--cached", "--name-only", "-z", "HEAD"]);
declined(root.as_path(), clean.as_path(), &["diff-index", "--cached", "--name-only", "--diff-filter=d", "-z", &head]);
declined(root.as_path(), clean.as_path(), &["ls-files", "--stage", "-z", "--", "*.txt"]);
declined(root.as_path(), clean.as_path(), &["ls-files", "--stage", "-z", "--", ":/", ":(top,exclude)a.txt"]);
let clean_tip = text(clean.as_path(), &["rev-parse", "HEAD"]);
declined(root.as_path(), clean.as_path(), &["diff-index", "--cached", "--name-only", "-z", "--diff-filter=A", "--diff-filter=D", &clean_tip]);
let nowhere = root.join("no-such-directory");
let real = clean.to_str().expect("UTF-8");
declined(root.as_path(), root.as_path(), &["-C", nowhere.to_str().expect("UTF-8"), "-C", real, "ls-files", "--stage", "-z"]);
answered(root.as_path(), root.as_path(), &["-C", real, "ls-files", "--stage", "-z"]);
declined(root.as_path(), clean.join("dir").as_path(), &["ls-files", "--stage", "-z", "--full-name"]);
remove(root.as_path());
}
fn sparse_index_repository(root: &Path) -> PathBuf {
let repo = root.join("sparse");
std::fs::create_dir(&repo).expect("dir");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main"]);
for directory in ["kept", "left-out/deep", "also-out"] {
std::fs::create_dir_all(repo.join(directory)).expect("dirs");
std::fs::write(repo.join(directory).join("f.txt"), format!("{directory}\n")).expect("file");
}
std::fs::write(repo.join("top.txt"), "top\n").expect("top");
git(repo.as_path(), &["add", "--all"]);
git(repo.as_path(), &["commit", "--quiet", "--message=first"]);
git(repo.as_path(), &["sparse-checkout", "init", "--cone", "--sparse-index"]);
git(repo.as_path(), &["sparse-checkout", "set", "kept"]);
return repo;
}
#[test]
fn a_real_sparse_index_is_declined() {
let root = fixture("repo-read-query-sparse");
let repo = sparse_index_repository(root.as_path());
let index = std::fs::read(repo.join(".git/index")).expect("index");
assert!(index.windows(4).any(|window| { return window == b"sdir"; }), "the index has no sparse-directory extension");
let listed = text(repo.as_path(), &["ls-files", "--sparse", "--stage"]);
let full = text(repo.as_path(), &["ls-files", "--stage"]);
assert_ne!(listed, full, "the sparse index lists a sparse directory that a full listing does not");
let head = text(repo.as_path(), &["rev-parse", "HEAD"]);
let shapes: [Vec<&str>; 3] = [
vec!["ls-files", "--stage", "-z", "--full-name"],
vec!["diff-index", "--cached", "--name-only", "-z", &head],
vec!["diff-index", "--cached", "--raw", "-z", "--no-renames", "--no-abbrev", &head],
];
for shape in &shapes {
declined(root.as_path(), repo.as_path(), shape);
}
let kept_lines: Vec<String> = std::fs::read_to_string(repo.join(".git/config"))
.expect("config")
.lines()
.filter(|line| {
let key = line.trim().to_ascii_lowercase();
return !(key.starts_with("sparsecheckout") || key.starts_with("sparse ") || key.starts_with("worktreeconfig"));
})
.map(str::to_string)
.collect();
std::fs::write(repo.join(".git/config"), kept_lines.join("\n") + "\n").expect("config");
if repo.join(".git/config.worktree").exists() {
std::fs::remove_file(repo.join(".git/config.worktree")).expect("worktree config");
}
for shape in &shapes {
assert!(mine(root.as_path(), repo.as_path(), shape).is_none() || compare(root.as_path(), repo.as_path(), shape), "{shape:?}");
}
assert!(mine(root.as_path(), repo.as_path(), &shapes[0]).is_none(), "the index extension must be declined without the keys");
remove(root.as_path());
}