use super::*;
#[test]
fn fetch_replies_answer_a_base_of_independent_tips_with_ready() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
git(&source, &["branch", "feature"]);
commit(&source, "two.txt", "two\n", "two");
let (first, prior) = export_against(root.path(), &source, "export-1", None, &store);
assert_eq!(
super::fetch_replies(&first)?,
super::FetchReplies {
bases: Vec::new(),
shallow_branches: vec![git(&source, &["rev-parse", "feature"])],
shallow_head: Some(git(&source, &["rev-parse", "main"])),
moved_wants: Vec::new(),
}
);
assert!(
std::fs::read(first.join("smart/shallow-head"))?.starts_with(&pkt_line(b"shallow-info\n"))
);
commit(&source, "three.txt", "three\n", "three");
let (second, _) = export_against(root.path(), &source, "export-2", Some(&prior), &store);
// git never sends a have for feature, it's under the old main.
let old_main = prior.refs["refs/heads/main"].clone();
assert_eq!(
super::fetch_replies(&second)?.bases,
vec![vec![old_main.clone()]]
);
let mut round = pkt_line(b"acknowledgments\n");
round.extend(pkt_line(format!("ACK {old_main}\n").as_bytes()));
round.extend(pkt_line(b"ready\n"));
round.extend_from_slice(b"0001");
round.extend(pkt_line(b"packfile\n"));
assert!(std::fs::read(second.join("smart/have-0"))?.starts_with(&round));
assert!(std::fs::read(second.join("smart/have-0-done"))?.starts_with(&pkt_line(b"packfile\n")));
Ok(())
}
#[test]
fn have_replies_share_the_prior_aside_and_each_other() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
commit(&source, "old/big.txt", &noise(2, 300 * 1024), "old");
let tree = git(&source, &["rev-parse", "HEAD^{tree}"]);
let tag = ["-c", "user.name=t", "-c", "user.email=t@t", "tag", "-a"];
git(
&source,
&[&tag[..], &["tree-tag", "-m", "a tree", &tree]].concat(),
);
git(&source, &["rm", "-q", "-r", "old"]);
commit(&source, "a.txt", "a\n", "a");
commit(&source, "big.txt", &noise(1, 1024 * 1024), "big");
let refs = |source: &Path| -> Vec<(String, String)> {
let refs = git(
source,
&["for-each-ref", "--format=%(refname) %(objectname)"],
);
refs.lines()
.map(|line| line.split_once(' ').unwrap())
.map(|(name, oid)| (name.to_string(), oid.to_string()))
.collect()
};
let (_, mut prior) = export_refs_against(
root.path(),
&source,
"export-1",
None,
&store,
refs(&source),
);
let mut exports = Vec::new();
for name in ["b", "c"] {
commit(&source, &format!("{name}.txt"), &format!("{name}\n"), name);
let (export, next) = export_refs_against(
root.path(),
&source,
&format!("export-{name}"),
Some(&prior),
&store,
refs(&source),
);
assert_have_replies_whole(root.path(), &export)?;
exports.push(export);
prior = next;
}
#[cfg(target_os = "linux")]
let [prior, smart] = [0, 1].map(|at| exports[at].join("smart"));
#[cfg(not(target_os = "linux"))]
let smart = exports[1].join("smart");
assert!(smart.join("have-0-done").exists() && smart.join("moved-0-done").exists());
#[cfg(target_os = "linux")]
if reflinks_here(root.path()) {
let done = smart.join("have-0-done");
let aside = same_on_disk(&done, 4096, &prior.join("have-0-done"), 4096);
assert!(aside, "the aside is the prior's");
let round = smart.join("have-0");
let [round_len, done_len] =
[&round, &done].map(|path| std::fs::metadata(path).unwrap().len());
let shared = same_on_disk(&round, round_len - 1, &done, done_len - 1);
assert!(shared, "the round's tail is the done reply's");
}
Ok(())
}
#[test]
fn have_replies_carry_what_only_tags_reach() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
let tag = |args: &[&str]| {
git(
&source,
&[
&["-c", "user.name=t", "-c", "user.email=t@t", "tag", "-a"],
args,
]
.concat(),
);
};
commit(
&source,
"old/x.txt",
"only the tree tag reaches this\n",
"old",
);
let tree = git(&source, &["rev-parse", "HEAD^{tree}"]);
tag(&["tree-tag", "-m", "a tree", &tree]);
git(&source, &["rm", "-q", "-r", "old"]);
commit(&source, "a.txt", "a\n", "a");
tag(&["v1", "-m", "v1"]);
let refs = |source: &Path| -> Vec<(String, String)> {
let refs = git(
source,
&["for-each-ref", "--format=%(refname) %(objectname)"],
);
refs.lines()
.map(|line| line.split_once(' ').unwrap())
.map(|(name, oid)| (name.to_string(), oid.to_string()))
.collect()
};
commit(&source, "big.txt", &noise(1, 64 * 1024), "big");
let (first, prior) = export_refs_against(
root.path(),
&source,
"export-1",
None,
&store,
refs(&source),
);
let mut exports = vec![prior];
let mut made = |name: &str, source: &Path| {
let prior = exports.last().unwrap();
let (export, next) =
export_refs_against(root.path(), source, name, Some(prior), &store, refs(source));
exports.push(next);
export
};
let inode = |export: &Path| {
use std::os::unix::fs::MetadataExt as _;
std::fs::metadata(
super::kept_pack(&export.join("smart/tag-aside-objects"))
.unwrap()
.unwrap(),
)
.unwrap()
.ino()
};
commit(&source, "b.txt", "b\n", "b");
let second = made("export-2", &source);
assert_have_replies_whole(root.path(), &second)?;
assert_eq!(inode(&first), inode(&second), "the cold export's aside");
commit(&source, "c.txt", "c\n", "c");
let third = made("export-3", &source);
assert_have_replies_whole(root.path(), &third)?;
assert_have_replies_whole(root.path(), &third)?;
assert_eq!(inode(&second), inode(&third), "no tag moved");
tag(&["v0", "-m", "v0", "HEAD~3"]);
commit(&source, "d.txt", "d\n", "d");
tag(&["v2", "-m", "v2"]);
let fourth = made("export-4", &source);
assert_ne!(inode(&third), inode(&fourth), "a tag moved");
assert_have_replies_whole(root.path(), &fourth)?;
commit(&source, "d.txt", "d-changed\n", "d changed");
made("export-4b", &source);
commit(&source, "d.txt", "d\n", "d again");
let restored = made("export-4c", &source);
assert_have_replies_whole(root.path(), &restored)?;
// the reset drops the tagged commits off main, and the next base can't reach them.
git(&source, &["reset", "-q", "--hard", "HEAD~4"]);
commit(&source, "e.txt", "e\n", "e");
made("export-5", &source);
commit(&source, "f.txt", "f\n", "f");
let sixth = made("export-6", &source);
assert_have_replies_whole(root.path(), &sixth)?;
Ok(())
}
#[test]
fn fetch_replies_skip_a_base_nginx_cannot_route() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
for n in 0..100 {
let tip = orphan_commit(&source, &format!("side {n}"));
git(
&source,
&["update-ref", &format!("refs/heads/side-{n:03}"), &tip],
);
}
let (_, prior) = export_against(root.path(), &source, "export-1", None, &store);
commit(&source, "two.txt", "two\n", "two");
let (export, _) = export_against(root.path(), &source, "export-2", Some(&prior), &store);
assert!(super::fetch_replies(&export)?.bases.is_empty());
assert!(!export.join("smart/have-0-done").exists());
Ok(())
}
#[test]
fn fetch_replies_keep_the_newest_bases_within_the_count() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
commit(&source, "heavy.txt", &noise(0, 64 * 1024), "heavy");
let main = git(&source, &["rev-parse", "main"]);
let mut sides = Vec::new();
for n in 0..12 {
git(
&source,
&["checkout", "-q", "-b", &format!("side-{n}"), "main"],
);
commit(&source, &format!("side-{n}.txt"), "side\n", "side");
sides.push(git(&source, &["rev-parse", "HEAD"]));
}
git(&source, &["checkout", "-q", "main"]);
let bases: Vec<Vec<String>> = sides
.iter()
.map(|side| {
let mut base = vec![main.clone(), side.clone()];
base.sort();
base
})
.collect();
let prior = ExportPrior {
fingerprint: "none".into(),
refs: [("refs/heads/main".to_string(), main.clone())].into(),
bases: bases.clone(),
};
let (export, _) = export_against(root.path(), &source, "export", Some(&prior), &store);
let mut expected: Vec<Vec<String>> = sides.iter().map(|side| vec![side.clone()]).collect();
expected.insert(0, vec![main]);
expected.truncate(super::FETCH_BASES);
assert_eq!(super::fetch_replies(&export)?.bases, expected);
Ok(())
}
#[test]
fn fetch_replies_together_stay_within_the_full_pack() -> anyhow::Result<()> {
for tagged in [false, true] {
replies_stay_within_the_full_pack(tagged)?;
}
Ok(())
}
fn replies_stay_within_the_full_pack(tagged: bool) -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
if tagged {
git(&source, &["tag", "v0"]);
}
let mut history = Vec::new();
for n in 0..6 {
commit(
&source,
&format!("blob-{n}.txt"),
&noise(n, 16 * 1024),
"blob",
);
history.push(git(&source, &["rev-parse", "main"]));
}
let current = history.pop().unwrap();
let older: Vec<Vec<String>> = history.iter().rev().map(|oid| vec![oid.clone()]).collect();
let prior = ExportPrior {
fingerprint: "none".into(),
refs: [("refs/heads/main".to_string(), older[0][0].clone())].into(),
bases: older[1..].to_vec(),
};
let mut refs = heads(&source);
if tagged {
refs.push(("refs/tags/v0".into(), git(&source, &["rev-parse", "v0"])));
}
let (export, _) =
export_refs_against(root.path(), &source, "export", Some(&prior), &store, refs);
assert_eq!(git(&export, &["rev-parse", "main"]), current);
let replies = super::fetch_replies(&export)?;
let kept = replies.bases;
assert_eq!(replies.moved_wants.is_empty(), !tagged);
assert!(!kept.is_empty() && kept.len() < older.len(), "{kept:?}");
assert_eq!(kept, older[..kept.len()]);
let smart = export.join("smart");
let size = |name: String| std::fs::metadata(smart.join(name)).map_or(0, |m| m.len());
let made = |n: usize| [format!("have-{n}-done"), format!("moved-{n}-done")];
let together: u64 = (0..kept.len()).flat_map(made).map(size).sum();
assert!(together <= size("fetch-done".into()), "{together}");
let keys = std::fs::read_to_string(smart.join(super::REPLY_KEYS))?;
for dropped in made(kept.len()) {
assert!(!smart.join(&dropped).exists(), "{dropped}");
assert!(!keys.contains(&format!(" {dropped}\n")), "{keys}");
}
Ok(())
}
#[test]
fn delta_export_drops_the_prior_entrys_fetch_replies() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
commit(&source, "heavy.txt", &noise(0, 64 * 1024), "heavy");
let first = git(&source, &["rev-parse", "main"]);
commit(&source, "two.txt", "two\n", "two");
let (_, a) = export_against(root.path(), &source, "export-a", None, &store);
commit(&source, "three.txt", "three\n", "three");
let a = ExportPrior {
bases: vec![vec![first]],
..a
};
let (b_dir, b) = export_against(root.path(), &source, "export-b", Some(&a), &store);
assert!(b_dir.join("smart/have-1").exists());
commit(&source, "four.txt", "four\n", "four");
let b = ExportPrior {
bases: Vec::new(),
..b
};
let (c_dir, _) = export_against(root.path(), &source, "export-c", Some(&b), &store);
assert_eq!(super::fetch_replies(&c_dir)?.bases.len(), 1);
assert!(!c_dir.join("smart/have-1").exists());
assert!(!c_dir.join("smart/have-1-done").exists());
Ok(())
}
#[test]
fn export_fingerprint_follows_the_fetch_bases() {
let root = tempfile::tempdir().unwrap();
let source = init_test_source(root.path());
let snap = snapshot(&source, heads(&source));
let prior = ExportPrior {
fingerprint: "prior".into(),
refs: heads(&source).into_iter().collect(),
bases: Vec::new(),
};
let with_base = ExportPrior {
bases: vec![vec!["1".repeat(40)]],
..prior.clone()
};
let fingerprints = [None, Some(&prior), Some(&with_base)]
.map(|prior| super::export_fingerprint(&snap, "main", prior));
assert_ne!(fingerprints[0], fingerprints[1]);
assert_ne!(fingerprints[1], fingerprints[2]);
}
/// ofs deltas, since a ref delta's base is looked up by oid.
#[test]
fn have_n_packs_use_ofs_deltas() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
let (_, prior) = export_against(root.path(), &source, "export-1", None, &store);
let text = noise(3, 32 * 1024);
commit(&source, "big.txt", &text, "big");
commit(&source, "big.txt", &format!("{text}more\n"), "bigger");
let (export, _) = export_against(root.path(), &source, "export-2", Some(&prior), &store);
let types = pack_entry_types(&std::fs::read(export.join("smart/have-0-done"))?);
assert!(types.contains(&6), "{types:?}");
assert!(!types.contains(&7), "{types:?}");
Ok(())
}