use super::*;
#[test]
fn the_partitions_stay_exact_through_every_kind_of_change() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
let config = test_config(root.path());
let export = |name: &str, prior: Option<&ExportPrior>| {
let mut snap = snapshot(&source, Vec::new());
let refs = git(
&source,
&[
"for-each-ref",
"--format=%(refname) %(objectname) %(*objectname)",
],
);
for line in refs.lines() {
let mut fields = line.split(' ');
let (name, raw) = (fields.next().unwrap(), fields.next().unwrap());
let peeled = fields.next().filter(|peeled| !peeled.is_empty());
if crate::snapshot::is_cob_ref(name) {
snap.cob_refs.push((name.into(), raw.into()));
} else if name.starts_with("refs/heads/") || name.starts_with("refs/tags/") {
snap.public_refs.push(crate::snapshot::PublicRef {
name: name.into(),
raw: raw.into(),
peeled: peeled.map(str::to_string),
});
}
}
let fingerprint = super::export_fingerprint(&snap, "main", prior);
let dir = root.path().join(name);
super::materialize_export(&config, &snap, &dir, &fingerprint, Some(&store), prior).unwrap();
assert_partitions_exact(root.path(), &source, &dir, &snap);
// a fast-forward keeps the packs the replies frame and only merges the ones after them.
let partitions = super::Partitions::read(&dir).unwrap().unwrap();
let grown =
prior.is_some_and(|prior| super::refs_fast_forwarded(prior, &snap, &config).unwrap());
if let Some(prior) = prior.filter(|_| grown) {
let entry = store
.read_directory(super::EXPORT_CACHE_KIND, &prior.fingerprint)
.unwrap();
let before = super::Partitions::read(&entry).unwrap().unwrap();
let tails = crate::consolidate::tails(&entry.join("smart")).unwrap();
let framed = [0, 1].map(|at| tails[0].packs()[at].max(tails[1].packs()[at]));
assert!(
partitions
.public()
.starts_with(&before.public()[..framed[0]]),
"{name} packed anew"
);
assert!(
partitions.cob().starts_with(&before.cob()[..framed[1]]),
"{name} packed anew"
);
}
let refs = exported_refs(&snap)
.map(|(name, raw)| (name.to_string(), raw.to_string()))
.collect();
let bases = super::fetch_replies(&dir).unwrap().bases;
(
dir,
grown,
ExportPrior {
fingerprint,
refs,
bases,
},
)
};
let tag = |args: &[&str]| {
let mut command = git_command(&source);
command
.args(["-c", "user.name=t", "-c", "user.email=t@t", "tag"])
.args(args);
run_cmd(command, "git tag").unwrap();
};
let mut state = 7u64;
let mut pick = |n: u64| {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(state >> 33) % n
};
let (_, _, mut prior) = export("export-0", None);
let mut grown = 0;
for step in 0..40 {
match pick(11) {
0 => commit(
&source,
&format!("f{step}.txt"),
&noise(step, 300),
"change",
),
1 => commit(&source, "a.txt", &noise(pick(3), 300), "back and forth"),
2 => drop(git(
&source,
&["branch", "-f", &format!("b{}", pick(3)), "main"],
)),
3 => {
git(&source, &["checkout", "-q", "-B", "side"]);
commit(
&source,
&format!("s{step}.txt"),
&noise(step + 50, 300),
"side",
);
git(&source, &["reset", "-q", "--hard", "HEAD~1"]);
commit(
&source,
&format!("t{step}.txt"),
&noise(step + 90, 300),
"forced",
);
git(&source, &["checkout", "-q", "main"]);
}
4 => {
let branches = git(
&source,
&["for-each-ref", "--format=%(refname:short)", "refs/heads/b*"],
);
if let Some(branch) = branches.lines().next() {
git(&source, &["branch", "-D", branch]);
}
}
5 => {
let target = match pick(4) {
0 => "main".to_string(),
1 => git(&source, &["rev-parse", "main^{tree}"]),
2 => git(&source, &["rev-parse", "main:hello.txt"]),
_ => "v0".to_string(),
};
if target != "v0" || git(&source, &["tag", "-l", "v0"]) == "v0" {
tag(&["-f", "-a", "-m", "tag", &format!("v{}", pick(3)), &target]);
}
}
6 => {
let ticket = format!("refs/cobs/org.tangled.track.ticket/{}", pick(2));
let parent = git(
&source,
&["for-each-ref", "--format=%(objectname)", &ticket],
);
let blob = git_stdin(
&source,
&["hash-object", "-w", "--stdin"],
&noise(step + 200, 100),
);
let tree = git_stdin(
&source,
&["mktree"],
&format!("100644 blob {blob}\tticket\n"),
);
let mut args = vec![
"-c",
"user.name=t",
"-c",
"user.email=t@t",
"commit-tree",
&tree,
"-m",
"cob",
];
if !parent.is_empty() {
args.extend(["-p", &parent]);
}
let cob = git(&source, &args);
git(&source, &["update-ref", &ticket, &cob]);
}
7 => {
git(&source, &["checkout", "-q", "-b", "pull"]);
commit(
&source,
&format!("p{step}.txt"),
&noise(step + 300, 300),
"pull",
);
let pull = git(&source, &["rev-parse", "HEAD"]);
git(&source, &["checkout", "-q", "main"]);
git(&source, &["branch", "-D", "pull"]);
git(
&source,
&["update-ref", &format!("refs/tngl/keep/{step}"), &pull],
);
}
8 => {
let keeps = git(
&source,
&["for-each-ref", "--format=%(objectname)", "refs/tngl/keep/"],
);
if let Some(pull) = keeps.lines().next() {
let mut merge = git_command(&source);
merge.args([
"-c",
"user.name=t",
"-c",
"user.email=t@t",
"merge",
"-q",
"--no-ff",
"-m",
"merge",
pull,
]);
run_cmd(merge, "git merge").unwrap();
}
}
9 => {
commit(
&source,
&format!("r{step}.txt"),
&noise(step + 400, 300),
"tagged",
);
tag(&["-f", "-a", "-m", "tag", &format!("v{}", pick(3)), "main"]);
let mut show = git_command(&source);
show.args(["show", "main:a.txt"]);
let text = String::from_utf8(run_cmd(show, "git show")?.stdout)?;
commit(&source, "a.txt", &noise(step + 500, 300), "changed");
let (dir, _, next) = export(&format!("export-{step}-changed"), Some(&prior));
assert_have_replies_whole(root.path(), &dir)?;
prior = next;
commit(&source, "a.txt", &text, "restored");
}
_ => {
let tags = git(&source, &["tag", "-l"]);
if let Some(name) = tags.lines().next() {
git(&source, &["tag", "-d", name]);
}
}
}
let (dir, delta, next) = export(&format!("export-{}", step + 1), Some(&prior));
assert_have_replies_whole(root.path(), &dir)?;
grown += usize::from(delta);
prior = next;
}
assert!(grown > 20, "only {grown} exports were grown");
Ok(())
}
/// the lent graph names hidden commits too, and none of them may reach a reply or clone.
#[test]
fn hidden_commits_the_lent_graph_names_reach_no_reply() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
commit(&source, "secret.txt", &noise(9, 4096), "secret");
let hidden = git(&source, &["rev-parse", "HEAD"]);
let secret = git(&source, &["rev-parse", "HEAD:secret.txt"]);
git(&source, &["update-ref", "refs/hidden/x", &hidden]);
git(&source, &["reset", "-q", "--hard", "HEAD~1"]);
crate::commit_graph::refresh(&source);
let (first, prior) = export_against(root.path(), &source, "export-1", None, &store);
commit(&source, "two.txt", "two\n", "two");
crate::commit_graph::refresh(&source);
let (second, _) = export_against(root.path(), &source, "export-2", Some(&prior), &store);
let replies = ["upload-pack", "fetch-done", "have-", "moved-", "shallow-"];
let lists = [super::SHALLOW_BRANCHES, super::MOVED_WANTS];
for export in [&first, &second] {
let info = export.join("objects/info");
assert!(!info.join("commit-graph").exists() && !info.join("commit-graphs").exists());
for entry in std::fs::read_dir(export.join("smart"))? {
let path = entry?.path();
let name = path.file_name().unwrap().to_string_lossy().into_owned();
let reply = replies.iter().any(|reply| name.starts_with(reply));
let map = name.ends_with(".map");
if !path.is_file() || !reply || map || lists.contains(&name.as_str()) {
continue;
}
let alone = tempfile::tempdir_in(root.path())?;
git(alone.path(), &["init", "-q", "--bare"]);
let mut index = git_command(alone.path());
index.args(["index-pack", "--stdin"]);
index.stdin(stdin_bytes(&demux(&std::fs::read(&path)?).1)?);
run_cmd(index, "git index-pack")?;
let all = [
"cat-file",
"--batch-all-objects",
"--batch-check=%(objectname)",
];
let objects = git(alone.path(), &all);
assert!(
!objects.contains(&hidden) && !objects.contains(&secret),
"{name}"
);
}
}
let clone = clone_over_http(root.path(), &second, "clone")?;
git(&clone, &["fsck", "--strict"]);
let mut probe = git_command(&clone);
probe.args(["cat-file", "-e", &secret]);
assert!(
run_cmd(probe, "probe").is_err(),
"the hidden blob reached a clone"
);
Ok(())
}
#[test]
fn a_lent_graph_holds_through_the_sources_rewrite() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
for message in ["old", "older"] {
let mut backdated = git_command(&source);
backdated
.args(["commit", "-q", "--allow-empty", "-m", message])
.env("GIT_AUTHOR_NAME", "t")
.env("GIT_AUTHOR_EMAIL", "t@t")
.env("GIT_COMMITTER_NAME", "t")
.env("GIT_COMMITTER_EMAIL", "t@t")
.env("GIT_COMMITTER_DATE", "1000000000 +0000");
run_cmd(backdated, "git commit")?;
}
crate::commit_graph::refresh(&source);
let (export, _) = export_against(root.path(), &source, "export", None, &store);
let tip = git(&source, &["rev-parse", "main"]);
let count = git(&source, &["rev-list", "--count", "main"]);
let info = export.join("objects/info");
let lent = super::LentGraph::lend(&source, &export);
let graph = gix::commitgraph::Graph::from_info_dir(&info).unwrap();
let tip = gix::ObjectId::from_hex(tip.as_bytes()).unwrap();
assert!(graph.lookup(tip).is_some());
crate::commit_graph::rewrite(&source);
assert_eq!(git(&export, &["rev-list", "--count", "main"]), count);
let layers = info.join("commit-graphs");
for layer in std::fs::read_dir(&layers)? {
let layer = layer?.path();
if layer.extension().is_some_and(|ext| ext == "graph") {
std::fs::remove_file(layer)?;
break;
}
}
assert_eq!(git(&export, &["rev-list", "--count", "main"]), count);
drop(lent);
assert!(!layers.exists() && !info.join("commit-graph").exists());
assert!(gix::commitgraph::Graph::from_info_dir(&source.join(".git/objects/info")).is_ok());
Ok(())
}
#[test]
fn a_source_pack_with_hidden_objects_is_not_linked() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let source = init_test_source(root.path());
let main = git(&source, &["rev-parse", "main"]);
let hidden = orphan_commit(&source, "hidden");
git(&source, &["update-ref", "refs/atproto/commit", &hidden]);
git(&source, &["repack", "-adq"]);
let snap = snapshot(&source, vec![("refs/heads/main".to_string(), main)]);
let export = root.path().join("export");
export_repo_test(root.path(), &snap, &export);
assert!(pack_inodes(&source.join(".git")).is_disjoint(&pack_inodes(&export)));
let mut probe = git_command(&export);
probe.args(["cat-file", "-e", &hidden]);
assert!(
run_cmd(probe, "probe").is_err(),
"the hidden commit reached the export"
);
Ok(())
}