use girt::ObjectFormat;
use girt::commit_graph::{CommitGraph, GraphError, GraphLimits};
use rstest::rstest;
#[allow(dead_code)]
#[path = "support/history_git.rs"]
mod history_git;
use history_git::{History, git};
fn graph(h: &History, version: &str) -> Vec<u8> {
git(
h.root.path(),
&[
"-c",
&format!("commitGraph.generationVersion={version}"),
"commit-graph",
"write",
"--reachable",
],
b"",
);
git(h.root.path(), &["commit-graph", "verify"], b"");
std::fs::read(h.root.path().join("objects/info/commit-graph")).unwrap()
}
#[rstest]
fn git_metadata_matches_objects(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
#[values("1", "2")] version: &str,
) {
let h = History::with_format(
&[vec![], vec![0], vec![0], vec![0], vec![1, 2, 3]],
false,
format,
);
let bytes = graph(&h, version);
let graph = CommitGraph::parse(&bytes, format).unwrap();
assert_eq!(graph.commits().len(), 5);
let merge = graph.get(h.ids[4]).unwrap();
assert_eq!(merge.parents, vec![h.ids[1], h.ids[2], h.ids[3]]);
assert_eq!(merge.generation, 3);
assert_eq!(merge.commit_time, 1700000100);
assert_eq!(
merge.corrected_commit_date,
if version == "2" {
Some(1700000102)
} else {
None
}
);
assert_eq!(
merge.tree,
h.objects().read_commit(h.ids[4]).unwrap().tree()
);
assert!(graph.get(h.ids[0]).unwrap().parents.is_empty());
}
#[rstest]
fn truncation_and_damage_are_rejected(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![], vec![0]], false, format);
let bytes = graph(&h, "2");
assert!((0..bytes.len()).all(|length| CommitGraph::parse(&bytes[..length], format).is_err()));
let mut damaged = bytes;
damaged[8] ^= 1;
assert!(matches!(
CommitGraph::parse(&damaged, format),
Err(GraphError::Checksum)
));
}
#[rstest]
fn caller_bounds_are_enforced(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![], vec![0]], false, format);
let bytes = graph(&h, "2");
let mut limits = GraphLimits::default();
limits.max_commits = 1;
assert!(matches!(
CommitGraph::parse_with(&bytes, format, limits),
Err(GraphError::Limit("commits"))
));
limits.max_commits = 2;
limits.max_parents = 0;
assert!(matches!(
CommitGraph::parse_with(&bytes, format, limits),
Err(GraphError::Limit("parents"))
));
limits.max_bytes = 0;
assert!(matches!(
CommitGraph::parse_with(&bytes, format, limits),
Err(GraphError::Limit("bytes"))
));
}
#[rstest]
fn split_layers_resolve_base_parents(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![], vec![0], vec![1]], false, format);
write_layer(&h, h.ids[0]);
write_layer(&h, h.ids[1]);
write_layer(&h, h.ids[2]);
git(h.root.path(), &["commit-graph", "verify"], b"");
let graph = CommitGraph::open(h.root.path().join("objects").as_path(), format)
.unwrap()
.unwrap();
assert_eq!(graph.commits().len(), 3);
assert_eq!(graph.get(h.ids[2]).unwrap().parents, vec![h.ids[1]]);
graph.verify(&h.objects()).unwrap();
let repo = girt::Repository::open(h.root.path()).unwrap();
assert_eq!(repo.commit_graph().unwrap().unwrap().commits().len(), 3);
}
fn write_layer(h: &History, tip: girt::ObjectId) {
git(
h.root.path(),
&[
"commit-graph",
"write",
"--stdin-commits",
"--split=no-merge",
],
format!("{tip}\n").as_bytes(),
);
}
#[rstest]
fn disabled_graph_is_not_opened(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![]], false, format);
graph(&h, "2");
std::fs::remove_file(h.root.path().join("objects/info/commit-graph")).unwrap();
std::fs::write(h.root.path().join("objects/info/commit-graph"), b"damaged").unwrap();
git(h.root.path(), &["config", "core.commitGraph", "false"], b"");
let repo = girt::Repository::open(h.root.path()).unwrap();
assert!(repo.commit_graph().unwrap().is_none());
assert_eq!(repo.objects().unwrap().walk(&h.ids).unwrap(), h.ids);
}
#[rstest]
fn missing_chain_layer_is_reported(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![], vec![0]], false, format);
write_layer(&h, h.ids[0]);
write_layer(&h, h.ids[1]);
let directory = h.root.path().join("objects/info/commit-graphs");
let chain = std::fs::read_to_string(directory.join("commit-graph-chain")).unwrap();
let base = chain.lines().next().unwrap();
std::fs::remove_file(directory.join(format!("graph-{base}.graph"))).unwrap();
assert!(CommitGraph::open(&h.root.path().join("objects"), format).is_err());
assert_eq!(
h.objects().walk(&[h.ids[1]]).unwrap(),
vec![h.ids[1], h.ids[0]]
);
}
#[rstest]
fn corrected_date_overflow_matches_git(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![]], false, format);
let tree = h.objects().read_commit(h.ids[0]).unwrap().tree();
let parent = dated_commit(&h, tree, &[], 5_000_000_000);
let child = dated_commit(&h, tree, &[parent], 1);
git(
h.root.path(),
&["update-ref", "refs/heads/overflow", &child.to_string()],
b"",
);
let graph = CommitGraph::parse(&graph(&h, "2"), format).unwrap();
assert_eq!(graph.get(parent).unwrap().commit_time, 5_000_000_000);
assert_eq!(
graph.get(child).unwrap().corrected_commit_date,
Some(5_000_000_001)
);
graph.verify(&h.objects()).unwrap();
}
fn dated_commit(
h: &History,
tree: girt::ObjectId,
parents: &[girt::ObjectId],
time: u64,
) -> girt::ObjectId {
let mut payload = format!("tree {tree}\n");
for parent in parents {
payload.push_str(&format!("parent {parent}\n"));
}
payload.push_str(&format!("author A <a@example.com> {time} +0000\ncommitter A <a@example.com> {time} +0000\n\ndate fixture\n"));
let output = git(
h.root.path(),
&["hash-object", "-w", "-t", "commit", "--stdin"],
payload.as_bytes(),
);
std::str::from_utf8(&output)
.unwrap()
.trim()
.parse()
.unwrap()
}
#[rstest]
fn indexed_history_matches_git_and_keeps_budgets(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(
&[vec![], vec![0], vec![0], vec![1, 2], vec![2, 1]],
false,
format,
);
let graph = CommitGraph::parse(&graph(&h, "2"), format).unwrap();
let objects = h.objects().with_commit_graph(graph).unwrap();
let mut walked = objects.walk(&[h.ids[3], h.ids[4]]).unwrap();
walked.sort();
assert_eq!(walked, h.query("rev-list", &[h.ids[3], h.ids[4]]));
assert_eq!(
objects.merge_bases(h.ids[3], h.ids[4]).unwrap(),
h.query("merge-base", &[h.ids[3], h.ids[4]])
);
assert!(objects.is_ancestor(h.ids[0], h.ids[3]).unwrap());
assert!(!objects.is_ancestor(h.ids[3], h.ids[4]).unwrap());
assert!(
objects
.walk_with(
&[h.ids[3]],
girt::HistoryLimits::default().with_max_commits(1)
)
.is_err()
);
assert!(
objects
.walk_with(
&[h.ids[3]],
girt::HistoryLimits::default().with_max_parents(0)
)
.is_err()
);
let mut range = objects.walk_range(&[h.ids[3]], &[h.ids[1]]).unwrap();
range.sort();
assert_eq!(
range,
vec![h.ids[2], h.ids[3]]
.into_iter()
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>()
);
}
#[rstest]
fn partial_index_falls_back_to_objects(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![], vec![0], vec![1]], false, format);
git(
h.root.path(),
&["commit-graph", "write", "--stdin-commits"],
format!("{}\n", h.ids[0]).as_bytes(),
);
let graph = CommitGraph::open(&h.root.path().join("objects"), format)
.unwrap()
.unwrap();
assert_eq!(graph.commits().len(), 1);
assert_eq!(
h.objects()
.with_commit_graph(graph)
.unwrap()
.walk(&[h.ids[2]])
.unwrap(),
vec![h.ids[2], h.ids[1], h.ids[0]]
);
}
#[rstest]
#[case::tip_excluded(vec![4], vec![4])]
#[case::near_tip(vec![4], vec![3])]
#[case::side_branch(vec![3], vec![2])]
#[case::multiple_roots(vec![3,4], vec![1])]
#[case::duplicate_roots(vec![4,4], vec![2,2])]
#[case::no_excludes(vec![4], vec![])]
fn indexed_range_matches_git(
#[case] include: Vec<usize>,
#[case] exclude: Vec<usize>,
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
#[values("1", "2")] version: &str,
) {
let h = History::with_format(
&[vec![], vec![0], vec![0], vec![1, 2], vec![3]],
false,
format,
);
let graph = CommitGraph::parse(&graph(&h, version), format).unwrap();
let objects = h.objects().with_commit_graph(graph).unwrap();
let include: Vec<_> = include.iter().map(|i| h.ids[*i]).collect();
let exclude: Vec<_> = exclude.iter().map(|i| h.ids[*i]).collect();
let mut actual = objects.walk_range(&include, &exclude).unwrap();
actual.sort();
let mut args = vec!["rev-list".to_owned()];
args.extend(include.iter().map(ToString::to_string));
args.extend(exclude.iter().map(|id| format!("^{id}")));
let args: Vec<_> = args.iter().map(String::as_str).collect();
let output = git(h.root.path(), &args, b"");
let mut expected: Vec<girt::ObjectId> = std::str::from_utf8(&output)
.unwrap()
.lines()
.map(|s| s.parse().unwrap())
.collect();
expected.sort();
assert_eq!(actual, expected);
}
#[rstest]
fn indexed_near_tip_queries_stop_before_old_history(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![], vec![0], vec![1], vec![2], vec![3]], false, format);
let graph = CommitGraph::parse(&graph(&h, "2"), format).unwrap();
let objects = h.objects().with_commit_graph(graph).unwrap();
let limits = girt::HistoryLimits::default()
.with_max_commits(2)
.with_max_parents(1);
assert_eq!(
objects
.walk_range_with(&[h.ids[4]], &[h.ids[3]], limits)
.unwrap(),
vec![h.ids[4]]
);
assert!(
objects
.is_ancestor_with(h.ids[3], h.ids[4], limits)
.unwrap()
);
}
#[rstest]
fn indexed_ahead_behind_stops_at_shared_history(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
#[values("1", "2")] version: &str,
) {
let mut parents: Vec<Vec<usize>> = vec![vec![]];
parents.extend((0..9).map(|i| vec![i]));
parents.extend([vec![9], vec![9], vec![11]]);
let h = History::with_format(&parents, false, format);
let graph = CommitGraph::parse(&graph(&h, version), format).unwrap();
let objects = h.objects().with_commit_graph(graph).unwrap();
let limits = girt::HistoryLimits::default()
.with_max_commits(4)
.with_max_parents(3);
let counts = objects
.ahead_behind_with(h.ids[10], h.ids[12], limits)
.unwrap();
assert_eq!((counts.ahead, counts.behind), (1, 2));
assert!(matches!(
objects.ahead_behind_with(h.ids[10], h.ids[12], limits.with_max_commits(3)),
Err(girt::HistoryError::Limit("commits"))
));
}
#[rstest]
fn indexed_shallow_boundaries_override_parents(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![], vec![0], vec![1]], false, format);
let graph = CommitGraph::parse(&graph(&h, "2"), format).unwrap();
std::fs::write(h.root.path().join("shallow"), format!("{}\n", h.ids[1])).unwrap();
let objects = h.objects().with_commit_graph(graph).unwrap();
assert_eq!(objects.walk(&[h.ids[2]]).unwrap(), vec![h.ids[2], h.ids[1]]);
assert!(!objects.is_ancestor(h.ids[0], h.ids[2]).unwrap());
assert_eq!(
objects.walk_range(&[h.ids[2]], &[h.ids[1]]).unwrap(),
vec![h.ids[2]]
);
}
#[rstest]
fn bloom_filters_never_exclude_changed_paths(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
#[values("1", "2")] version: &str,
) {
let (h, changed, unchanged) = path_fixture(format);
git(
h.root.path(),
&[
"-c",
&format!("commitGraph.changedPathsVersion={version}"),
"commit-graph",
"write",
"--reachable",
"--changed-paths",
],
b"",
);
git(h.root.path(), &["commit-graph", "verify"], b"");
let graph = CommitGraph::open(&h.root.path().join("objects"), format)
.unwrap()
.unwrap();
assert_eq!(graph.might_change_path(changed, b"notes"), Some(true));
assert_eq!(graph.might_change_path(unchanged, b"notes"), Some(false));
assert_eq!(graph.might_change_path(h.ids[0], b"notes"), Some(false));
let objects = h.objects().with_commit_graph(graph).unwrap();
assert_eq!(
objects.walk_path_first_parent(unchanged, b"notes").unwrap(),
vec![changed]
);
assert_eq!(
objects.blame(unchanged, b"notes").unwrap()[0].commit,
changed
);
let git_paths = git(
h.root.path(),
&[
"log",
"--first-parent",
"--format=%H",
&unchanged.to_string(),
"--",
"notes",
],
b"",
);
assert_eq!(
std::str::from_utf8(&git_paths).unwrap().trim(),
changed.to_string()
);
}
fn path_fixture(format: ObjectFormat) -> (History, girt::ObjectId, girt::ObjectId) {
let h = History::with_format(&[vec![]], false, format);
let blob = git(h.root.path(), &["hash-object", "-w", "--stdin"], b"line\n");
let blob = std::str::from_utf8(&blob).unwrap().trim();
let first = git(
h.root.path(),
&["mktree"],
format!("100644 blob {blob}\tnotes\n").as_bytes(),
);
let first = std::str::from_utf8(&first).unwrap().trim().parse().unwrap();
let changed = dated_commit(&h, first, &[h.ids[0]], 1700000200);
let second = git(
h.root.path(),
&["mktree"],
format!("100644 blob {blob}\tnotes\n100644 blob {blob}\tother\n").as_bytes(),
);
let second = std::str::from_utf8(&second)
.unwrap()
.trim()
.parse()
.unwrap();
let unchanged = dated_commit(&h, second, &[changed], 1700000300);
git(
h.root.path(),
&["update-ref", "refs/heads/paths", &unchanged.to_string()],
b"",
);
(h, changed, unchanged)
}
#[rstest]
#[case::utf8(b"dir/\xc3\xa9file".to_vec())]
#[case::raw_high_byte(b"dir/\xfffile".to_vec())]
#[case::long_path([vec![b'a'; 200], vec![b'b'; 200], vec![b'c'; 200], b"notes".to_vec()].join(&b'/'))]
fn bloom_changed_paths_and_directory_prefixes(
#[case] path: Vec<u8>,
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
#[values("1", "2")] version: &str,
) {
let h = History::with_format(&[vec![]], false, format);
let tree = nested_tree(&h, &path);
let changed = dated_commit(&h, tree, &[], 1700000200);
git(
h.root.path(),
&["update-ref", "refs/heads/paths", &changed.to_string()],
b"",
);
write_blooms(&h, version);
let graph = CommitGraph::open(&h.root.path().join("objects"), format)
.unwrap()
.unwrap();
assert!(path_prefixes(&path).iter().all(|prefix| {
let expected = if version == "1" && !prefix.is_ascii() {
None
} else {
Some(true)
};
graph.might_change_path(changed, prefix) == expected
}));
assert_eq!(
h.objects()
.with_commit_graph(graph)
.unwrap()
.walk_path_first_parent(changed, &path)
.unwrap(),
vec![changed]
);
}
fn path_prefixes(path: &[u8]) -> Vec<&[u8]> {
path.iter()
.enumerate()
.filter_map(|(i, b)| (*b == b'/').then_some(&path[..i]))
.chain(std::iter::once(path))
.collect()
}
fn nested_tree(h: &History, path: &[u8]) -> girt::ObjectId {
let output = git(
h.root.path(),
&["hash-object", "-w", "--stdin"],
b"original\n",
);
let mut id: girt::ObjectId = std::str::from_utf8(&output)
.unwrap()
.trim()
.parse()
.unwrap();
let mut mode = "100644 blob";
for name in path.split(|b| *b == b'/').rev() {
let mut entry = format!("{mode} {id}\t").into_bytes();
entry.extend_from_slice(name);
entry.push(0);
let output = git(h.root.path(), &["mktree", "-z"], &entry);
id = std::str::from_utf8(&output)
.unwrap()
.trim()
.parse()
.unwrap();
mode = "040000 tree";
}
id
}
fn write_blooms(h: &History, version: &str) {
git(
h.root.path(),
&[
"-c",
&format!("commitGraph.changedPathsVersion={version}"),
"commit-graph",
"write",
"--reachable",
"--changed-paths",
],
b"",
);
git(h.root.path(), &["commit-graph", "verify"], b"");
}
#[rstest]
fn bloom_merge_filter_describes_only_first_parent(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
#[values("1", "2")] version: &str,
) {
let (h, changed, _) = path_fixture(format);
let tree = h.objects().read_commit(changed).unwrap().tree();
let merge = dated_commit(&h, tree, &[changed, h.ids[0]], 1700000400);
git(
h.root.path(),
&["update-ref", "refs/heads/merge", &merge.to_string()],
b"",
);
write_blooms(&h, version);
let graph = CommitGraph::open(&h.root.path().join("objects"), format)
.unwrap()
.unwrap();
assert_eq!(graph.might_change_path(merge, b"notes"), Some(false));
let objects = h.objects().with_commit_graph(graph).unwrap();
assert_eq!(
objects.walk_path_first_parent(merge, b"notes").unwrap(),
vec![changed]
);
assert_eq!(objects.blame(merge, b"notes").unwrap()[0].commit, changed);
}
#[rstest]
fn bloom_large_change_filter_is_always_conservative(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
#[values("1", "2")] version: &str,
) {
let h = History::with_format(&[vec![]], false, format);
let output = git(h.root.path(), &["hash-object", "-w", "--stdin"], b"one\n");
let blob = std::str::from_utf8(&output).unwrap().trim();
let entries: String = (0..600)
.map(|i| format!("100644 blob {blob}\tfile-{i:04}\n"))
.collect();
let output = git(h.root.path(), &["mktree"], entries.as_bytes());
let tree = std::str::from_utf8(&output)
.unwrap()
.trim()
.parse()
.unwrap();
let large = dated_commit(&h, tree, &[h.ids[0]], 1700000500);
git(
h.root.path(),
&["update-ref", "refs/heads/large", &large.to_string()],
b"",
);
write_blooms(&h, version);
let graph = CommitGraph::open(&h.root.path().join("objects"), format)
.unwrap()
.unwrap();
assert_eq!(graph.might_change_path(large, b"file-0000"), Some(true));
assert_eq!(graph.might_change_path(large, b"not-present"), Some(true));
}
#[rstest]
fn girt_writes_graphs_git_can_verify(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(
&[vec![], vec![0], vec![0], vec![0], vec![1, 2, 3]],
false,
format,
);
let graph = CommitGraph::from_objects(&h.objects(), &[h.ids[4]]).unwrap();
let bytes = graph.encode().unwrap();
std::fs::create_dir_all(h.root.path().join("objects/info")).unwrap();
std::fs::write(h.root.path().join("objects/info/commit-graph"), &bytes).unwrap();
git(h.root.path(), &["commit-graph", "verify"], b"");
assert_eq!(
CommitGraph::parse(&bytes, format).unwrap().commits(),
graph.commits()
);
}
#[rstest]
fn girt_incremental_layers_match_git(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![], vec![0], vec![1]], false, format);
let base = CommitGraph::from_objects(&h.objects(), &[h.ids[0]])
.unwrap()
.encode()
.unwrap();
let all = CommitGraph::from_objects(&h.objects(), &[h.ids[2]]).unwrap();
let tip = all.encode_incremental(&[&base]).unwrap();
let parsed = CommitGraph::parse_chain(&[&base, &tip], format).unwrap();
assert_eq!(parsed.commits().len(), 3);
let directory = h.root.path().join("objects/info/commit-graphs");
std::fs::create_dir_all(&directory).unwrap();
let first = install_layer(&directory, &base, format);
let second = install_layer(&directory, &tip, format);
std::fs::write(
directory.join("commit-graph-chain"),
format!("{first}\n{second}\n"),
)
.unwrap();
git(h.root.path(), &["commit-graph", "verify"], b"");
}
fn install_layer(
directory: &std::path::Path,
bytes: &[u8],
format: ObjectFormat,
) -> girt::ObjectId {
let checksum =
girt::ObjectId::from_bytes(format, &bytes[bytes.len() - format.digest_len()..]).unwrap();
std::fs::write(directory.join(format!("graph-{checksum}.graph")), bytes).unwrap();
checksum
}
#[rstest]
fn writer_handles_corrected_date_overflow(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![]], false, format);
let tree = h.objects().read_commit(h.ids[0]).unwrap().tree();
let parent = dated_commit(&h, tree, &[], 5_000_000_000);
let child = dated_commit(&h, tree, &[parent], 1);
let graph = CommitGraph::from_objects(&h.objects(), &[child]).unwrap();
let bytes = graph.encode().unwrap();
std::fs::create_dir_all(h.root.path().join("objects/info")).unwrap();
std::fs::write(h.root.path().join("objects/info/commit-graph"), &bytes).unwrap();
git(h.root.path(), &["commit-graph", "verify"], b"");
assert_eq!(
CommitGraph::parse(&bytes, format)
.unwrap()
.get(child)
.unwrap()
.corrected_commit_date,
Some(5_000_000_001)
);
}
#[rstest]
fn graph_writer_keeps_input_bounds_and_rejects_shallow_history(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![], vec![0]], false, format);
assert!(
CommitGraph::from_objects_with(
&h.objects(),
&[h.ids[1]],
girt::HistoryLimits::default().with_max_commits(1)
)
.is_err()
);
assert!(
CommitGraph::from_objects_with(
&h.objects(),
&[h.ids[1]],
girt::HistoryLimits::default().with_max_parents(0)
)
.is_err()
);
std::fs::write(h.root.path().join("shallow"), format!("{}\n", h.ids[1])).unwrap();
assert!(matches!(
CommitGraph::from_objects(&h.objects(), &[h.ids[1]]),
Err(girt::commit_graph::GraphWriteError::Shallow)
));
}
#[rstest]
fn graph_publication_migrates_full_to_split_without_changing_objects(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
use girt::commit_graph::GraphWriteMode;
let h = History::with_format(&[vec![], vec![0], vec![1]], false, format);
let object_dir = h.root.path().join("objects");
let base = CommitGraph::from_objects(&h.objects(), &[h.ids[0]]).unwrap();
base.install(&object_dir, GraphWriteMode::Full).unwrap();
git(h.root.path(), &["commit-graph", "verify"], b"");
let all = CommitGraph::from_objects(&h.objects(), &[h.ids[2]]).unwrap();
let report = all
.install(&object_dir, GraphWriteMode::Incremental)
.unwrap();
assert_eq!(report.layers, 2);
assert_eq!(report.commits_written, 2);
assert!(!object_dir.join("info/commit-graph").exists());
assert_eq!(
CommitGraph::open(&object_dir, format)
.unwrap()
.unwrap()
.commits()
.len(),
3
);
git(h.root.path(), &["commit-graph", "verify"], b"");
git(h.root.path(), &["fsck", "--full"], b"");
}
#[rstest]
fn held_graph_lock_preserves_previous_index(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
use girt::commit_graph::GraphWriteMode;
let h = History::with_format(&[vec![], vec![0]], false, format);
let original = graph(&h, "2");
let directory = h.root.path().join("objects");
std::fs::write(directory.join("info/commit-graph.lock"), b"held").unwrap();
let graph = CommitGraph::from_objects(&h.objects(), &[h.ids[1]]).unwrap();
assert!(graph.install(&directory, GraphWriteMode::Full).is_err());
assert_eq!(
std::fs::read(directory.join("info/commit-graph")).unwrap(),
original
);
assert_eq!(
std::fs::read(directory.join("info/commit-graph.lock")).unwrap(),
b"held"
);
}
#[rstest]
fn failed_manifest_publication_preserves_standalone_graph(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
use girt::commit_graph::GraphWriteMode;
let h = History::with_format(&[vec![], vec![0]], false, format);
let original = graph(&h, "2");
let directory = h.root.path().join("objects");
std::fs::create_dir_all(directory.join("info/commit-graphs/commit-graph-chain")).unwrap();
let graph = CommitGraph::from_objects(&h.objects(), &[h.ids[1]]).unwrap();
let failure = graph
.install(&directory, GraphWriteMode::Incremental)
.unwrap_err();
assert!(!failure.index_published);
assert_eq!(failure.operation, "publish chain manifest");
assert_eq!(
std::fs::read(directory.join("info/commit-graph")).unwrap(),
original
);
assert!(!directory.join("info/commit-graph.lock").exists());
assert!(
!directory
.join("info/commit-graphs/commit-graph-chain.lock")
.exists()
);
git(h.root.path(), &["commit-graph", "verify"], b"");
}
#[rstest]
fn git_gc_and_maintenance_write_optional_graphs(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![], vec![0]], false, format);
git(h.root.path(), &["gc"], b"");
assert!(h.root.path().join("objects/info/commit-graph").is_file());
std::fs::remove_file(h.root.path().join("objects/info/commit-graph")).unwrap();
git(
h.root.path(),
&["-c", "gc.writeCommitGraph=false", "gc"],
b"",
);
assert!(!h.root.path().join("objects/info/commit-graph").exists());
git(h.root.path(), &["maintenance", "run"], b"");
assert!(!h.root.path().join("objects/info/commit-graph").exists());
assert!(
h.root
.path()
.join("objects/info/commit-graphs/commit-graph-chain")
.is_file()
);
git(
h.root.path(),
&["maintenance", "run", "--task=commit-graph"],
b"",
);
assert!(
h.root
.path()
.join("objects/info/commit-graphs/commit-graph-chain")
.is_file()
);
git(h.root.path(), &["commit-graph", "verify"], b"");
}
#[rstest]
fn empty_graph_artifact_is_accepted_by_git(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let h = History::with_format(&[vec![]], false, format);
let graph = CommitGraph::from_objects(&h.objects(), &[]).unwrap();
graph
.install(
&h.root.path().join("objects"),
girt::commit_graph::GraphWriteMode::Full,
)
.unwrap();
git(h.root.path(), &["commit-graph", "verify"], b"");
}
#[test]
fn empty_index_retains_its_hash_format_at_attachment() {
let source = History::with_format(&[vec![]], false, ObjectFormat::Sha1);
let target = History::with_format(&[vec![]], false, ObjectFormat::Sha256);
let graph = CommitGraph::from_objects(&source.objects(), &[]).unwrap();
assert!(matches!(
target.objects().with_commit_graph(graph),
Err(GraphError::HashFormat)
));
}
#[rstest]
fn repository_readers_trust_graphs_without_decoding_commits(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
#[values(false, true)] controlled: bool,
) {
let h = History::with_format(&[vec![], vec![0]], false, format);
graph(&h, "2");
for id in &h.ids {
let hex = id.to_string();
std::fs::remove_file(
h.root
.path()
.join("objects")
.join(&hex[..2])
.join(&hex[2..]),
)
.unwrap();
}
let repo = girt::Repository::open(h.root.path()).unwrap();
let objects = if controlled {
repo.objects_controlled(
girt::PackLimits::default(),
girt::AlternateLimits::default(),
&std::sync::atomic::AtomicBool::new(false),
)
.unwrap()
} else {
repo.objects().unwrap()
};
assert!(objects.is_ancestor(h.ids[0], h.ids[1]).unwrap());
assert!(
repo.commit_graph()
.unwrap()
.unwrap()
.verify(&objects)
.is_err()
);
assert!(
objects
.without_commit_graph()
.is_ancestor(h.ids[0], h.ids[1])
.is_err()
);
git(h.root.path(), &["config", "core.commitGraph", "false"], b"");
let disabled = girt::Repository::open(h.root.path()).unwrap();
assert!(
disabled
.objects()
.unwrap()
.is_ancestor(h.ids[0], h.ids[1])
.is_err()
);
}
#[rstest]
fn repository_readers_fall_back_from_unusable_graphs(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
#[values(false, true)] cyclic: bool,
) {
let h = History::with_format(&[vec![], vec![0]], false, format);
let mut bytes = graph(&h, "2");
if cyclic {
let (entries, _) = bytes[8..].as_chunks::<12>();
let chunk = entries.iter().find(|entry| &entry[..4] == b"CDAT").unwrap();
let offset = u64::from_be_bytes(chunk[4..12].try_into().unwrap()) as usize;
for position in 0..2u32 {
let at = offset + position as usize * (format.digest_len() + 16) + format.digest_len();
bytes[at..at + 4].copy_from_slice(&position.to_be_bytes());
}
bytes.truncate(bytes.len() - format.digest_len());
use sha2::Digest;
let checksum = match format {
ObjectFormat::Sha1 => sha1::Sha1::digest(&bytes).to_vec(),
ObjectFormat::Sha256 => sha2::Sha256::digest(&bytes).to_vec(),
_ => unreachable!("test only supplies SHA-1 and SHA-256"),
};
bytes.extend_from_slice(&checksum);
} else {
bytes[8] ^= 1;
}
let graph_path = h.root.path().join("objects/info/commit-graph");
std::fs::remove_file(&graph_path).unwrap();
std::fs::write(graph_path, bytes).unwrap();
let repo = girt::Repository::open(h.root.path()).unwrap();
assert_eq!(
repo.objects().unwrap().walk(&[h.ids[1]]).unwrap(),
vec![h.ids[1], h.ids[0]]
);
}