use super::*;
#[test]
fn an_added_ref_seeds_from_the_prior_entry() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
let hidden = orphan_commit(&source, "hidden");
git(&source, &["update-ref", "refs/atproto/commit", &hidden]);
git(&source, &["repack", "-adq"]);
let (_, prior) = export_against(root.path(), &source, "export-1", None, &store);
let entry = store
.read_directory(super::EXPORT_CACHE_KIND, &prior.fingerprint)
.unwrap();
let shares = |dir: &Path| !pack_inodes(dir).is_disjoint(&pack_inodes(&entry));
git(&source, &["checkout", "-q", "-b", "feature"]);
commit(&source, "feature.txt", "feature\n", "feature");
let (added, next) = export_against(root.path(), &source, "export-2", Some(&prior), &store);
assert!(shares(&added), "an added ref took the delta path");
assert_eq!(
git(&added, &["rev-parse", "feature"]),
next.refs["refs/heads/feature"]
);
assert!(git(&added, &["fsck", "--unreachable", "--no-reflogs"]).is_empty());
git(&source, &["checkout", "-q", "main"]);
git(&source, &["branch", "-D", "feature"]);
let (deleted, next) = export_against(root.path(), &source, "export-3", Some(&next), &store);
assert!(
!shares(&deleted),
"a deletion of what only it reached takes the full path"
);
git(&source, &["branch", "merged"]);
git(&source, &["tag", "moving", "HEAD"]);
let (_, next) = export_against(root.path(), &source, "export-4", Some(&next), &store);
let entry = store
.read_directory(super::EXPORT_CACHE_KIND, &next.fingerprint)
.unwrap();
commit(&source, "after.txt", "after\n", "after");
git(&source, &["branch", "-D", "merged"]);
git(&source, &["tag", "-f", "moving", "HEAD"]);
let (grown, _) = export_against(root.path(), &source, "export-5", Some(&next), &store);
assert!(
!pack_inodes(&grown).is_disjoint(&pack_inodes(&entry)),
"a deletion that drops nothing takes the delta path"
);
assert!(git(&grown, &["fsck", "--unreachable", "--no-reflogs"]).is_empty());
Ok(())
}
#[test]
fn a_delta_export_grows_the_prior_full_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, "big.txt", &noise(3, 512 * 1024), "big");
let (_, first) = export_against(root.path(), &source, "export-1", None, &store);
commit(
&source,
"big.txt",
&(noise(3, 512 * 1024) + "more\n"),
"more",
);
commit(&source, "new.txt", &noise(4, 4096), "new");
let (grown, _) = export_against(root.path(), &source, "export-2", Some(&first), &store);
let tips = vec![git(&source, &["rev-parse", "main"])];
for name in ["upload-pack", "fetch-done"] {
let reply = std::fs::read(grown.join("smart").join(name))?;
assert_whole_pack(root.path(), &source, &reply, &tips, "sha1");
assert!(
!pack_entry_types(&reply).contains(&7),
"{name} has a ref-delta"
);
#[cfg(target_os = "linux")]
if reflinks_here(root.path()) {
assert!(
shares_extent(&grown.join("smart").join(name), 64 * 1024),
"{name} copied the prior reply"
);
}
}
let clone = clone_over_http(root.path(), &grown, "clone")?;
assert!(clone.join("new.txt").exists());
git(&clone, &["fsck", "--strict"]);
Ok(())
}
#[test]
fn a_full_reply_grows_from_a_copy_where_nothing_reflinks() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
let (first_dir, _) = export_against(root.path(), &source, "export-1", None, &store);
commit(&source, "two.txt", &noise(1, 4096), "two");
let main = git(&source, &["rev-parse", "main"]);
let segment = added_since(&first_dir, &source, &[main.as_str()]);
let prior = first_dir.join("smart/upload-pack");
let path = root.path().join("reply");
let unsupported: super::CloneFile = |_, _| Err(std::io::ErrorKind::Unsupported.into());
let format = knot_types::ObjectFormat::SHA1;
assert!(
super::grow_reply(
&prior,
segment.as_deref().map(super::Growth::Segment),
&path,
format,
Checksum::Hash,
unsupported
)
.is_err()
);
assert!(!path.exists());
let copy: super::CloneFile =
|source, target| std::io::copy(&mut &*source, &mut &*target).map(drop);
super::grow_reply(
&prior,
segment.as_deref().map(super::Growth::Segment),
&path,
format,
Checksum::Hash,
copy,
)?;
assert_whole_pack(
root.path(),
&source,
&std::fs::read(&path)?,
&[main],
"sha1",
);
Ok(())
}
#[test]
fn a_push_without_cobs_shares_v0s_grown_reply_into_v2() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
commit(&source, "big.txt", &noise(3, 512 * 1024), "big");
let (_, first) = export_against(root.path(), &source, "export-1", None, &store);
commit(&source, "new.txt", &noise(4, 4096), "new");
let (grown, _) = export_against(root.path(), &source, "export-2", Some(&first), &store);
let smart = grown.join("smart");
assert!(smart.join(super::SAME_PACK).exists());
let [v0, v2] = super::FULL_REPLIES.map(|name| std::fs::read(smart.join(name)));
let (v0, v2) = (v0?, v2?);
let [nak, packfile] = super::full_prefixes();
assert!(v0.starts_with(&nak) && v2.starts_with(&packfile));
assert!(v0[nak.len()..] == v2[packfile.len()..]);
let tips = [git(&source, &["rev-parse", "main"])];
assert_whole_pack(root.path(), &source, &v2, &tips, "sha1");
#[cfg(target_os = "linux")]
if reflinks_here(root.path()) {
let last = v2.len() as u64 - 1;
assert!(
shares_extent(&smart.join("fetch-done"), last),
"v2's tail is v0's"
);
}
Ok(())
}
#[test]
fn a_branch_at_an_exported_commit_keeps_the_prior_reply_bytes() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
commit(&source, "big.txt", &noise(5, 128 * 1024), "big");
let (first_dir, first) = export_against(root.path(), &source, "export-1", None, &store);
git(&source, &["branch", "same"]);
let (second_dir, _) = export_against(root.path(), &source, "export-2", Some(&first), &store);
for name in ["upload-pack", "fetch-done"] {
assert_eq!(
std::fs::read(second_dir.join("smart").join(name))?,
std::fs::read(first_dir.join("smart").join(name))?,
"{name}"
);
}
Ok(())
}
#[test]
fn a_restored_file_is_not_sent_twice() -> 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..250u64 {
std::fs::write(source.join(format!("f{n}.txt")), noise(n, 64))?;
}
commit(&source, "big.txt", &noise(5, 256 * 1024), "files");
let version = |name: &str| {
commit(
&source,
"a.txt",
&format!("{name}\n{}", noise(9, 2048)),
name,
);
git(&source, &["rev-parse", "main"])
};
let mut tips = vec![version("v1")];
let (_, mut prior) = export_against(root.path(), &source, "export-1", None, &store);
let mut priors = Vec::new();
for (n, name) in ["v2", "v3", "v2"].into_iter().enumerate() {
tips.push(version(name));
let (dir, next) = export_against(
root.path(),
&source,
&format!("export-{}", n + 2),
Some(&prior),
&store,
);
for reply in ["upload-pack", "fetch-done"] {
let bytes = std::fs::read(dir.join("smart").join(reply))?;
assert_whole_pack(root.path(), &source, &bytes, &tips[n + 1..n + 2], "sha1");
#[cfg(target_os = "linux")]
if reflinks_here(root.path()) {
assert!(
shares_extent(&dir.join("smart").join(reply), 64 * 1024),
"{reply} was cut whole"
);
}
}
priors.push(prior);
prior = next;
}
let third = priors.pop().unwrap();
let entry = store
.read_directory(super::EXPORT_CACHE_KIND, &third.fingerprint)
.unwrap();
let segment = added_since(&entry, &source, &[tips[3].as_str()]);
let copy: super::CloneFile =
|source, target| std::io::copy(&mut &*source, &mut &*target).map(drop);
let path = root.path().join("grown");
super::grow_reply(
&entry.join("smart/fetch-done"),
segment.as_deref().map(super::Growth::Segment),
&path,
knot_types::ObjectFormat::SHA1,
Checksum::Hash,
copy,
)?;
assert_whole_pack(
root.path(),
&source,
&std::fs::read(&path)?,
&tips[3..],
"sha1",
);
Ok(())
}
#[test]
fn a_merged_pull_keeps_the_v0_reply_whole() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
commit(&source, "big.txt", &noise(5, 256 * 1024), "big");
git(&source, &["checkout", "-q", "-b", "pull"]);
commit(&source, "pull.txt", &noise(6, 4096), "pull");
let pull = git(&source, &["rev-parse", "pull"]);
git(&source, &["checkout", "-q", "main"]);
git(&source, &["branch", "-D", "pull"]);
git(&source, &["update-ref", "refs/tngl/keep/1", &pull]);
let config = test_config(root.path());
let export = |name: &str, prior: Option<&ExportPrior>| {
let mut snap = snapshot(&source, heads(&source));
snap.cob_refs = vec![("refs/tngl/keep/1".into(), pull.clone())];
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);
let refs = exported_refs(&snap)
.map(|(name, raw)| (name.to_string(), raw.to_string()))
.collect();
let bases = super::fetch_replies(&dir).unwrap().bases;
(
dir,
ExportPrior {
fingerprint,
refs,
bases,
},
)
};
let (_, first) = export("export-1", None);
git(&source, &["merge", "-q", "--ff-only", &pull]);
let (merged, _) = export("export-2", Some(&first));
let v0 = std::fs::read(merged.join("smart/upload-pack"))?;
assert_whole_pack(root.path(), &source, &v0, &[pull.clone()], "sha1");
let v2 = std::fs::read(merged.join("smart/fetch-done"))?;
assert_whole_pack(root.path(), &source, &v2, &[pull], "sha1");
#[cfg(target_os = "linux")]
if reflinks_here(root.path()) {
assert!(
shares_extent(&merged.join("smart/upload-pack"), 64 * 1024),
"the v0 reply was cut whole"
);
}
let clone = clone_over_http(root.path(), &merged, "clone")?;
assert!(clone.join("pull.txt").exists());
git(&clone, &["fsck", "--strict"]);
Ok(())
}
#[test]
fn a_deleted_ref_grows_only_while_its_partition_still_reaches_it() -> 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 snap = || {
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),
});
}
}
snap
};
let export = |name: &str, prior: Option<&ExportPrior>| {
let snap = snap();
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);
let refs = exported_refs(&snap)
.map(|(name, raw)| (name.to_string(), raw.to_string()))
.collect();
let bases = super::fetch_replies(&dir).unwrap().bases;
ExportPrior {
fingerprint,
refs,
bases,
}
};
let grows = |prior: &ExportPrior| super::refs_fast_forwarded(prior, &snap(), &config).unwrap();
let ticket = |parent: Option<&str>, text: &str| {
let blob = git_stdin(&source, &["hash-object", "-w", "--stdin"], text);
let tree = git_stdin(
&source,
&["mktree"],
&format!("100644 blob {blob}\tticket\n"),
);
let mut args = vec!["commit-tree", &tree, "-m", "cob"];
if let Some(parent) = parent {
args.extend(["-p", parent]);
}
git(&source, &args)
};
const TICKET: &str = "refs/cobs/org.tangled.track.ticket/1";
const OTHER: &str = "refs/cobs/org.tangled.track.ticket/2";
git(&source, &["checkout", "-q", "-b", "pull"]);
commit(&source, "pull.txt", "pull\n", "pull");
let pull = git(&source, &["rev-parse", "pull"]);
git(&source, &["checkout", "-q", "main"]);
let first = ticket(None, "one");
git(&source, &["update-ref", TICKET, &first]);
git(&source, &["update-ref", OTHER, &first]);
git(&source, &["tag", "-a", "-m", "tag", "named", "main"]);
let tag = git(&source, &["rev-parse", "named"]);
git(&source, &["update-ref", "refs/tags/twice", &tag]);
git(&source, &["tag", "light", "main"]);
git(&source, &["tag", "tree", "main^{tree}"]);
git(
&source,
&["tag", "-a", "-m", "tree", "tree-named", "main^{tree}"],
);
git(&source, &["tag", "-a", "-m", "one", "one", "main"]);
git(&source, &["tag", "-a", "-m", "two", "two", "main"]);
for side in ["left", "right"] {
git(&source, &["checkout", "-q", "-b", side, "main"]);
commit(&source, &format!("{side}.txt"), side, side);
}
git(&source, &["merge", "-q", "--no-ff", "-m", "right", "left"]);
git(&source, &["checkout", "-q", "left"]);
git(
&source,
&["merge", "-q", "--no-ff", "-m", "left", "right~1"],
);
git(&source, &["checkout", "-q", "main"]);
git(
&source,
&["merge", "-q", "--no-ff", "-m", "both", "left", "right"],
);
crate::commit_graph::refresh(&source);
let prior = export("export-1", None);
git(
&source,
&["update-ref", TICKET, &ticket(Some(&first), "two")],
);
git(&source, &["update-ref", "-d", OTHER]);
git(&source, &["tag", "-d", "named"]);
git(
&source,
&["tag", "-f", "-a", "-m", "light", "light", "main"],
);
git(&source, &["tag", "-d", "tree"]);
commit(&source, "after.txt", "after\n", "after");
git(&source, &["branch", "-D", "left", "right"]);
assert!(grows(&prior), "nothing left either partition");
let prior = export("export-2", Some(&prior));
git(&source, &["tag", "-d", "one"]);
assert!(!grows(&prior), "the tag object leaves");
let prior = export("export-3", Some(&prior));
git(&source, &["branch", "-D", "pull"]);
git(&source, &["update-ref", "refs/tngl/keep/1", &pull]);
assert!(
!grows(&prior),
"the pull's objects leave the public partition"
);
export("export-4", Some(&prior));
Ok(())
}
#[test]
fn a_grown_reply_stays_in_whole_packets() -> anyhow::Result<()> {
for format in ["sha1", "sha256"] {
let format_id = knot_types::ObjectFormat::from_capability(format).unwrap();
let hash_len = format_id.kind().len_in_bytes();
let root = tempfile::tempdir()?;
let source = root.path().join("source");
std::fs::create_dir_all(&source)?;
let object_format = format!("--object-format={format}");
git(&source, &["init", "-q", "-b", "main", &object_format]);
git(&source, &["config", "core.compression", "0"]);
let pack_of = |revs: &str| {
let mut pack = git_command(&source);
pack.args([
"pack-objects",
"--revs",
"--stdout",
"--delta-base-offset",
"-q",
]);
pack.stdin(stdin_bytes(revs.as_bytes()).unwrap());
run_cmd(pack, "git pack-objects").unwrap().stdout
};
let in_packets = crate::reply_pack::tests::framed;
let grow = |prior: &[u8], segment: &[u8]| -> anyhow::Result<Vec<u8>> {
let prior_path = root.path().join("prior");
std::fs::write(&prior_path, prior)?;
let reply_path = root.path().join("reply");
std::fs::write(&reply_path, prior)?;
let mut reply = std::fs::OpenOptions::new().write(true).open(&reply_path)?;
let prior = std::fs::File::open(&prior_path)?;
crate::reply_pack::extend(&prior, &mut reply, segment, format_id, Checksum::Hash)?;
Ok(std::fs::read(&reply_path)?)
};
// the checksum in the last packet, across the last two, or starting one
for (n, past) in [30_000, hash_len / 2, 0].into_iter().enumerate() {
let mut size = 60_000i64;
let mut base = Vec::new();
for _ in 0..8 {
// main goes first, which makes this a root commit with only its objects
git(&source, &["update-ref", "-d", "refs/heads/main"]);
let blob = noise(n as u64, size as usize);
commit(&source, "a.txt", &blob, &format!("{n}"));
let tip = git(&source, &["rev-parse", "main"]);
base = pack_of(&format!("{tip}\n"));
let body = ((base.len() - hash_len) % 65515) as i64;
let target = ((65515 - past) % 65515) as i64;
if body == target {
break;
}
size += (target - body + 65515 + 32757) % 65515 - 32757;
}
let body = (base.len() - hash_len) % 65515;
assert_eq!(
body,
(65515 - past) % 65515,
"{format}: no pack of that length"
);
let old = git(&source, &["rev-parse", "main"]);
let mut tips = vec![old.clone()];
let mut reply = in_packets(&base, &[]);
for more in ["two", "three"] {
let prior = git(&source, &["rev-parse", "main"]);
commit(&source, &format!("{more}-{n}.txt"), more, more);
let tip = git(&source, &["rev-parse", "main"]);
reply = grow(&reply, &pack_of(&format!("{tip}\n^{prior}\n")))?;
tips = vec![tip];
assert_eq!(demux(&reply).0, vec![b"packfile\n".to_vec()]);
assert_whole_pack(root.path(), &source, &reply, &tips, format);
}
let segment = pack_of(&format!("{}\n^{old}\n", tips[0]));
let error = grow(&in_packets(&base, &[1, 3, 5, 7, 2]), &segment).unwrap_err();
assert!(
format!("{error:#}").contains("whole side-band packets"),
"{error:#}"
);
}
}
Ok(())
}
#[test]
fn a_prior_whose_partitions_name_a_missing_pack_is_packed_anew() -> anyhow::Result<()> {
use std::os::unix::fs::PermissionsExt as _;
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 entry = store
.read_directory(super::EXPORT_CACHE_KIND, &prior.fingerprint)
.unwrap();
let file = entry.join(super::partitions::PARTITIONS);
let text = std::fs::read_to_string(&file)?;
let last = text.trim_end().chars().last().unwrap();
let other = if last == '0' { '1' } else { '0' };
let broken = format!("{}{other}\n", &text.trim_end()[..text.trim_end().len() - 1]);
std::fs::set_permissions(&entry, std::fs::Permissions::from_mode(0o755))?;
std::fs::remove_file(&file)?;
std::fs::write(&file, &broken)?;
assert!(
store
.read_directory(super::EXPORT_CACHE_KIND, &prior.fingerprint)
.is_some()
);
commit(&source, "two.txt", "two\n", "two");
let (export, _) = export_against(root.path(), &source, "export-2", Some(&prior), &store);
let clone = clone_over_http(root.path(), &export, "clone")?;
assert!(clone.join("two.txt").exists());
Ok(())
}
#[test]
fn replies_framed_from_several_packs_hold_all_their_objects() -> anyhow::Result<()> {
for format in ["sha1", "sha256"] {
let object_format = knot_types::ObjectFormat::from_capability(format).unwrap();
let root = tempfile::tempdir()?;
let repo = root.path().join("repo");
git(
root.path(),
&["init", "-q", &format!("--object-format={format}"), "repo"],
);
let (mut packs, mut tips) = (Vec::new(), Vec::new());
for (n, size) in [70_000, 3, 140_000, 65_515].into_iter().enumerate() {
commit(&repo, &format!("f{n}"), &noise(n as u64, size), "c");
tips.push(git(&repo, &["rev-parse", "HEAD"]));
let mut revs = vec![tips[n].clone()];
revs.extend(tips.get(n.wrapping_sub(1)).map(|prior| format!("^{prior}")));
let base = root.path().join(format!("p{n}"));
let hash = git_stdin(
&repo,
&["pack-objects", "--revs", "-q", base.to_str().unwrap()],
&revs.join("\n"),
);
packs.push(PathBuf::from(format!("{}-{hash}.pack", base.display())));
}
let (mut v0, mut v2) = (tempfile::tempfile()?, tempfile::tempfile()?);
crate::reply_pack::frame(
object_format,
&packs[..2],
&packs[2..],
Some((&pkt_line(b"NAK\n"), &mut v0)),
(&pkt_line(b"packfile\n"), &mut v2),
None,
)?;
for (mut reply, tip) in [(v0, &tips[1]), (v2, &tips[3])] {
let mut bytes = Vec::new();
std::io::Seek::rewind(&mut reply)?;
std::io::Read::read_to_end(&mut reply, &mut bytes)?;
let mut objects = reply_objects(root.path(), &bytes, object_format);
let mut reached: Vec<String> = git(&repo, &["rev-list", "--objects", tip])
.lines()
.map(|line| listed_oid(line).to_string())
.collect();
objects.sort();
reached.sort();
assert_eq!(objects, reached, "{format}");
}
}
Ok(())
}
#[test]
fn a_reply_framed_like_an_earlier_one_shares_its_first_packs() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let (_, packs) = commit_packs(root.path(), &[300_000, 70_000, 90_000]);
let format = knot_types::ObjectFormat::SHA1;
let packfile = pkt_line(b"packfile\n");
let frame = |name: &str, packs: &[PathBuf], like: Option<&std::fs::File>| {
let path = root.path().join(name);
let mut file = read_write(&path)?;
let like = like.map(|like| (like, 1));
crate::reply_pack::frame(format, packs, &[], None, (&packfile, &mut file), like)?;
anyhow::Ok((path, file))
};
let (_, earlier) = frame("earlier", &packs[..2], None)?;
let (_, other) = frame("other", &[packs[1].clone(), packs[0].clone()], None)?;
let wanted = [packs[0].clone(), packs[2].clone()];
let (alone, _) = frame("alone", &wanted, None)?;
for (name, like) in [("like", &earlier), ("unlike", &other)] {
let (path, _) = frame(name, &wanted, Some(like))?;
assert!(std::fs::read(&path)? == std::fs::read(&alone)?, "{name}");
#[cfg(target_os = "linux")]
if reflinks_here(root.path()) {
let shares = name == "like";
assert!(
!shares_extent(&path, 0),
"{name}: the first block is its own"
);
assert_eq!(shares_extent(&path, 4096), shares, "{name}");
let last = std::fs::metadata(&path)?.len() - 1;
assert!(!shares_extent(&path, last), "{name}: the tail is its own");
}
}
Ok(())
}
#[test]
fn a_round_carries_the_done_reply_blocks_apart() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let (_, packs) = commit_packs(root.path(), &[200_000]);
let format = knot_types::ObjectFormat::SHA1;
let packfile = pkt_line(b"packfile\n");
let done_path = root.path().join("done");
let mut done = read_write(&done_path)?;
crate::reply_pack::frame(format, &packs, &[], None, (&packfile, &mut done), None)?;
let done_bytes = std::fs::read(&done_path)?;
for acks in [0, 1, 13, 4095, 4096, 4097, 9000] {
let acks = vec![b'a'; acks];
let path = root.path().join(format!("round-{}", acks.len()));
crate::reply_pack::behind(&acks, &done, &read_write(&path)?)?;
let round = std::fs::read(&path)?;
let shift = round.len() - done_bytes.len();
assert_eq!(shift % 4096, 0, "{} bytes of acks", acks.len());
let mut rest = &round[acks.len() + packfile.len()..];
while let Some(after) = rest.strip_prefix(b"0005\x02") {
rest = after;
}
assert!(round.starts_with(&acks) && round[acks.len()..].starts_with(&packfile));
assert!(
rest == &done_bytes[packfile.len()..],
"{} bytes of acks",
acks.len()
);
#[cfg(target_os = "linux")]
if reflinks_here(root.path()) {
assert!(shares_extent(&path, round.len() as u64 - 1));
}
}
Ok(())
}
#[test]
fn a_v2_reply_framed_beside_v0_shares_its_bytes() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let (repo, packs) = commit_packs(root.path(), &[300_000, 70_000, 200_000]);
let format = knot_types::ObjectFormat::SHA1;
let [padded, packfile] = super::full_prefixes();
let file = |name: &str| read_write(&root.path().join(name));
for (cobs, nak) in [(0, &padded), (1, &padded), (1, &pkt_line(b"NAK\n"))] {
let (public, cob) = (&packs[..2], &packs[2..2 + cobs]);
let [v0, v2, alone] =
["v0", "v2", "alone"].map(|name| format!("{name}-{cobs}-{}", nak.len()));
let (mut v0_file, mut v2_file) = (file(&v0)?, file(&v2)?);
crate::reply_pack::frame(
format,
public,
cob,
Some((nak, &mut v0_file)),
(&packfile, &mut v2_file),
None,
)?;
crate::reply_pack::frame(
format,
public,
cob,
None,
(&packfile, &mut file(&alone)?),
None,
)?;
let read = |name: &str| std::fs::read(root.path().join(name));
let (v0_bytes, v2_bytes) = (read(&v0)?, read(&v2)?);
assert!(v2_bytes == read(&alone)?, "{cobs} cob packs");
let mut reached = git(
&repo,
&["rev-list", "--objects", "--no-object-names", "HEAD~1"],
)
.lines()
.map(str::to_string)
.collect::<Vec<_>>();
let mut objects = reply_objects(root.path(), &v0_bytes, format);
reached.sort();
objects.sort();
assert_eq!(objects, reached, "v0 with {cobs} cob packs");
#[cfg(target_os = "linux")]
if reflinks_here(root.path()) {
let shared = nak.len() == packfile.len();
let v2_path = root.path().join(&v2);
assert!(!shares_extent(&v2_path, 0), "v2's first block is its own");
assert_eq!(shares_extent(&v2_path, 4096), shared, "{cobs} cob packs");
let last = v2_bytes.len() as u64 - 1;
assert!(!shares_extent(&v2_path, last), "{cobs} cob packs");
}
}
Ok(())
}
#[test]
fn streamed_replies_match_the_buffered_ones() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let source = init_test_source(root.path());
commit(&source, "two.txt", &noise(1, 4096), "two");
let snap = snapshot(&source, heads(&source));
let export = root.path().join("export");
export_repo_test(root.path(), &snap, &export);
let main = git(&source, &["rev-parse", "main"]);
let mut request =
pkt_line(format!("want {main} ofs-delta side-band-64k no-progress\n").as_bytes());
request.extend_from_slice(b"0000");
request.extend(pkt_line(b"done\n"));
let path = root.path().join("reply");
upload_pack_into(&export, &request, false, &path)?;
assert_eq!(
std::fs::read(&path)?,
upload_pack(&export, &[], Some(&request), false)?
);
Ok(())
}
#[test]
fn replies_to_an_unchanged_request_are_linked_from_the_prior_entry() -> anyhow::Result<()> {
use std::os::unix::fs::MetadataExt as _;
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
git(&source, &["branch", "feature"]);
let config = test_config(root.path());
let export = |name: &str, cob: &str, prior: Option<&ExportPrior>| {
let mut snap = snapshot(&source, heads(&source));
snap.cob_refs = vec![("refs/cobs/org.tangled.track.ticket/1".into(), cob.into())];
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);
let refs = exported_refs(&snap)
.map(|(name, raw)| (name.to_string(), raw.to_string()))
.collect();
let bases = super::fetch_replies(&dir).unwrap().bases;
(
dir,
ExportPrior {
fingerprint,
refs,
bases,
},
)
};
let (first, prior) = export("export-1", &orphan_commit(&source, "cob 1"), None);
commit(&source, "two.txt", "two\n", "two");
let (second, prior) = export("export-2", &orphan_commit(&source, "cob 1"), Some(&prior));
let cob = orphan_commit(&source, "cob 2");
let (third, prior) = export("export-3", &cob, Some(&prior));
let (fourth, _) = export("export-4", &cob, Some(&prior));
let inode = |dir: &Path, name: &str| {
std::fs::metadata(dir.join("smart").join(name))
.unwrap()
.ino()
};
for name in ["upload-pack", "shallow-head"] {
assert_ne!(inode(&first, name), inode(&second, name), "{name}");
assert_eq!(inode(&second, name), inode(&third, name), "{name}");
}
assert_ne!(inode(&second, "fetch-done"), inode(&third, "fetch-done"));
assert_eq!(
inode(&second, "shallow-head.map"),
inode(&third, "shallow-head.map"),
"a reply's map is linked with it"
);
for name in ["have-0", "have-0-done"] {
assert_eq!(inode(&third, name), inode(&fourth, name), "{name}");
}
let round = std::fs::read(third.join("smart/have-0"))?;
let done = std::fs::read(third.join("smart/have-0-done"))?;
// empty band-2 packets (`0005\x02`) sit between the acks and the done reply
let packfile = pkt_line(b"packfile\n");
let at = round
.windows(packfile.len())
.position(|w| w == packfile)
.unwrap();
let mut rest = &round[at + packfile.len()..];
while let Some(after) = rest.strip_prefix(b"0005\x02") {
rest = after;
}
assert!(round[..at].ends_with(b"0001") && done.starts_with(&packfile));
assert!(
rest == &done[packfile.len()..],
"the round carries the done pack"
);
Ok(())
}
/// git won't move an existing tag on fetch without `+`. the delta path
/// checks the move is a fast-forward instead.
#[test]
fn a_fast_forwarded_lightweight_tag_keeps_the_delta_path() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
let hidden = orphan_commit(&source, "hidden");
git(&source, &["update-ref", "refs/atproto/commit", &hidden]);
git(&source, &["repack", "-adq"]);
let config = test_config(root.path());
let export = |name: &str, prior: Option<&ExportPrior>| {
git(&source, &["tag", "-f", "v1", "main"]);
let mut refs = heads(&source);
refs.push(("refs/tags/v1".into(), git(&source, &["rev-parse", "v1"])));
let snap = snapshot(&source, refs.clone());
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();
let refs = refs.into_iter().collect();
ExportPrior {
fingerprint,
refs,
bases: Vec::new(),
}
};
let prior = export("export-1", None);
let entry = store
.read_directory(super::EXPORT_CACHE_KIND, &prior.fingerprint)
.unwrap();
commit(&source, "two.txt", "two\n", "two");
export("export-2", Some(&prior));
let moved = root.path().join("export-2");
assert!(
!pack_inodes(&moved).is_disjoint(&pack_inodes(&entry)),
"the tag move took the delta path"
);
assert_eq!(
git(&moved, &["rev-parse", "v1"]),
git(&source, &["rev-parse", "main"])
);
Ok(())
}