use super::*;
use crate::repo_read_index::parse_index_with_tree;
use crate::repo_read_query::tests::{answered, declined, mine, text};
use crate::test_support::{fixture, git, git_output, remove};
use std::path::{Path, PathBuf};
fn committed_repository(root: &Path, name: &str, format: &str, directories: usize) -> PathBuf {
return repository_with(root, name, format, directories, true);
}
fn repository_with(root: &Path, name: &str, format: &str, directories: usize, packed: bool) -> 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}")]);
for directory in 0..directories {
std::fs::create_dir_all(repo.join(format!("d{directory}/sub"))).expect("dirs");
std::fs::write(repo.join(format!("d{directory}/sub/f.txt")), format!("{directory}\n")).expect("file");
std::fs::write(repo.join(format!("d{directory}/g.txt")), format!("g{directory}\n")).expect("file");
}
std::fs::write(repo.join("root.txt"), "root\n").expect("root");
git(repo.as_path(), &["add", "--all"]);
git(repo.as_path(), &["commit", "--quiet", "--message=first"]);
std::fs::write(repo.join("d1/sub/f.txt"), "changed\n").expect("edit");
git(repo.as_path(), &["commit", "--quiet", "--all", "--message=second"]);
if packed {
git(repo.as_path(), &["repack", "-a", "-d", "-f", "--quiet"]);
git(repo.as_path(), &["prune-packed"]);
}
return repo;
}
fn cached_shapes(root: &Path, repo: &Path) {
let head = text(repo, &["rev-parse", "HEAD"]);
let before = text(repo, &["rev-parse", "HEAD~1"]);
for object in [head.as_str(), before.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]);
answered(root, repo, &["diff-index", "--cached", "--raw", "-z", "--no-renames", "--no-abbrev", object, "--", "d3"]);
answered(root, repo, &["diff-index", "--cached", "--quiet", object]);
answered(root, repo, &["diff-index", "--cached", "--quiet", object, "--", "d1/sub/f.txt"]);
}
}
fn without_cache_tree(repo: &Path) {
let listing = git_output(repo, &["ls-files", "--stage", "-z"]);
let mut update = std::process::Command::new(crate::test_support::REAL_GIT)
.current_dir(repo)
.env("GIT_INDEX_FILE", repo.join(".git/fresh-index"))
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.args(["update-index", "-z", "--index-info"])
.stdin(std::process::Stdio::piped())
.spawn()
.expect("update-index starts");
std::io::Write::write_all(&mut update.stdin.take().expect("stdin"), listing.stdout.as_slice()).expect("entries");
assert!(update.wait().expect("update-index ends").success());
std::fs::rename(repo.join(".git/fresh-index"), repo.join(".git/index")).expect("installed");
}
fn has_cache_tree(repo: &Path, format: crate::repo_read_layout::ObjectFormat) -> bool {
let bytes = std::fs::read(repo.join(".git/index")).expect("index");
return parse_index_with_tree(bytes.as_slice(), format).expect("a plain index").tree.is_some();
}
#[test]
fn the_answers_equal_git_in_every_state_of_the_cache_tree() {
let root = fixture("repo-read-cached-states");
for format in ["sha1", "sha256"] {
let object_format = if format == "sha256" { crate::repo_read_layout::ObjectFormat::Sha256 } else { crate::repo_read_layout::ObjectFormat::Sha1 };
let repo = committed_repository(root.as_path(), format, format, 6);
assert!(has_cache_tree(repo.as_path(), object_format), "a committed index has a cache-tree");
cached_shapes(root.as_path(), repo.as_path());
std::fs::write(repo.join("d3/sub/f.txt"), "staged\n").expect("edit");
git(repo.as_path(), &["add", "d3/sub/f.txt"]);
cached_shapes(root.as_path(), repo.as_path());
std::fs::write(repo.join("top.txt"), "top\n").expect("top");
git(repo.as_path(), &["add", "top.txt"]);
cached_shapes(root.as_path(), repo.as_path());
std::fs::create_dir_all(repo.join("fresh/inner")).expect("dirs");
std::fs::write(repo.join("fresh/inner/n.txt"), "new\n").expect("new");
git(repo.as_path(), &["add", "fresh"]);
git(repo.as_path(), &["rm", "--cached", "--quiet", "-r", "d4"]);
std::fs::set_permissions(repo.join("d2/g.txt"), std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("chmod");
git(repo.as_path(), &["add", "d2/g.txt"]);
cached_shapes(root.as_path(), repo.as_path());
git(repo.as_path(), &["write-tree"]);
cached_shapes(root.as_path(), repo.as_path());
for version in ["4", "3"] {
git(repo.as_path(), &["update-index", "--index-version", version]);
cached_shapes(root.as_path(), repo.as_path());
}
without_cache_tree(repo.as_path());
assert!(!has_cache_tree(repo.as_path(), object_format), "the fresh index has none");
cached_shapes(root.as_path(), repo.as_path());
}
remove(root.as_path());
}
#[test]
fn states_the_cache_tree_says_nothing_of_are_read_in_full_or_left_to_git() {
let root = fixture("repo-read-cached-odd");
let repo = committed_repository(root.as_path(), "odd", "sha1", 4);
git(repo.as_path(), &["rm", "--cached", "--quiet", "root.txt"]);
std::fs::remove_file(repo.join("root.txt")).expect("rm");
std::fs::create_dir_all(repo.join("root.txt")).expect("dir");
std::fs::write(repo.join("root.txt/inner"), "x\n").expect("inner");
git(repo.as_path(), &["add", "root.txt"]);
let head = text(repo.as_path(), &["rev-parse", "HEAD"]);
for shape in [vec!["diff-index", "--cached", "--raw", "-z", "--no-renames", "--no-abbrev", head.as_str()], vec!["diff-index", "--cached", "--quiet", head.as_str()]] {
if let Some(found) = mine(root.as_path(), repo.as_path(), shape.as_slice()) {
let expected = git_output(repo.as_path(), shape.as_slice());
assert_eq!(found.stdout, expected.stdout, "{shape:?}");
assert_eq!(Some(found.code), expected.status.code(), "{shape:?}");
}
}
git(repo.as_path(), &["reset", "--quiet", "--hard", "HEAD"]);
std::fs::write(repo.join("d2/new.txt"), "ita\n").expect("ita");
git(repo.as_path(), &["add", "--intent-to-add", "d2/new.txt"]);
declined(root.as_path(), repo.as_path(), &["diff-index", "--cached", "--name-only", "-z", "--no-renames", head.as_str()]);
answered(root.as_path(), repo.as_path(), &["diff-index", "--cached", "--quiet", head.as_str()]);
git(repo.as_path(), &["reset", "--quiet", "--hard", "HEAD"]);
git(repo.as_path(), &["branch", "side", "HEAD~1"]);
git(repo.as_path(), &["checkout", "--quiet", "side"]);
std::fs::write(repo.join("d1/sub/f.txt"), "side\n").expect("side");
git(repo.as_path(), &["commit", "--quiet", "--all", "--message=side"]);
let merge = git_output(repo.as_path(), &["merge", "--no-commit", "main"]);
assert!(!merge.status.success(), "the merge conflicts");
let side = text(repo.as_path(), &["rev-parse", "HEAD"]);
declined(root.as_path(), repo.as_path(), &["diff-index", "--cached", "--name-only", "-z", "--no-renames", side.as_str()]);
answered(root.as_path(), repo.as_path(), &["diff-index", "--cached", "--quiet", side.as_str()]);
remove(root.as_path());
}
#[test]
fn a_sparse_index_is_left_to_git() {
let root = fixture("repo-read-cached-sparse");
let repo = committed_repository(root.as_path(), "sparse", "sha1", 4);
git(repo.as_path(), &["sparse-checkout", "init", "--cone", "--sparse-index"]);
git(repo.as_path(), &["sparse-checkout", "set", "d1"]);
let head = text(repo.as_path(), &["rev-parse", "HEAD"]);
declined(root.as_path(), repo.as_path(), &["diff-index", "--cached", "--name-only", "-z", "--no-renames", head.as_str()]);
declined(root.as_path(), repo.as_path(), &["diff-index", "--cached", "--quiet", head.as_str()]);
remove(root.as_path());
}
#[test]
fn the_directories_the_cache_tree_proves_equal_are_not_read() {
let root = fixture("repo-read-cached-reads");
let directories: usize = 40;
let repo = committed_repository(root.as_path(), "reads", "sha1", directories);
std::fs::write(repo.join("d3/sub/f.txt"), "staged\n").expect("edit");
git(repo.as_path(), &["add", "d3/sub/f.txt"]);
let head = text(repo.as_path(), &["rev-parse", "HEAD"]);
let decoded_by = |shape: &[&str]| -> u64 {
let started = crate::repo_read_pack::DECODED_TOTAL.load(std::sync::atomic::Ordering::Relaxed);
answered(root.as_path(), repo.as_path(), shape);
return crate::repo_read_pack::DECODED_TOTAL.load(std::sync::atomic::Ordering::Relaxed) - started;
};
let raw = decoded_by(&["diff-index", "--cached", "--raw", "-z", "--no-renames", "--no-abbrev", head.as_str()]);
let quiet = decoded_by(&["diff-index", "--cached", "--quiet", head.as_str()]);
let directories_u64: u64 = directories as u64;
assert!(raw > 0 && raw < directories_u64, "only the trees along the staged path are read: {raw} entries decoded for {directories} directories");
assert!(quiet > 0 && quiet < directories_u64, "the quiet comparison reads as little: {quiet} entries decoded");
without_cache_tree(repo.as_path());
let whole = decoded_by(&["diff-index", "--cached", "--raw", "-z", "--no-renames", "--no-abbrev", head.as_str()]);
assert!(whole >= directories_u64 * 2, "without a cache-tree every directory is read: {whole} entries decoded");
remove(root.as_path());
}
#[test]
fn a_count_that_does_not_match_the_index_declines_the_walk() {
let root = fixture("repo-read-cached-count");
let repo = committed_repository(root.as_path(), "count", "sha1", 5);
let format = crate::repo_read_layout::ObjectFormat::Sha1;
let bytes = std::fs::read(repo.join(".git/index")).expect("index");
let parsed = parse_index_with_tree(bytes.as_slice(), format).expect("index");
let mut cache = parsed.tree.expect("a committed index has a cache-tree");
let index: Vec<FlatEntry> = parsed.entries.iter().map(|entry| { return FlatEntry { path: entry.path.clone(), mode: entry.mode, id: entry.id.clone() }; }).collect();
let database = ObjectDatabase::open(repo.join(".git").as_path(), None, format).expect("database");
let head_tree: Vec<u8> = crate::repo_read_object::from_hex(text(repo.as_path(), &["rev-parse", "HEAD^{tree}"]).as_bytes(), format).expect("name");
assert_eq!(changes_with_cache(&database, head_tree.as_slice(), format, &cache, index.as_slice()).map(|changes| { return changes.len(); }), Some(0), "the cache-tree as written gives no change");
cache.entry_count += 1;
assert!(changes_with_cache(&database, head_tree.as_slice(), format, &cache, index.as_slice()).is_none(), "a root count that is off declines");
remove(root.as_path());
}
#[test]
fn a_root_tree_that_is_gone_is_not_hidden_by_a_matching_cache_tree() {
let root = fixture("repo-read-cached-missing-root");
for format in ["sha1", "sha256"] {
let repo = repository_with(root.as_path(), format, format, 4, false);
let tree = text(repo.as_path(), &["rev-parse", "HEAD^{tree}"]);
let head = text(repo.as_path(), &["rev-parse", "HEAD"]);
answered(root.as_path(), repo.as_path(), &["diff-index", "--cached", "--quiet", head.as_str()]);
std::fs::remove_file(repo.join(".git/objects").join(&tree[..2]).join(&tree[2..])).expect("the tree is loose");
declined(root.as_path(), repo.as_path(), &["diff-index", "--cached", "--raw", "-z", "--no-renames", "--no-abbrev", head.as_str()]);
declined(root.as_path(), repo.as_path(), &["diff-index", "--cached", "--name-only", "-z", "--no-renames", head.as_str()]);
declined(root.as_path(), repo.as_path(), &["diff-index", "--cached", "--quiet", head.as_str()]);
}
remove(root.as_path());
}
#[test]
fn split_and_sparse_indexes_are_left_to_git_in_both_object_formats() {
let root = fixture("repo-read-cached-split-sparse");
for format in ["sha1", "sha256"] {
let split = committed_repository(root.as_path(), format!("split-{format}").as_str(), format, 4);
git(split.as_path(), &["update-index", "--split-index"]);
let head = text(split.as_path(), &["rev-parse", "HEAD"]);
declined(root.as_path(), split.as_path(), &["diff-index", "--cached", "--raw", "-z", "--no-renames", "--no-abbrev", head.as_str()]);
declined(root.as_path(), split.as_path(), &["diff-index", "--cached", "--quiet", head.as_str()]);
let sparse = committed_repository(root.as_path(), format!("sparse-{format}").as_str(), format, 4);
git(sparse.as_path(), &["sparse-checkout", "init", "--cone", "--sparse-index"]);
git(sparse.as_path(), &["sparse-checkout", "set", "d1"]);
let tip = text(sparse.as_path(), &["rev-parse", "HEAD"]);
declined(root.as_path(), sparse.as_path(), &["diff-index", "--cached", "--raw", "-z", "--no-renames", "--no-abbrev", tip.as_str()]);
declined(root.as_path(), sparse.as_path(), &["diff-index", "--cached", "--quiet", tip.as_str()]);
}
remove(root.as_path());
}