use super::*;
#[test]
fn smart_export_serves_a_real_clone() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let source = init_test_source(root.path());
commit(&source, "src/deep/file.txt", "deep\n", "two");
git(&source, &["branch", "feature"]);
let main_tip = git(&source, &["rev-parse", "main"]);
let feature_tip = git(&source, &["rev-parse", "feature"]);
let refs = vec![
("refs/heads/feature".to_string(), feature_tip.clone()),
("refs/heads/main".to_string(), main_tip.clone()),
];
let export_dir = root.path().join("export");
let snap = snapshot(&source, refs);
export_repo_test(root.path(), &snap, &export_dir);
let advertisement = std::fs::read(export_dir.join("smart/advertisement"))?;
let pack = std::fs::read(export_dir.join("smart/upload-pack"))?;
assert!(
advertisement.starts_with(b"001e# service=git-upload-pack\n0000"),
"advertisement must open with the service preamble"
);
assert!(bytes_contains(&advertisement, main_tip.as_bytes()));
assert!(
bytes_contains(&pack, b"PACK"),
"pack response must carry a packfile"
);
let clone_dir = clone_over_http(root.path(), &export_dir, "clone")?;
let clone_log = git(&clone_dir, &["log", "--oneline"]);
assert_eq!(clone_log.lines().count(), 2, "both commits fetched");
assert!(clone_dir.join("src/deep/file.txt").exists());
let fetched_feature = git(&clone_dir, &["rev-parse", "origin/feature"]);
assert_eq!(fetched_feature, feature_tip);
Ok(())
}
#[test]
fn advertised_refs_are_matched_by_oid_and_name_together() {
let (a, b, peeled) = ("a".repeat(40), "b".repeat(40), "c".repeat(40));
let mut advertisement = pkt_line(format!("{a} HEAD\0multi_ack ofs-delta\n").as_bytes());
advertisement.extend(pkt_line(format!("{a} refs/heads/main\n").as_bytes()));
advertisement.extend(pkt_line(format!("{b} refs/tags/v1\n").as_bytes()));
advertisement.extend(pkt_line(format!("{peeled} refs/tags/v1^{{}}\n").as_bytes()));
advertisement.extend_from_slice(b"0000");
let refs = |pairs: &[(&str, &str)]| -> Vec<crate::snapshot::PublicRef> {
pairs
.iter()
.map(|(name, raw)| crate::snapshot::PublicRef {
name: name.to_string(),
raw: raw.to_string(),
peeled: None,
})
.collect()
};
ensure_advertised(
&advertisement,
&refs(&[("refs/heads/main", &a), ("refs/tags/v1", &b)]),
)
.unwrap();
for wrong in [("refs/heads/main", b.as_str()), ("refs/heads/gone", &a)] {
let error = ensure_advertised(&advertisement, &refs(&[wrong])).unwrap_err();
assert!(error.to_string().contains(wrong.0), "{error}");
}
}
#[test]
fn fast_forward_delta_export_serves_a_real_clone() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let source = init_test_source(root.path());
let snap1 = snapshot(
&source,
vec![(
"refs/heads/main".to_string(),
git(&source, &["rev-parse", "main"]),
)],
);
let config = test_config(root.path());
let store = RepoStore::open(&root.path().join("repo-store"));
let fp1 = super::export_fingerprint(&snap1, "main", None);
let export1 = root.path().join("export-1");
super::materialize_export(&config, &snap1, &export1, &fp1, Some(&store), None)?;
commit(&source, "second.txt", "more\n", "two");
let snap2 = snapshot(
&source,
vec![(
"refs/heads/main".to_string(),
git(&source, &["rev-parse", "main"]),
)],
);
let fp2 = super::export_fingerprint(&snap2, "main", None);
assert_ne!(fp1, fp2);
let prior_refs = snap1
.public_refs
.iter()
.map(|r| (r.name.clone(), r.raw.clone()))
.collect();
let prior = super::ExportPrior {
fingerprint: fp1,
refs: prior_refs,
bases: Vec::new(),
};
let export2 = root.path().join("export-2");
super::materialize_export(&config, &snap2, &export2, &fp2, Some(&store), Some(&prior))?;
let new_tip = git(&source, &["rev-parse", "main"]);
let exported_head = git(&export2, &["rev-parse", "refs/heads/main"]);
assert_eq!(exported_head, new_tip, "delta export must move the ref");
assert!(
bytes_contains(
&std::fs::read(export2.join("smart/advertisement"))?,
new_tip.as_bytes()
),
"advertisement must carry the new tip"
);
let clone_dir = clone_over_http(root.path(), &export2, "clone")?;
assert!(
clone_dir.join("second.txt").exists(),
"delta clone has the new commit"
);
assert_eq!(git(&clone_dir, &["rev-parse", "HEAD"]), new_tip);
let first = git(&source, &["rev-parse", "HEAD~1"]);
git(&source, &["update-ref", "refs/heads/main", &first]);
let snap3 = snapshot(
&source,
vec![("refs/heads/main".to_string(), first.clone())],
);
let fp3 = super::export_fingerprint(&snap3, "main", None);
let export3 = root.path().join("export-3");
super::materialize_export(&config, &snap3, &export3, &fp3, Some(&store), Some(&prior))?;
assert_eq!(git(&export3, &["rev-parse", "refs/heads/main"]), first);
Ok(())
}
#[test]
fn cob_and_keep_refs_leave_only_through_the_v2_replies() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let source = init_test_source(root.path());
let main_tip = git(&source, &["rev-parse", "main"]);
let tree = git(&source, &["rev-parse", "main^{tree}"]);
let orphan = |message: &str| {
git(
&source,
&[
"-c",
"user.name=t",
"-c",
"user.email=t@t",
"commit-tree",
&tree,
"-m",
message,
],
)
};
let cob_refs: Vec<(String, String)> = [
("refs/cobs/org.tangled.track.ticket/1", "cob"),
("refs/tngl/keep/did/org.tangled.review.patch/1/2", "keep"),
]
.into_iter()
.map(|(name, message)| (name.to_string(), orphan(message)))
.collect();
let hidden = [
"refs/hidden/x",
"refs/atproto/commit",
"refs/cob-checkpoints/c/1",
]
.map(|name| (name, orphan(name)));
for (name, oid) in cob_refs
.iter()
.map(|(n, o)| (n.as_str(), o))
.chain(hidden.iter().map(|(n, o)| (*n, o)))
{
git(&source, &["update-ref", name, oid]);
}
let mut snap = snapshot(&source, vec![("refs/heads/main".to_string(), main_tip)]);
snap.cob_refs = cob_refs.clone();
let export_dir = root.path().join("export");
export_repo_test(root.path(), &snap, &export_dir);
let exported = git(&export_dir, &["for-each-ref", "--format=%(refname)"]);
assert_eq!(
exported.lines().collect::<Vec<_>>(),
[
"refs/cobs/org.tangled.track.ticket/1",
"refs/heads/main",
"refs/tngl/keep/did/org.tangled.review.patch/1/2"
]
);
for (_, oid) in &cob_refs {
git(&export_dir, &["cat-file", "-e", oid]);
}
for (name, oid) in &hidden {
let mut probe = git_command(&export_dir);
probe.args(["cat-file", "-e", oid]);
assert!(
run_cmd(probe, "probe").is_err(),
"{name} reached the export"
);
}
let smart = |name: &str| std::fs::read(export_dir.join("smart").join(name)).unwrap();
let listed = smart("ls-refs");
for (name, oid) in &cob_refs {
assert!(bytes_contains(&listed, name.as_bytes()));
assert!(bytes_contains(&listed, oid.as_bytes()));
}
for v0 in ["advertisement", "upload-pack", "ls-refs-head"] {
let bytes = smart(v0);
assert!(!bytes_contains(&bytes, b"refs/cobs"), "{v0} lists a cob");
assert!(
!bytes_contains(&bytes, b"refs/tngl"),
"{v0} lists a keep ref"
);
}
for (name, _) in &hidden {
for canned in ["advertisement", "ls-refs", "adv-v2", "ls-refs-head"] {
assert!(
!bytes_contains(&smart(canned), name.as_bytes()),
"{canned} lists {name}"
);
}
}
let clone = clone_over_http(root.path(), &export_dir, "clone")?;
for (_, oid) in &cob_refs {
let mut probe = git_command(&clone);
probe.args(["cat-file", "-e", oid]);
assert!(
run_cmd(probe, "probe").is_err(),
"the v0 pack carried {oid}"
);
}
Ok(())
}
/// never rewrite a seeded staging file in place, it shares its inode with the cache entry.
#[test]
fn delta_export_leaves_the_prior_entry_bytes_untouched() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let source = init_test_source(root.path());
let snap1 = snapshot(
&source,
vec![(
"refs/heads/main".to_string(),
git(&source, &["rev-parse", "main"]),
)],
);
let config = test_config(root.path());
let store = RepoStore::open(&root.path().join("repo-store"));
let fp1 = super::export_fingerprint(&snap1, "main", None);
let export1 = root.path().join("export-1");
super::materialize_export(&config, &snap1, &export1, &fp1, Some(&store), None)?;
let entry1 = store
.read_directory(super::EXPORT_CACHE_KIND, &fp1)
.expect("full export publishes its cache entry");
let adv_before = std::fs::read(entry1.join("smart/advertisement"))?;
let pack_before = std::fs::read(entry1.join("smart/upload-pack"))?;
commit(&source, "second.txt", "more\n", "two");
let snap2 = snapshot(
&source,
vec![(
"refs/heads/main".to_string(),
git(&source, &["rev-parse", "main"]),
)],
);
let fp2 = super::export_fingerprint(&snap2, "main", None);
let prior_refs = snap1
.public_refs
.iter()
.map(|r| (r.name.clone(), r.raw.clone()))
.collect();
let prior = super::ExportPrior {
fingerprint: fp1,
refs: prior_refs,
bases: Vec::new(),
};
let export2 = root.path().join("export-2");
super::materialize_export(&config, &snap2, &export2, &fp2, Some(&store), Some(&prior))?;
assert_eq!(
std::fs::read(entry1.join("smart/advertisement"))?,
adv_before,
"prior entry advertisement must be untouched"
);
assert_eq!(
std::fs::read(entry1.join("smart/upload-pack"))?,
pack_before,
"prior entry upload-pack must be untouched"
);
assert_ne!(
std::fs::read(export2.join("smart/advertisement"))?,
adv_before,
"the delta export must carry its own new-tip artifacts"
);
Ok(())
}
/// publish replaces the marker, it doesn't write through a hardlink into the seed entry.
#[test]
fn delta_exports_publish_entries_that_seed_the_next_delta() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let source = init_test_source(root.path());
let config = test_config(root.path());
let store = RepoStore::open(&root.path().join("repo-store"));
let mut prior: Option<super::ExportPrior> = None;
let mut fingerprints: Vec<String> = Vec::new();
for n in 0..3 {
commit(&source, &format!("{n}.txt"), "more\n", "more");
let snap = snapshot(
&source,
vec![(
"refs/heads/main".to_string(),
git(&source, &["rev-parse", "main"]),
)],
);
let fingerprint = super::export_fingerprint(&snap, "main", prior.as_ref());
let export = root.path().join(format!("export-{n}"));
super::materialize_export(
&config,
&snap,
&export,
&fingerprint,
Some(&store),
prior.as_ref(),
)?;
if let Some(previous) = fingerprints.last() {
let entry = store
.read_directory(super::EXPORT_CACHE_KIND, previous)
.expect("the entry this export was seeded from still validates");
assert!(
!pack_inodes(&export).is_disjoint(&pack_inodes(&entry)),
"export-{n} was seeded from its prior"
);
}
assert!(
store
.read_directory(super::EXPORT_CACHE_KIND, &fingerprint)
.is_some(),
"export-{n} published a valid entry"
);
prior = Some(super::ExportPrior {
fingerprint: fingerprint.clone(),
refs: snap
.public_refs
.iter()
.map(|r| (r.name.clone(), r.raw.clone()))
.collect(),
bases: Vec::new(),
});
fingerprints.push(fingerprint);
}
Ok(())
}
#[test]
fn export_writes_packed_refs_without_loose_refs() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let store = RepoStore::open(&root.path().join("repo-store"));
let source = init_test_source(root.path());
git(
&source,
&[
"-c",
"user.name=t",
"-c",
"user.email=t@t",
"tag",
"-a",
"-m",
"v1",
"v1",
],
);
let main = git(&source, &["rev-parse", "main"]);
let tag_raw = git(&source, &["rev-parse", "refs/tags/v1"]);
let mut snap = snapshot(&source, vec![("refs/heads/main".into(), main.clone())]);
snap.public_refs.push(crate::snapshot::PublicRef {
name: "refs/tags/v1".into(),
raw: tag_raw.clone(),
peeled: Some(main.clone()),
});
let config = test_config(root.path());
let fp1 = super::export_fingerprint(&snap, "main", None);
let export1 = root.path().join("export-1");
super::materialize_export(&config, &snap, &export1, &fp1, Some(&store), None)?;
let packed = std::fs::read_to_string(export1.join("packed-refs"))?;
assert!(packed.contains(&format!("{main} refs/heads/main\n")));
assert!(packed.contains(&format!("{tag_raw} refs/tags/v1\n^{main}\n")));
assert_eq!(std::fs::read_dir(export1.join("refs/heads"))?.count(), 0);
assert_eq!(std::fs::read_dir(export1.join("refs/tags"))?.count(), 0);
git(&export1, &["fsck", "--strict"]);
assert_eq!(git(&export1, &["rev-parse", "refs/tags/v1^{}"]), main);
Ok(())
}