use super::*;
use crate::git_export::{git_command, run_cmd, stdin_bytes};
fn git(repo: &Path, args: &[&str], stdin: &str) -> String {
let mut command = git_command(repo);
command
.args(["-c", "user.name=t", "-c", "user.email=t@t"])
.args(args);
command.stdin(stdin_bytes(stdin.as_bytes()).unwrap());
let output = run_cmd(command, &format!("git {args:?}")).unwrap();
String::from_utf8(output.stdout).unwrap().trim().to_string()
}
impl Holds for HashSet<ObjectId> {
fn holds(&self, oid: &gix::hash::oid) -> bool {
self.contains(oid)
}
}
#[test]
fn the_walk_skips_gitlinks_and_stops_at_known_objects() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
git(repo, &["init", "-q", "--bare"], "");
let blob = git(repo, &["hash-object", "-w", "--stdin"], "old\n");
let elsewhere = "1".repeat(40);
let tree = git(
repo,
&["mktree"],
&format!("100644 blob {blob}\ta.txt\n160000 commit {elsewhere}\tsub\n"),
);
let commit = git(repo, &["commit-tree", &tree, "-m", "one"], "");
let opened = knot_git::Repo::open(repo).unwrap();
let known = HashSet::from([ObjectId::from_hex(blob.as_bytes()).unwrap()]);
let tip = ObjectId::from_hex(commit.as_bytes()).unwrap();
let segment = unknown_reached(opened.git(), &[tip], &known).unwrap();
let mut found: Vec<String> = segment.oids().map(|oid| oid.to_string()).collect();
found.sort();
let mut expected = vec![commit, tree];
expected.sort();
assert_eq!(found, expected);
}
#[test]
fn partitions_name_only_packs_that_are_there() {
let dir = tempfile::tempdir().unwrap();
let export = dir.path();
let packs = export.join("objects/pack");
std::fs::create_dir_all(&packs).unwrap();
let stem = format!("pack-{}", "a".repeat(40));
let read = |text: &str| {
std::fs::write(export.join(PARTITIONS), text).unwrap();
Partitions::read(export)
};
std::fs::write(packs.join(format!("{stem}.idx")), "").unwrap();
assert!(read(&format!("public {stem}\n")).is_err(), "no .pack");
std::fs::write(packs.join(format!("{stem}.pack")), "").unwrap();
assert!(read(&format!("public {stem}\n")).unwrap().is_some());
for bad in ["../pack-x", "pack-xyz", "public"] {
for ext in ["idx", "pack"] {
std::fs::write(packs.join(format!("{bad}.{ext}")), "").unwrap();
}
assert!(read(&format!("public {bad}\n")).is_err(), "{bad}");
}
assert!(read(&format!("cob {stem}\n")).is_err(), "no public pack");
}
fn commit(repo: &Path, parent: Option<&str>, files: &[(&str, &str)]) -> String {
let entries: String = files
.iter()
.map(|(name, text)| {
let blob = git(repo, &["hash-object", "-w", "--stdin"], text);
format!("100644 blob {blob}\t{name}\n")
})
.collect();
let tree = git(repo, &["mktree"], &entries);
let mut args = vec!["commit-tree", tree.as_str(), "-m", files[0].1];
if let Some(parent) = parent {
args.extend(["-p", parent]);
}
git(repo, &args, "")
}
fn bitmapped(repo: &Path) -> (String, String) {
git(repo, &["init", "-q", "--bare"], "");
let first = commit(repo, None, &[("a.txt", "one\n")]);
let second = commit(repo, Some(&first), &[("a.txt", "two\n")]);
let hidden = commit(repo, Some(&second), &[("secret.txt", "hidden\n")]);
git(repo, &["update-ref", "refs/heads/main", &second], "");
git(repo, &["update-ref", "refs/hidden/x", &hidden], "");
git(repo, &["repack", "-adq", "--no-write-bitmap-index"], "");
git(repo, &["multi-pack-index", "write"], "");
let opened = knot_git::Repo::open(repo).unwrap();
assert!(crate::bitmap::write_midx_bitmap(&opened, None).unwrap());
let main = commit(repo, Some(&second), &[("a.txt", "three\n")]);
git(repo, &["update-ref", "refs/heads/main", &main], "");
(main, hidden)
}
#[test]
fn a_mark_through_knot_gits_bitmap_is_what_git_walks() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
let (main, hidden) = bitmapped(repo);
let tip = ObjectId::from_hex(main.as_bytes()).unwrap();
let expected = reached(repo, &main);
assert!(!expected.contains(&hidden));
let through = bitmap_reach::<20>(repo, &[tip])
.unwrap()
.expect("knot_git's reach");
let through: HashSet<String> = (through.iter())
.map(|id| ObjectId::from_bytes_or_panic(id).to_string())
.collect();
assert_eq!(through, expected);
let marked = Marked::of(repo, &[tip], knot_types::ObjectFormat::SHA1, None).unwrap();
let count: u32 = (marked.packs.iter())
.map(|pack| pack.bits.iter().map(|word| word.count_ones()).sum::<u32>())
.sum();
let elsewhere: HashSet<String> = marked.unpacked.oids().map(|oid| oid.to_string()).collect();
assert_eq!(count as usize + elsewhere.len(), expected.len());
assert!(elsewhere.is_subset(&expected));
}
#[test]
fn a_replace_ref_keeps_the_mark_off_knot_gits_reach() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
let (main, hidden) = bitmapped(repo);
git(
repo,
&["update-ref", &format!("refs/replace/{main}"), &hidden],
"",
);
let tip = ObjectId::from_hex(main.as_bytes()).unwrap();
assert!(bitmap_reach::<20>(repo, &[tip]).unwrap().is_none());
let reached: HashSet<String> = (reach::<20>(repo, &[tip], 0).unwrap().iter())
.map(|id| ObjectId::from_bytes_or_panic(id).to_string())
.collect();
assert!(reached.contains(&main));
assert!(
!reached.contains(&hidden),
"the replacement's commit was marked"
);
}
#[test]
fn a_mark_takes_the_bitmap_and_stays_exact() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
git(repo, &["init", "-q", "--bare"], "");
let first = commit(repo, None, &[("a.txt", "one\n")]);
let main = commit(repo, Some(&first), &[("a.txt", "two\n")]);
let hidden = commit(repo, Some(&main), &[("secret.txt", "hidden\n")]);
let blob = git(repo, &["hash-object", "-w", "--stdin"], "a tree's\n");
let tree = git(repo, &["mktree"], &format!("100644 blob {blob}\tt.txt\n"));
git(repo, &["update-ref", "refs/heads/main", &main], "");
git(repo, &["update-ref", "refs/hidden/x", &hidden], "");
git(repo, &["update-ref", "refs/tags/t", &tree], "");
git(repo, &["repack", "-adq", "--window=0"], "");
git(repo, &["multi-pack-index", "write", "--bitmap"], "");
let expected: HashSet<String> = &reached(repo, &main) | &reached(repo, &tree);
let pack = std::fs::read_dir(repo.join("objects/pack"))
.unwrap()
.map(|entry| entry.unwrap().path())
.find(|path| path.extension().is_some_and(|ext| ext == "pack"))
.unwrap();
let root = git(repo, &["rev-parse", &format!("{main}^{{tree}}")], "");
let mut show = git_command(repo);
show.arg("show-index")
.stdin(stdin_bytes(&std::fs::read(pack.with_extension("idx")).unwrap()).unwrap());
let listed = run_cmd(show, "git show-index").unwrap().stdout;
let offset: u64 = String::from_utf8(listed)
.unwrap()
.lines()
.find(|line| line.contains(&root))
.and_then(|line| line.split(' ').next()?.parse().ok())
.unwrap();
{
use std::os::unix::fs::{FileExt as _, PermissionsExt as _};
std::fs::set_permissions(&pack, std::fs::Permissions::from_mode(0o644)).unwrap();
let file = std::fs::OpenOptions::new().write(true).open(&pack).unwrap();
file.write_all_at(&[0xff; 8], offset + 4).unwrap();
}
let mut walked = git_command(repo);
walked.args(["rev-list", "--objects", &main]);
assert!(run_cmd(walked, "git rev-list").is_err(), "the tree reads");
let tips = [&main, &tree].map(|tip| ObjectId::from_hex(tip.as_bytes()).unwrap());
let marked = Marked::of(repo, &tips, knot_types::ObjectFormat::SHA1, None).unwrap();
let mut found = HashSet::new();
for pack in &marked.packs {
let index =
gix::odb::pack::index::File::at(pack.path.with_extension("idx"), gix::hash::Kind::Sha1)
.unwrap();
for (at, entry) in index.iter().enumerate() {
if pack.has(at as u32) {
found.insert(entry.oid.to_string());
}
}
}
assert!(marked.unpacked.is_empty());
assert_eq!(found, expected);
}
#[test]
fn a_mark_takes_each_object_from_the_first_pack_holding_it() {
for format in [
knot_types::ObjectFormat::SHA1,
knot_types::ObjectFormat::SHA256,
] {
mark_of(format);
}
}
fn mark_of(format: knot_types::ObjectFormat) {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
let object_format = format!("--object-format={}", format.capability());
git(repo, &["init", "-q", "--bare", &object_format], "");
let first = commit(repo, None, &[("a.txt", "one\n")]);
let second = commit(repo, Some(&first), &[("b.txt", "two\n")]);
for revs in [first.clone(), second.clone(), format!("{second}\n^{first}")] {
git(
repo,
&["pack-objects", "-q", "--revs", "objects/pack/pack"],
&format!("{revs}\n"),
);
}
let main = commit(repo, Some(&second), &[("c.txt", "three\n")]);
let hidden = commit(repo, Some(&main), &[("secret.txt", "hidden\n")]);
git(repo, &["prune-packed"], "");
git(repo, &["update-ref", "refs/heads/main", &main], "");
git(repo, &["update-ref", "refs/hidden/x", &hidden], "");
let tip = ObjectId::from_hex(main.as_bytes()).unwrap();
let marked = Marked::of(repo, &[tip], format, None).unwrap();
assert_eq!(marked.packs.len(), 3);
let mut found: Vec<String> = Vec::new();
let mut earlier = HashSet::new();
let expected = reached(repo, &main);
for pack in &marked.packs {
let index = gix::odb::pack::index::File::at(pack.path.with_extension("idx"), format.kind())
.unwrap();
let held: HashSet<String> = index.iter().map(|entry| entry.oid.to_string()).collect();
let marked_here: HashSet<String> = index
.iter()
.enumerate()
.filter(|&(at, _)| pack.has(at as u32))
.map(|(_, entry)| entry.oid.to_string())
.collect();
let first_here: HashSet<String> = &(&held & &expected) - &earlier;
assert_eq!(marked_here, first_here, "{}", pack.path.display());
found.extend(marked_here);
earlier.extend(held);
}
let elsewhere: HashSet<String> = marked.unpacked.oids().map(|oid| oid.to_string()).collect();
assert!(!elsewhere.is_empty());
assert_eq!(elsewhere, &expected - &earlier);
found.extend(elsewhere);
assert_eq!(found.len(), expected.len(), "an object marked twice");
assert_eq!(found.into_iter().collect::<HashSet<_>>(), expected);
}
#[test]
fn a_mark_finds_every_object_of_a_full_pack() {
let dir = tempfile::tempdir().unwrap();
let (_, main) = crate::bench_fixture::repo(dir.path(), 300, 8, 0);
let marked = Marked::of(dir.path(), &[main], knot_types::ObjectFormat::SHA1, None).unwrap();
let reached = reached(dir.path(), &main.to_string()).len();
let marked_count: u32 = (marked.packs.iter())
.map(|pack| pack.bits.iter().map(|word| word.count_ones()).sum::<u32>())
.sum();
assert_eq!(marked_count as usize, reached);
assert!(marked.unpacked.is_empty());
}
fn named_walk(repo: &gix::Repository, tips: &[ObjectId], known: &dyn Holds) -> Vec<u8> {
let kind = repo.object_hash();
let mut stack: Vec<(ObjectId, Option<Vec<u8>>)> = tips.iter().map(|tip| (*tip, None)).collect();
let mut seen = HashSet::new();
let mut list = Vec::new();
while let Some((oid, name)) = stack.pop() {
if known.holds(&oid) || !seen.insert(oid) {
continue;
}
let object = repo.find_object(oid).unwrap();
match object.kind {
gix::object::Kind::Blob => {}
gix::object::Kind::Tree => {
for entry in gix::objs::TreeRefIter::from_bytes(&object.data, kind) {
let entry = entry.unwrap();
if !entry.mode.is_commit() {
let path = match &name {
Some(dir) => [dir.as_slice(), b"/", entry.filename].concat(),
None => entry.filename.to_vec(),
};
stack.push((entry.oid.to_owned(), Some(path)));
}
}
}
gix::object::Kind::Commit => {
let mut commit = gix::objs::CommitRefIter::from_bytes(&object.data, kind);
let tree = commit.tree_id().unwrap();
stack.extend(commit.parent_ids().map(|parent| (parent, None)));
stack.push((tree, None));
}
gix::object::Kind::Tag => {
let target = gix::objs::TagRefIter::from_bytes(&object.data, kind)
.target_id()
.unwrap();
stack.push((target, None));
}
}
list.extend_from_slice(oid.to_string().as_bytes());
if let Some(name) = name {
list.push(b' ');
list.extend_from_slice(&name);
}
list.push(b'\n');
}
list
}
#[test]
fn the_walk_lists_each_object_with_the_path_it_was_found_at() {
let dir = tempfile::tempdir().unwrap();
let nested = dir.path().join("nested");
let (base, main) = crate::bench_fixture::repo(&nested, 300, 8, 4);
let opened = knot_git::Repo::open(&nested).unwrap();
let below: HashSet<ObjectId> = unknown_reached(opened.git(), &[base], &HashSet::new())
.unwrap()
.oids()
.collect();
for (tips, known) in [(vec![main], HashSet::new()), (vec![main, base], below)] {
let walked = unknown_reached(opened.git(), &tips, &known).unwrap();
let expected = named_walk(opened.git(), &tips, &known);
assert_eq!(walked.object_list(), expected.as_slice());
assert_eq!(walked.len(), expected.split(|&b| b == b'\n').count() - 1);
}
let repo = dir.path().join("shared");
std::fs::create_dir_all(&repo).unwrap();
git(&repo, &["init", "-q", "--bare"], "");
let blob = git(&repo, &["hash-object", "-w", "--stdin"], "twice\n");
let sub = git(
&repo,
&["mktree"],
&format!("100644 blob {blob}\tinner.txt\n"),
);
let gitlink = "1".repeat(40);
let tree = git(
&repo,
&["mktree"],
&format!(
"100644 blob {blob}\ta.txt\n160000 commit {gitlink}\tmodule\n040000 tree {sub}\tsub\n"
),
);
let commit = git(&repo, &["commit-tree", &tree, "-m", "c"], "");
git(&repo, &["update-ref", "refs/heads/main", &commit], "");
git(&repo, &["tag", "-a", "-m", "t", "inner", &commit], "");
git(&repo, &["tag", "-a", "-m", "t", "outer", "inner"], "");
let outer = ObjectId::from_hex(git(&repo, &["rev-parse", "outer"], "").as_bytes()).unwrap();
let opened = knot_git::Repo::open(&repo).unwrap();
let walked = unknown_reached(opened.git(), &[outer], &HashSet::new()).unwrap();
let expected = named_walk(opened.git(), &[outer], &HashSet::new());
assert_eq!(walked.object_list(), expected.as_slice());
assert_eq!(walked.len(), 6, "{}", String::from_utf8_lossy(&expected));
}
#[test]
fn the_listed_ids_are_read_whatever_the_last_line_ends_with() {
fn read<const N: usize>(kind: gix::hash::Kind) {
let (one, two) = (ObjectId::empty_tree(kind), ObjectId::empty_blob(kind));
for listed in [format!("{one}\n{two}\n"), format!("{one}\n{two}")] {
let read = read_ids::<N>(listed.as_bytes(), 2).unwrap();
let read: Vec<&[u8]> = read.iter().map(|id| id.as_slice()).collect();
assert_eq!(read, [one.as_bytes(), two.as_bytes()]);
}
}
read::<20>(gix::hash::Kind::Sha1);
read::<32>(gix::hash::Kind::Sha256);
assert!(read_ids::<20>(&b""[..], 0).unwrap().is_empty());
assert!(read_ids::<20>(&b"not an id\n"[..], 1).is_err());
let sha256 = format!("{}\n", ObjectId::empty_tree(gix::hash::Kind::Sha256));
assert!(
read_ids::<20>(sha256.as_bytes(), 1).is_err(),
"a sha256 id read as sha1"
);
}
/// reads the objects as stored, not through any replace ref.
fn reached(repo: &Path, tip: &str) -> HashSet<String> {
let listed = git(
repo,
&["--no-replace-objects", "rev-list", "--objects", tip],
"",
);
listed
.lines()
.map(|line| line.split(' ').next().unwrap().to_string())
.collect()
}
fn objects_of(export: &Path, packs: &[PartitionPack]) -> Vec<String> {
packs
.iter()
.flat_map(|pack| {
let index =
gix::odb::pack::index::File::at(pack.path(export, "idx"), gix::hash::Kind::Sha1)
.unwrap();
index
.iter()
.map(|entry| entry.oid.to_string())
.collect::<Vec<_>>()
})
.collect()
}
fn git_rev(path: &Path) -> Vec<u8> {
let dir = tempfile::tempdir().unwrap();
let copy = dir.path().join("p.pack");
std::fs::copy(path, ©).unwrap();
git(
dir.path(),
&["index-pack", "--rev-index", copy.to_str().unwrap()],
"",
);
std::fs::read(copy.with_extension("rev")).unwrap()
}
#[test]
fn a_cold_export_links_and_copies_the_source_packs() {
for revs in [true, false] {
cold_export(revs);
}
}
fn cold_export(revs: bool) {
use std::os::unix::fs::MetadataExt as _;
let dir = tempfile::tempdir().unwrap();
let repo = dir.path().join("source");
git(
dir.path(),
&["init", "-q", "--bare", repo.to_str().unwrap()],
"",
);
let mut main = commit(&repo, None, &[("a.txt", "one")]);
let first = main.clone();
for text in ["two", "three"] {
main = commit(&repo, Some(&main), &[("a.txt", text)]);
}
git(&repo, &["update-ref", "refs/heads/main", &main], "");
git(&repo, &["repack", "-dq"], "");
let hidden = commit(&repo, Some(&main), &[("secret.txt", "hidden")]);
git(&repo, &["update-ref", "refs/hidden/x", &hidden], "");
let cob = commit(&repo, None, &[("cob.txt", "cob"), ("a.txt", "three")]);
git(&repo, &["update-ref", "refs/cobs/x", &cob], "");
main = commit(&repo, Some(&main), &[("a.txt", "four")]);
git(&repo, &["update-ref", "refs/heads/main", &main], "");
git(&repo, &["repack", "-dq"], "");
let again = git(&repo, &["rev-parse", &format!("{first}^{{tree}}")], "");
git(
&repo,
&["pack-objects", "-q", "objects/pack/pack"],
&format!("{again}\n"),
);
main = commit(&repo, Some(&main), &[("a.txt", "five")]);
git(&repo, &["update-ref", "refs/heads/main", &main], "");
let source_packs: Vec<PathBuf> = std::fs::read_dir(repo.join("objects/pack"))
.unwrap()
.map(|entry| entry.unwrap().path())
.filter(|path| path.extension().is_some_and(|ext| ext == "pack"))
.collect();
for pack in &source_packs {
assert!(pack.with_extension("rev").is_file(), "git wrote no .rev");
if !revs {
std::fs::remove_file(pack.with_extension("rev")).unwrap();
}
std::fs::write(pack.with_extension("keep"), "").unwrap();
std::fs::write(pack.with_extension("promisor"), "").unwrap();
}
let unindexed = repo.join(format!("objects/pack/pack-{}.pack", "0".repeat(40)));
std::fs::write(&unindexed, "PACK").unwrap();
let export = dir.path().join("export");
std::fs::create_dir_all(export.join("objects/pack")).unwrap();
let oid = |hex: &str| ObjectId::from_hex(hex.as_bytes()).unwrap();
let fresh = Partitions::pack(
&repo,
&export,
&[oid(&main)],
&[oid(&cob)],
knot_types::ObjectFormat::SHA1,
None,
)
.unwrap();
let public = reached(&repo, &main);
let cob_only: HashSet<String> = reached(&repo, &cob).difference(&public).cloned().collect();
let in_public = objects_of(&export, fresh.public());
let in_cob = objects_of(&export, fresh.cob());
assert_eq!(
in_public.len(),
public.len(),
"the public packs hold an object twice or miss one"
);
assert_eq!(in_public.iter().cloned().collect::<HashSet<_>>(), public);
assert_eq!(in_cob.iter().cloned().collect::<HashSet<_>>(), cob_only);
assert_eq!(in_cob.len(), cob_only.len());
for secret in reached(&repo, &hidden).difference(&public) {
assert!(
!in_public.contains(secret) && !in_cob.contains(secret),
"{secret}"
);
}
let inodes: HashSet<u64> = source_packs
.iter()
.map(|path| std::fs::metadata(path).unwrap().ino())
.collect();
let linked = (fresh.public().iter())
.filter(|pack| {
inodes.contains(&std::fs::metadata(pack.path(&export, "pack")).unwrap().ino())
})
.count();
assert!(
linked > 0 && linked < fresh.public().len(),
"{linked} of {}",
fresh.public().len()
);
for entry in std::fs::read_dir(export.join("objects/pack")).unwrap() {
let path = entry.unwrap().path();
let ext = path.extension().unwrap().to_str().unwrap();
assert!(["pack", "idx", "rev"].contains(&ext), "{}", path.display());
assert!(!path.ends_with(unindexed.file_name().unwrap()));
}
for pack in fresh.public() {
let ino = std::fs::metadata(pack.path(&export, "pack")).unwrap().ino();
if inodes.contains(&ino) {
let written = std::fs::read(pack.path(&export, "rev")).unwrap();
let source = (source_packs.iter())
.find(|path| std::fs::metadata(path).unwrap().ino() == ino)
.unwrap();
assert_eq!(written, git_rev(source), "{}", source.display());
}
}
}
#[test]
fn a_replace_ref_changes_nothing_an_export_packs() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path().join("source");
git(
dir.path(),
&["init", "-q", "--bare", repo.to_str().unwrap()],
"",
);
let first = commit(&repo, None, &[("a.txt", "one")]);
let main = commit(&repo, Some(&first), &[("a.txt", "two")]);
let hidden = commit(&repo, None, &[("secret.txt", "hidden")]);
git(&repo, &["update-ref", "refs/heads/main", &main], "");
git(&repo, &["update-ref", "refs/hidden/x", &hidden], "");
git(
&repo,
&["update-ref", &format!("refs/replace/{first}"), &hidden],
"",
);
let export = dir.path().join("export");
std::fs::create_dir_all(export.join("objects/pack")).unwrap();
let oid = |hex: &str| ObjectId::from_hex(hex.as_bytes()).unwrap();
let fresh = Partitions::pack(
&repo,
&export,
&[oid(&main)],
&[],
knot_types::ObjectFormat::SHA1,
None,
)
.unwrap();
let packed: HashSet<String> = objects_of(&export, fresh.public()).into_iter().collect();
assert_eq!(packed, reached(&repo, &main));
assert!(!packed.contains(&hidden));
}
fn ref_delta_repo(dir: &Path) -> (PathBuf, String, PathBuf) {
let repo = dir.join("source");
git(dir, &["init", "-q", "--bare", repo.to_str().unwrap()], "");
let text: String = (0..400).map(|n| format!("line {n}\n")).collect();
let mut main = commit(&repo, None, &[("a.txt", &text)]);
for n in 0..3 {
main = commit(&repo, Some(&main), &[("a.txt", &format!("{text}{n}\n"))]);
}
git(&repo, &["update-ref", "refs/heads/main", &main], "");
git(
&repo,
&["pack-objects", "-q", "--revs", "objects/pack/pack"],
&format!("{main}\n"),
);
git(&repo, &["prune-packed"], "");
let source = std::fs::read_dir(repo.join("objects/pack"))
.unwrap()
.map(|entry| entry.unwrap().path())
.find(|path| path.extension().is_some_and(|ext| ext == "pack"))
.unwrap();
(repo, main, source)
}
#[test]
fn a_pack_with_ref_deltas_is_copied_with_offset_deltas() {
use std::os::unix::fs::MetadataExt as _;
let dir = tempfile::tempdir().unwrap();
let (repo, main, source) = ref_delta_repo(dir.path());
let format = knot_types::ObjectFormat::SHA1;
assert!(crate::packfile::has_ref_deltas(&source, format).unwrap());
let export = dir.path().join("export");
std::fs::create_dir_all(export.join("objects/pack")).unwrap();
let oid = ObjectId::from_hex(main.as_bytes()).unwrap();
let fresh = Partitions::pack(&repo, &export, &[oid], &[], format, None).unwrap();
let [copied] = fresh.public() else {
panic!("{:?}", fresh.public());
};
let copied = copied.path(&export, "pack");
let ino = |path: &Path| std::fs::metadata(path).unwrap().ino();
assert_ne!(
ino(&copied),
ino(&source),
"a pack with ref deltas was linked"
);
assert!(!crate::packfile::has_ref_deltas(&copied, format).unwrap());
let packed: HashSet<String> = objects_of(&export, fresh.public()).into_iter().collect();
assert_eq!(packed, reached(&repo, &main));
}
#[test]
fn the_ref_delta_answer_is_filed_once_and_read_after() {
use std::os::unix::fs::MetadataExt as _;
let dir = tempfile::tempdir().unwrap();
let (repo, main, source) = ref_delta_repo(dir.path());
let format = knot_types::ObjectFormat::SHA1;
let checksum = gix::odb::pack::index::File::at(source.with_extension("idx"), format.kind())
.unwrap()
.pack_checksum()
.to_string();
let store = RepoStore::open(&dir.path().join("store"));
let oid = ObjectId::from_hex(main.as_bytes()).unwrap();
let mut runs = 0;
let mut linked = || {
runs += 1;
let export = dir.path().join(format!("export-{runs}"));
std::fs::create_dir_all(export.join("objects/pack")).unwrap();
let fresh = Partitions::pack(&repo, &export, &[oid], &[], format, Some(&store)).unwrap();
let [pack] = fresh.public() else {
panic!("{:?}", fresh.public());
};
let ino = |path: &Path| std::fs::metadata(path).unwrap().ino();
ino(&pack.path(&export, "pack")) == ino(&source)
};
assert!(!linked(), "a pack with ref deltas was linked");
assert_eq!(
store.read_file::<bool>(REF_DELTAS_KIND, &checksum),
Some(true)
);
assert!(!linked(), "the filed answer was lost");
store
.publish_file(REF_DELTAS_KIND, &checksum, &false)
.unwrap();
assert!(linked(), "the pack was scanned again");
std::fs::write(store.file_path(REF_DELTAS_KIND, &checksum), "not json").unwrap();
assert!(!linked(), "a corrupt answer was taken");
assert_eq!(
store.read_file::<bool>(REF_DELTAS_KIND, &checksum),
Some(true)
);
std::fs::remove_file(store.file_path(REF_DELTAS_KIND, &checksum)).unwrap();
assert!(!linked(), "a missing answer wasn't scanned for");
}
#[test]
fn nothing_public_is_an_empty_public_pack() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path().join("source");
git(
dir.path(),
&["init", "-q", "--bare", repo.to_str().unwrap()],
"",
);
let cob = commit(&repo, None, &[("cob.txt", "cob")]);
git(&repo, &["update-ref", "refs/cobs/x", &cob], "");
git(&repo, &["repack", "-adq"], "");
let export = dir.path().join("export");
std::fs::create_dir_all(export.join("objects/pack")).unwrap();
let oid = ObjectId::from_hex(cob.as_bytes()).unwrap();
let fresh = Partitions::pack(
&repo,
&export,
&[],
&[oid],
knot_types::ObjectFormat::SHA1,
None,
)
.unwrap();
assert_eq!(fresh.public().len(), 1);
assert!(objects_of(&export, fresh.public()).is_empty());
let cobs: HashSet<String> = objects_of(&export, fresh.cob()).into_iter().collect();
assert_eq!(cobs, reached(&repo, &cob));
}