crates/storage-git-gix/parked-gix-2/gunnar-store-gix/tests/repack_recomputes_deltas.rs
#![cfg(all(feature = "fs", feature = "repack-delta"))]
//! **A repository with one pack can have its deltas recomputed**, and stock git
//! is the judge of the result.
//!
//! # Why this file exists, and why it is built the way it is
//!
//! Until 2026-08-04 `repack()` returned early whenever a repository held one
//! pack, no loose objects and nothing to prune — which is the state a repository
//! seeded by a single push is in, and the state 18 of the 19 real repositories
//! in gunnar's corpus were in. `gunnar gc` therefore could not re-encode a
//! delta anywhere it mattered, and a bench lane measured exactly that: 1 pack of
//! 1 618 917 B before `gc` and 1 pack of 1 618 917 B after.
//!
//! **That was the third feature lost to the same branch**, after the
//! reachability bitmap and the cold-tier fold. All three had green tests. The
//! reason is one sentence and it governs this whole file:
//!
//! > A fixture built by three pushes holds three packs and never takes the
//! > branch.
//!
//! So [`one_pack`] pushes **once**, and asserts the pack count before asserting
//! anything else. A fixture that quietly ended up with two packs would make
//! every test below pass while testing the wrong branch, which is precisely how
//! this bug survived twice.
//!
//! # The red, in the same test as the green
//!
//! [`a_single_pack_repository_can_have_its_deltas_recomputed`] runs the *same*
//! repository under [`DeltaPolicy::Preserve`] and under
//! [`DeltaPolicy::Recompute`], and asserts the first does nothing. The first is
//! the old behaviour, exactly; if the fix regressed to it, the second assertion
//! goes red rather than the test quietly proving that two identical paths agree.
use std::path::{Path, PathBuf};
use gix_hash::Kind;
use gunnar_store::fs::FsStore;
use gunnar_store::repack::{DeltaOrder, DeltaPolicy, RepackOptions, RepackOutcome};
// ── the `git` binary ─────────────────────────────────────────────────────────
fn git(dir: &Path, args: &[&str]) -> String {
let out = git_try(dir, args);
assert!(
out.status.success(),
"git {args:?} in {} failed: {:?}\n--- stdout ---\n{}\n--- stderr ---\n{}",
dir.display(),
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn git_try(dir: &Path, args: &[&str]) -> std::process::Output {
std::process::Command::new("git")
.current_dir(dir)
.args(args)
.env("GIT_AUTHOR_NAME", "Gunnar Test")
.env("GIT_AUTHOR_EMAIL", "gunnar@example.invalid")
.env("GIT_AUTHOR_DATE", "1700000000 +0000")
.env("GIT_COMMITTER_NAME", "Gunnar Test")
.env("GIT_COMMITTER_EMAIL", "gunnar@example.invalid")
.env("GIT_COMMITTER_DATE", "1700000000 +0000")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("could not run git")
}
fn git_available() -> bool {
std::process::Command::new("git")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
// ── the fixture ──────────────────────────────────────────────────────────────
struct Fixture {
_dir: tempfile::TempDir,
git_dir: PathBuf,
hash: Kind,
}
impl Fixture {
fn store(&self) -> FsStore {
FsStore::open_or_init(&self.git_dir, self.hash).expect("open the repository")
}
fn packs(&self) -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = std::fs::read_dir(self.git_dir.join("objects/pack"))
.expect("the pack directory exists")
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("pack"))
.collect();
out.sort();
out
}
fn pack_bytes(&self) -> u64 {
self.packs()
.iter()
.filter_map(|p| std::fs::metadata(p).ok())
.map(|m| m.len())
.sum()
}
/// Every object id the repository can reach, sorted. The set a repack must
/// leave alone.
fn object_ids(&self) -> Vec<String> {
let mut v: Vec<String> = git(&self.git_dir, &["rev-list", "--objects", "--all"])
.lines()
.filter_map(|l| l.split_whitespace().next().map(str::to_owned))
.collect();
v.sort();
v
}
}
/// A monorepo shape, pushed **once**, so the repository holds exactly one pack.
///
/// `dirs` directories each holding an `index.ts` and a `README.md`, over
/// `revisions` commits. The shape is not decoration: it is the case git's
/// name-hash v1 cannot see, because every `index.ts` in the tree shares its last
/// sixteen path bytes and therefore its v1 hash. A fixture of one file per
/// commit would order identically under all three [`DeltaOrder`]s and could not
/// tell them apart.
fn one_pack(hash: Kind, dirs: usize, revisions: usize) -> Fixture {
let dir = tempfile::tempdir().expect("temporary directory");
let git_dir = dir.path().join("repo.git");
FsStore::open_or_init(&git_dir, hash).expect("create the bare repository");
// Without this git unpacks a small push into loose objects and the
// single-pack state this whole file is about never arises.
git(
dir.path(),
&[
"--git-dir",
git_dir.to_str().unwrap(),
"config",
"receive.unpackLimit",
"1",
],
);
let work = dir.path().join("work");
std::fs::create_dir_all(&work).expect("create the working tree");
let fmt = match hash {
Kind::Sha256 => "sha256",
_ => "sha1",
};
git(
&work,
&[
"init",
"--quiet",
&format!("--object-format={fmt}"),
"--initial-branch=main",
".",
],
);
for rev in 0..revisions {
for d in 0..dirs {
let path = work.join(format!("packages/pkg{d:03}/src"));
std::fs::create_dir_all(&path).expect("mkdir");
// Long, highly similar content: what a delta encoder is for. Each
// revision changes one line in the middle, so the right base makes
// the delta tiny and the wrong base makes it the whole file.
let body: String = (0..200)
.map(|line| {
if line == rev % 200 {
format!("export const marker{d}_{rev} = {line};\n")
} else {
format!("export const value{line} = 'package {d} line {line}';\n")
}
})
.collect();
std::fs::write(path.join("index.ts"), &body).expect("write");
std::fs::write(
path.parent().unwrap().join("README.md"),
format!("# package {d}\n\nrevision {rev}\n{}", "filler\n".repeat(50)),
)
.expect("write");
}
git(&work, &["add", "-A"]);
git(&work, &["commit", "--quiet", "-m", &format!("rev {rev}")]);
}
// ONE push. This is the whole premise of the file.
git(
&work,
&[
"push",
"--quiet",
git_dir.to_str().unwrap(),
"HEAD:refs/heads/main",
],
);
let f = Fixture {
_dir: dir,
git_dir,
hash,
};
// The premise, asserted rather than assumed. A fixture that ended up with
// two packs would exercise the branch that always worked, and every
// assertion in this file would be green for the wrong reason — which is the
// exact mistake that let this bug survive two earlier fixes.
assert_eq!(
f.packs().len(),
1,
"the fixture must hold exactly ONE pack, or it tests the wrong branch"
);
f
}
fn repack(f: &Fixture, options: &RepackOptions) -> RepackOutcome {
f.store().repack(options).expect("repack")
}
// ── 1. the headline, with its own red ────────────────────────────────────────
/// A single-pack repository re-encodes its deltas, and the *same* repository
/// under the old policy does not.
///
/// The `Preserve` half is not a formality. It is the behaviour this change
/// removed, run against the identical fixture: if the single-pack short-circuit
/// ever comes back, that half stays green and the `Recompute` half goes red,
/// which is the only arrangement in which the green means anything.
#[test]
fn a_single_pack_repository_can_have_its_deltas_recomputed() {
assert!(git_available(), "this test requires `git` on PATH");
let f = one_pack(Kind::Sha1, 12, 8);
let before_bytes = f.pack_bytes();
let before_ids = f.object_ids();
// ── the old behaviour, on purpose ────────────────────────────────────────
let preserved = repack(
&f,
&RepackOptions::new().delta_policy(DeltaPolicy::Preserve),
);
assert!(
!preserved.rewrote,
"consolidating a single-pack repository rewrote it, so this fixture is \
not in the state the test is about: {preserved:?}"
);
assert_eq!(
f.pack_bytes(),
before_bytes,
"a preserving repack of one pack changed its bytes"
);
// ── and the fix ─────────────────────────────────────────────────────────
let recoded = repack(
&f,
&RepackOptions::new().delta_policy(DeltaPolicy::Recompute),
);
assert!(
recoded.rewrote,
"a single-pack repository STILL cannot be re-encoded; this is the bug \
the whole change exists to remove: {recoded:?}"
);
assert!(
recoded.objects_deltified > 0,
"the repack rewrote the pack but computed not one delta, so it did the \
expensive half of the work and none of the useful half: {recoded:?}"
);
assert_eq!(
recoded.objects_copied_from_pack, 0,
"the re-encoding producer copied an entry, which means it did not \
re-encode it: {recoded:?}"
);
assert_eq!(
recoded.objects_copied_from_pack + recoded.objects_recompressed + recoded.objects_deltified,
recoded.objects as usize,
"the generator's accounting does not add up: {recoded:?}"
);
// The applied output, read back off the pack that is on disk — not the
// encoder's own report about itself.
let after = recoded
.delta_fraction_after
.expect("the pack's composition was not measured");
let before = recoded
.delta_fraction_before
.expect("the pack's composition was not measured");
assert!(
after > 0.0,
"the pack on disk holds no deltas at all after a re-encode: {recoded:?}"
);
eprintln!(
"delta fraction {before:.3} -> {after:.3}, pack {before_bytes} -> {} bytes",
f.pack_bytes()
);
// ── and nothing was lost ────────────────────────────────────────────────
assert_eq!(
f.packs().len(),
1,
"a re-encoding repack left more than one pack behind"
);
assert_eq!(
f.object_ids(),
before_ids,
"the re-encode changed which objects the repository holds"
);
}
// ── 2. stock git is the judge ────────────────────────────────────────────────
/// `index-pack --strict`, `fsck --full --strict`, and every object's bytes
/// compared back through `cat-file`.
///
/// gunnar reading back what gunnar wrote can be wrong at both ends, and a delta
/// that reconstructs wrongly is invisible to every check that does not actually
/// reconstruct it. `--strict` on `index-pack` is what makes the pack's deltas be
/// applied rather than merely parsed.
#[test]
fn stock_git_accepts_a_re_encoded_pack() {
assert!(git_available(), "this test requires `git` on PATH");
for hash in [Kind::Sha1, Kind::Sha256] {
let f = one_pack(hash, 8, 6);
let before = object_bytes(&f);
let outcome = repack(
&f,
&RepackOptions::new().delta_policy(DeltaPolicy::Recompute),
);
assert!(outcome.rewrote, "{hash}: nothing was re-encoded");
assert!(
outcome.objects_deltified > 0,
"{hash}: no deltas were computed, so `--strict` would be applying none"
);
// fsck walks and reconstructs every object in the repository.
git(&f.git_dir, &["fsck", "--full", "--strict"]);
// …and the bitmap written beside the re-encoded pack still answers what
// the walk answers. This is not covered by fsck and it is not
// hypothetical: a re-encode emits its entries in the delta search's
// order rather than the order they sat in the old pack, so anything
// that keyed on pack position would go wrong here and nowhere else.
assert!(
outcome.bitmap.is_some(),
"{hash}: no bitmap was written beside the re-encoded pack"
);
let ids = |args: &[&str]| -> Vec<String> {
let mut v: Vec<String> = git(&f.git_dir, args)
.lines()
.filter_map(|l| l.split_whitespace().next().map(str::to_owned))
.collect();
v.sort();
v
};
let walked = ids(&["rev-list", "--objects", "--all"]);
let bitmapped = ids(&["rev-list", "--objects", "--use-bitmap-index", "--all"]);
assert!(
walked.len() > 50,
"{hash}: the plain walk found only {} objects",
walked.len()
);
assert_eq!(
bitmapped, walked,
"{hash}: the bitmap over the re-encoded pack disagrees with the walk"
);
// …and the pack re-verifies from its bytes alone, in a directory that
// holds nothing else, so nothing can be resolved out of the repository.
let pack = f.packs().pop().expect("a .pack");
let scratch = tempfile::tempdir().expect("temporary directory");
// `--object-format` is not optional here and its absence is not a
// compile error: outside a repository `index-pack` assumes SHA-1, reads
// a SHA-256 pack with 20-byte ids and reports `badTree: cannot be
// parsed as a tree` — a corruption message about a pack that is
// perfectly fine. MEASURED 2026-08-04 by hitting exactly that.
let format = format!(
"--object-format={}",
match hash {
Kind::Sha256 => "sha256",
_ => "sha1",
}
);
let out = git_try(
scratch.path(),
&[
"index-pack",
"--strict",
&format,
"-o",
"out.idx",
pack.to_str().unwrap(),
],
);
assert!(
out.status.success(),
"{hash}: git index-pack --strict rejected the re-encoded pack: {}",
String::from_utf8_lossy(&out.stderr)
);
// Byte for byte, not by count. A pack that returned different bytes
// under the right ids would pass every check above.
let after = object_bytes(&f);
assert_eq!(
before.len(),
after.len(),
"{hash}: the object count changed across the re-encode"
);
assert!(
before.len() > 50,
"{hash}: the fixture has only {} objects, which is not evidence",
before.len()
);
assert_eq!(
before, after,
"{hash}: an object came back with different bytes across the re-encode"
);
}
}
/// The negative control for the check above: `index-pack --strict` must be
/// **able** to reject a pack whose delta bytes are wrong.
///
/// Without it, every green in [`stock_git_accepts_a_re_encoded_pack`] could be a
/// check that accepts anything. The damage is one byte inside the pack's body,
/// past the header, which is the shape of a mis-encoded delta.
#[test]
fn git_goes_red_on_a_damaged_re_encoded_pack() {
assert!(git_available(), "this test requires `git` on PATH");
let f = one_pack(Kind::Sha1, 6, 4);
let outcome = repack(
&f,
&RepackOptions::new().delta_policy(DeltaPolicy::Recompute),
);
assert!(outcome.rewrote);
let pack = f.packs().pop().expect("a .pack");
let mut bytes = std::fs::read(&pack).expect("read the pack");
// Positive control first: undamaged, it must be accepted, or the red below
// proves nothing.
let scratch = tempfile::tempdir().expect("temporary directory");
std::fs::write(scratch.path().join("copy.pack"), &bytes).expect("write");
let good = git_try(
scratch.path(),
&["index-pack", "--strict", "-o", "good.idx", "copy.pack"],
);
assert!(
good.status.success(),
"the undamaged pack is already rejected, so this test cannot prove \
anything: {}",
String::from_utf8_lossy(&good.stderr)
);
// One byte in the middle of the body.
let at = bytes.len() / 2;
bytes[at] ^= 0xff;
let scratch = tempfile::tempdir().expect("temporary directory");
std::fs::write(scratch.path().join("copy.pack"), &bytes).expect("write");
let bad = git_try(
scratch.path(),
&["index-pack", "--strict", "-o", "bad.idx", "copy.pack"],
);
assert!(
!bad.status.success(),
"git accepted a pack with a corrupted body, so `--strict` is blind here"
);
}
/// Every object's canonical bytes, straight out of `git cat-file`.
fn object_bytes(f: &Fixture) -> Vec<(String, Vec<u8>)> {
let mut out = Vec::new();
for id in f.object_ids() {
let raw = std::process::Command::new("git")
.current_dir(&f.git_dir)
.args(["cat-file", "-p", &id])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("run git cat-file");
assert!(raw.status.success(), "git cat-file -p {id} failed");
out.push((id, raw.stdout));
}
out
}
// ── 3. the policy ────────────────────────────────────────────────────────────
/// `Auto` re-encodes a repository with no deltas, and declines one that already
/// has them.
///
/// Both halves, because a policy that always fired and a policy that never fired
/// would each pass one of them.
#[test]
fn the_auto_policy_fires_on_a_bad_pack_and_declines_a_good_one() {
assert!(git_available(), "this test requires `git` on PATH");
let f = one_pack(Kind::Sha1, 10, 6);
// git's own push is well deltified, so `Auto` must leave it alone.
let healthy = repack(&f, &RepackOptions::new());
let fraction = healthy
.delta_fraction_before
.expect("the composition was not measured");
assert!(
fraction >= 0.20,
"premise: git's pushed pack is only {fraction:.3} deltified, so this \
half is testing the wrong case"
);
assert!(
!healthy.rewrote,
"`Auto` re-encoded a pack that is {fraction:.3} deltified: {healthy:?}"
);
// Now a repository whose objects are all loose and therefore all
// undeltified, which is what gunnar's own client used to produce.
let loose = loose_only(Kind::Sha1, 6, 6);
let bad = repack(&loose, &RepackOptions::new());
assert_eq!(
bad.delta_fraction_before,
Some(0.0),
"premise: the loose fixture is supposed to hold no deltas at all"
);
assert!(
bad.rewrote,
"`Auto` declined a repository with no deltas whatever: {bad:?}"
);
assert!(
bad.objects_deltified > 0,
"`Auto` fired but computed no deltas: {bad:?}"
);
}
/// A repository whose objects are all loose: no packs, no deltas, nothing to
/// copy. `git gc`'s starting point, and gunnar's own client's output before
/// `write_pack_deltified` existed.
fn loose_only(hash: Kind, dirs: usize, revisions: usize) -> Fixture {
let dir = tempfile::tempdir().expect("temporary directory");
let git_dir = dir.path().join("repo.git");
FsStore::open_or_init(&git_dir, hash).expect("create the bare repository");
let work = dir.path().join("work");
std::fs::create_dir_all(&work).expect("create the working tree");
let fmt = match hash {
Kind::Sha256 => "sha256",
_ => "sha1",
};
git(
&work,
&[
"init",
"--quiet",
&format!("--object-format={fmt}"),
"--initial-branch=main",
".",
],
);
for rev in 0..revisions {
for d in 0..dirs {
let path = work.join(format!("packages/pkg{d:03}/src"));
std::fs::create_dir_all(&path).expect("mkdir");
let body: String = (0..150)
.map(|line| {
if line == rev {
format!("changed at {d}/{rev}\n")
} else {
format!("stable line {line} of package {d}\n")
}
})
.collect();
std::fs::write(path.join("index.ts"), &body).expect("write");
}
git(&work, &["add", "-A"]);
git(&work, &["commit", "--quiet", "-m", &format!("rev {rev}")]);
}
// `--no-thin` plus a high unpackLimit: git unpacks the push into loose
// objects, which is exactly the state being tested.
git(
dir.path(),
&[
"--git-dir",
git_dir.to_str().unwrap(),
"config",
"receive.unpackLimit",
"100000",
],
);
git(
&work,
&[
"push",
"--quiet",
"--no-thin",
git_dir.to_str().unwrap(),
"HEAD:refs/heads/main",
],
);
let f = Fixture {
_dir: dir,
git_dir,
hash,
};
assert!(
f.packs().is_empty(),
"the loose fixture ended up with a pack, so it is not loose"
);
f
}
// ── 4. the ordering, which is the whole lever ────────────────────────────────
/// The three orderings produce **different packs**, and the path-aware ones are
/// not worse.
///
/// This is the assertion that the sort key is wired at all. A `DeltaOrder` that
/// was accepted, logged and then ignored would give three identical pack sizes,
/// and every other test in this file would still be green — which is the exact
/// shape of a silent green this tree has found nine of.
///
/// It asserts *difference* and a *direction*, not a ratio. The published ratios
/// (fluentui 439 MiB → 160 MiB) are for repositories four orders of magnitude
/// larger than a fixture, and claiming one here would be inventing a number.
#[test]
fn the_delta_ordering_changes_the_pack_it_produces() {
assert!(git_available(), "this test requires `git` on PATH");
let mut sizes = Vec::new();
for order in [
DeltaOrder::Size,
DeltaOrder::NameHashV1,
DeltaOrder::NameHashV2,
DeltaOrder::PathWalk,
] {
// A fresh fixture per ordering: repacking the same one repeatedly would
// measure each ordering against the previous ordering's output.
//
// **Loose**, not a pushed pack, and that is not incidental. A repack of
// a single pack refuses to install a result that is not smaller than the
// pack it would replace (see
// `a_re_encode_that_would_not_shrink_the_pack_is_declined`), and
// `DeltaOrder::Size` really is worse than what stock git shipped — so on
// a pushed fixture this loop would measure "the guard declined" three
// times and compare nothing. A repository with no pack has nothing to be
// declined against, so every ordering's output reaches the disk and can
// be read.
let f = loose_only(Kind::Sha1, 16, 10);
let outcome = repack(
&f,
&RepackOptions::new()
.delta_policy(DeltaPolicy::Recompute)
.delta_order(order),
);
assert!(outcome.rewrote, "{order:?}: nothing was re-encoded");
git(&f.git_dir, &["fsck", "--full", "--strict"]);
let bytes = f.pack_bytes();
eprintln!(
"{order:?}: {bytes} pack bytes, {} deltified of {}",
outcome.objects_deltified, outcome.objects
);
sizes.push((order, bytes, outcome.objects_deltified));
}
let by_size: u64 = sizes[0].1;
let v2: u64 = sizes[2].1;
let path_walk: u64 = sizes[3].1;
// The key is wired: at least one path-aware ordering produced a different
// pack from the size-only one. Identical bytes would mean the sort key never
// reached the window.
assert!(
v2 != by_size || path_walk != by_size,
"every ordering produced the same {by_size}-byte pack, so the sort key \
is not reaching the delta search at all: {sizes:?}"
);
// And it is not wired backwards. On a fixture whose every `index.ts` shares
// its last sixteen path bytes, grouping by path must not do *worse* than
// ignoring paths entirely — a 5% margin, because a fixture this small has
// window-boundary noise a real repository does not.
let margin = by_size + by_size / 20;
assert!(
v2 <= margin,
"name-hash v2 produced {v2} bytes against {by_size} for no ordering at \
all, which is the wrong direction: {sizes:?}"
);
assert!(
path_walk <= margin,
"path-walk produced {path_walk} bytes against {by_size} for no ordering \
at all, which is the wrong direction: {sizes:?}"
);
}
/// A re-encode that would leave the repository **bigger** is thrown away, and
/// the pack that was already there is kept.
///
/// `DeltaOrder::Size` is the honest case, not a contrived one: on a monorepo
/// shape it is genuinely worse than the ordering stock git used when it built
/// the pack in the first place, so the search runs, produces a valid pack, and
/// that pack is larger. Installing it would churn the pack id — invalidating
/// every reader's mmap — to make the repository bigger.
///
/// Seen red: delete the `new_bytes >= old_bytes` branch in `repack()` and this
/// goes red on `rewrote`, with the pack on disk grown.
#[test]
fn a_re_encode_that_would_not_shrink_the_pack_is_declined() {
assert!(git_available(), "this test requires `git` on PATH");
let f = one_pack(Kind::Sha1, 16, 10);
let before_bytes = f.pack_bytes();
let before_packs = f.packs();
let outcome = repack(
&f,
&RepackOptions::new()
.delta_policy(DeltaPolicy::Recompute)
.delta_order(DeltaOrder::Size),
);
// The premise, and the whole reason `reencode_declined` is a field: without
// it, "the search ran and its answer was worse" is indistinguishable from
// "the search never ran", and this test would pass just as happily against a
// build where `DeltaPolicy::Recompute` did nothing at all.
assert!(
outcome.reencode_declined,
"the re-encode was never declined, so either it was not attempted or its \
result was installed: {outcome:?}"
);
assert!(
!outcome.rewrote,
"a re-encode that makes the repository bigger was installed anyway: {outcome:?}"
);
assert_eq!(
f.pack_bytes(),
before_bytes,
"the pack on disk changed size despite the repack reporting no rewrite"
);
assert_eq!(
f.packs(),
before_packs,
"the declined pack was left on disk, or the original was replaced"
);
// And the repository is still readable, which is the thing a half-finished
// decline would break.
git(&f.git_dir, &["fsck", "--full", "--strict"]);
}
// ── 5. real repositories ─────────────────────────────────────────────────────
/// The same checks against **real repositories**, which is where the branch
/// actually mattered.
///
/// Point `GUNNAR_REAL_REPOS` at a colon-separated list of repository paths.
/// Skipped when unset — the corpus lives outside this repository — but the skip
/// is printed, so a green here is never mistaken for a run.
///
/// The mirrors are built by **pushing**, not by `git clone --mirror`: a mirror
/// clone silently repacks, so a corpus built that way measures an
/// already-consolidated, already-well-deltified pack and reports that `gc` had
/// nothing to do. That mistake cost one lane a published conclusion on
/// 2026-08-04 and is why the pack count is asserted below.
#[test]
fn real_repositories_survive_a_re_encode_and_report_what_it_cost() {
assert!(git_available(), "this test requires `git` on PATH");
let Ok(list) = std::env::var("GUNNAR_REAL_REPOS") else {
eprintln!("SKIPPED: set GUNNAR_REAL_REPOS to a colon-separated list of repositories");
return;
};
let scratch = tempfile::tempdir().expect("temporary directory");
let mut checked = 0usize;
let mut smaller = 0usize;
for src in list.split(':').filter(|s| !s.is_empty()) {
let name = Path::new(src)
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_owned();
let mirror = scratch.path().join(format!("{name}.git"));
let fmt = git(Path::new(src), &["rev-parse", "--show-object-format"]);
let hash = if fmt.trim() == "sha256" {
Kind::Sha256
} else {
Kind::Sha1
};
FsStore::open_or_init(&mirror, hash).expect("create the bare repository");
git(
scratch.path(),
&[
"--git-dir",
mirror.to_str().unwrap(),
"config",
"receive.unpackLimit",
"1",
],
);
let pushed = git_try(
Path::new(src),
&["push", "--quiet", "--mirror", mirror.to_str().unwrap()],
);
if !pushed.status.success() {
eprintln!("skipping {name}: push failed");
continue;
}
let f = Fixture {
_dir: tempfile::tempdir().expect("temporary directory"),
git_dir: mirror.clone(),
hash,
};
let before_ids = f.object_ids();
if before_ids.len() < 200 {
eprintln!("skipping {name}: only {} objects", before_ids.len());
continue;
}
let before_bytes = f.pack_bytes();
let packs_before = f.packs().len();
let started = std::time::Instant::now();
let outcome = repack(
&f,
&RepackOptions::new()
.delta_policy(DeltaPolicy::Recompute)
.delta_order(DeltaOrder::NameHashV2),
);
let elapsed = started.elapsed();
assert!(outcome.rewrote, "{name}: nothing was re-encoded");
assert_eq!(
f.object_ids(),
before_ids,
"{name}: the re-encode changed the object graph"
);
git(&f.git_dir, &["fsck", "--full", "--strict"]);
let after_bytes = f.pack_bytes();
if after_bytes < before_bytes {
smaller += 1;
}
eprintln!(
"{name} ({}): {} packs, {} objects, {before_bytes} -> {after_bytes} bytes \
({:.4}x), delta fraction {:.3} -> {:.3}, {:.1} s",
fmt.trim(),
packs_before,
before_ids.len(),
after_bytes as f64 / before_bytes as f64,
outcome.delta_fraction_before.unwrap_or(f32::NAN),
outcome.delta_fraction_after.unwrap_or(f32::NAN),
elapsed.as_secs_f64(),
);
checked += 1;
}
assert!(
checked > 0,
"the list named no usable repository, so nothing was checked"
);
eprintln!("{smaller} of {checked} repositories came out smaller");
}
// ── 5. the window, which is the ordering's other half ────────────────────────
/// `RepackOptions::delta_search` reaches the encoder, and the proof is an
/// **applied output**: a repack given a window of `1` produces a *different,
/// larger* pack than the same repack given a window of `32`.
///
/// The two numbers were `..Default::default()`'s hardcoded 10 and 50 until
/// 2026-08-04 and no caller could reach them. A field added to `RepackOptions`,
/// threaded through `write_recoded_pack_file` and then dropped on the floor
/// would leave both repacks byte-identical and every other test in this file
/// green — the exact shape of the silent green this tree has found nine of, so
/// the assertion is on the bytes and not on the option round-tripping.
///
/// **Seen red on 2026-08-04**: revert the `window:`/`depth:` lines in
/// `write_recoded_pack_file` to `..Default::default()` and the two repacks
/// produce the same byte count, which fails on `assert_ne`.
///
/// The direction is asserted too, and it is not a tuning claim: a window of one
/// offers every object exactly one candidate, so it can only find a subset of
/// the bases a window of 32 can. Fewer bases found cannot make a pack smaller.
#[test]
fn the_delta_window_reaches_the_search_and_changes_the_pack() {
assert!(git_available(), "this test requires `git` on PATH");
let mut out = Vec::new();
for window in [1usize, 32] {
// Loose, for the same reason the ordering test is loose: a repack of a
// single pack declines a result that is not smaller, so the narrow
// window would be measured as "declined" rather than as its own pack.
let f = loose_only(Kind::Sha1, 16, 10);
let outcome = repack(
&f,
&RepackOptions::new()
.delta_policy(DeltaPolicy::Recompute)
.delta_order(DeltaOrder::NameHashV2)
.delta_search(window, 50),
);
assert!(outcome.rewrote, "window {window}: nothing was re-encoded");
git(&f.git_dir, &["fsck", "--full", "--strict"]);
let bytes = f.pack_bytes();
eprintln!(
"window {window}: {bytes} pack bytes, {} deltified of {}",
outcome.objects_deltified, outcome.objects
);
out.push((window, bytes, outcome.objects_deltified));
}
let (_, narrow, narrow_deltas) = out[0];
let (_, wide, wide_deltas) = out[1];
assert_ne!(
narrow, wide,
"a window of 1 and a window of 32 produced the same {narrow}-byte pack, \
so `RepackOptions::delta_window` is not reaching the delta search: {out:?}"
);
assert!(
wide < narrow,
"the wider window produced the LARGER pack ({wide} against {narrow}), \
which means the window is wired backwards: {out:?}"
);
assert!(
wide_deltas >= narrow_deltas,
"the wider window found FEWER bases ({wide_deltas} against \
{narrow_deltas}), which a superset search cannot do: {out:?}"
);
}
/// The depth cap reaches the search too, and it is a **separate** wire from the
/// window.
///
/// One setter sets both, so a mapping that assigned the window to both fields
/// would pass the test above and be undetectable there. Here the window is held
/// at 32 and only the depth moves; a pack built with a chain limit of 1 cannot
/// be the same bytes as one built with a limit of 50.
///
/// **Seen red on 2026-08-04**: map `depth: options.delta_window` in
/// `write_recoded_pack_file` and this fails while
/// `the_delta_window_reaches_the_search_and_changes_the_pack` still passes.
#[test]
fn the_delta_depth_is_its_own_wire() {
assert!(git_available(), "this test requires `git` on PATH");
let mut out = Vec::new();
for depth in [1usize, 50] {
let f = loose_only(Kind::Sha1, 16, 10);
let outcome = repack(
&f,
&RepackOptions::new()
.delta_policy(DeltaPolicy::Recompute)
.delta_order(DeltaOrder::NameHashV2)
.delta_search(32, depth),
);
assert!(outcome.rewrote, "depth {depth}: nothing was re-encoded");
git(&f.git_dir, &["fsck", "--full", "--strict"]);
let bytes = f.pack_bytes();
eprintln!("depth {depth}: {bytes} pack bytes");
out.push((depth, bytes));
}
assert_ne!(
out[0].1, out[1].1,
"a chain limit of 1 and one of 50 produced the same pack, so \
`RepackOptions::delta_depth` is not reaching the delta search — or it \
is being fed the window: {out:?}"
);
assert!(
out[1].1 < out[0].1,
"the deeper chain limit produced the LARGER pack ({} against {}), which \
means the depth is wired backwards: {out:?}",
out[1].1,
out[0].1
);
}
// ── 6. the copy path must not roll dice ──────────────────────────────────────
/// A store shaped like a real one: **several pushes**, so several packs, with
/// the same objects in more than one of them.
///
/// That last part is the whole point and it is not contrived. A push sends a
/// thin pack and `index-pack --fix-thin` appends the bases it was thin against,
/// so every object that was a delta base for the next push is physically in two
/// packs. All 19 repositories in gunnar's corpus are in this state; the
/// single-pack fixtures above are the state a repository is in only immediately
/// after a `gc`.
fn many_packs(hash: Kind, dirs: usize, revisions: usize, pushes: usize) -> Fixture {
let dir = tempfile::tempdir().expect("temporary directory");
let git_dir = dir.path().join("repo.git");
FsStore::open_or_init(&git_dir, hash).expect("create the bare repository");
git(
dir.path(),
&[
"--git-dir",
git_dir.to_str().unwrap(),
"config",
"receive.unpackLimit",
"1",
],
);
let work = dir.path().join("work");
std::fs::create_dir_all(&work).expect("create the working tree");
let fmt = match hash {
Kind::Sha256 => "sha256",
_ => "sha1",
};
git(
&work,
&[
"init",
"--quiet",
&format!("--object-format={fmt}"),
"--initial-branch=main",
".",
],
);
let per_push = revisions.div_ceil(pushes).max(1);
for rev in 0..revisions {
for d in 0..dirs {
let path = work.join(format!("packages/pkg{d:03}/src"));
std::fs::create_dir_all(&path).expect("mkdir");
let body: String = (0..200)
.map(|line| {
if line == rev % 200 {
format!("export const marker{d}_{rev} = {line};\n")
} else {
format!("export const value{line} = 'package {d} line {line}';\n")
}
})
.collect();
std::fs::write(path.join("index.ts"), &body).expect("write");
std::fs::write(
path.parent().unwrap().join("README.md"),
format!("# package {d}\n\nrevision {rev}\n{}", "filler\n".repeat(50)),
)
.expect("write");
}
git(&work, &["add", "-A"]);
git(&work, &["commit", "--quiet", "-m", &format!("rev {rev}")]);
// A push every `per_push` commits, and one at the end: each one is a
// thin pack against what the server already has, and `--fix-thin`
// duplicates its bases into the new pack.
if (rev + 1) % per_push == 0 || rev + 1 == revisions {
git(
&work,
&[
"push",
"--quiet",
git_dir.to_str().unwrap(),
"HEAD:refs/heads/main",
],
);
}
}
let f = Fixture {
_dir: dir,
git_dir,
hash,
};
assert!(
f.packs().len() >= 3,
"the fixture must hold several packs or it exercises the wrong branch, \
got {}",
f.packs().len()
);
f
}
/// **Two `gc`s of one store produce the same pack, byte for byte.**
///
/// One store, copied three times with its timestamps preserved, repacked three
/// times. A copy-path repack — the delta policy is `Preserve`, nothing is
/// re-encoded — so the three runs differ in exactly one thing: which thread got
/// to which object first.
///
/// # Why a forge may not roll dice here
///
/// gunnar's serve path copies pack entries verbatim (`P-001`). Whatever the
/// repacker leaves in the pack is what every clone and every fetch is sent
/// until the next `gc`. A nondeterministic repacker is therefore a
/// nondeterministic *wire format*, and "the same server, the same repository,
/// the same client, a different number of bytes" is not a thing anyone can
/// debug.
///
/// # Seen red, on the corpus and then here
///
/// MEASURED on oden 2026-08-04, before the fix: three `gunnar gc` runs over
/// three byte-identical copies of the 19-repository `workspace_nordisk` store,
/// default flags, the policy declining on all 19 —
///
/// | repository | run 1 | run 2 | run 3 | spread |
/// |---|---:|---:|---:|---:|
/// | nornir | 17 063 575 | 17 550 935 | 18 106 353 | **6.1%** |
/// | knut | 3 461 019 | 3 628 435 | 3 647 139 | 5.4% |
/// | modgunn | 1 582 224 | 1 516 439 | 1 558 746 | 4.3% |
///
/// Pinned to a single CPU the same runs were byte-identical, and nornir came
/// out 15 854 870 — smaller than all three. The cause is `count::objects`
/// recording whichever pack a duplicated object was found in first, and
/// `iter_from_counts` then sorting by `(pack_id, pack_offset)` and being able
/// to copy a delta only when its base precedes it.
///
/// Restore `thread_limit: options.thread_limit` on the count in
/// `write_pack_file` and this test goes red on run 1.
///
/// # What this does NOT guarantee, and it was found the hard way
///
/// The copies keep their **mtimes**, deliberately. An earlier version of this
/// test built a fresh fixture per run and failed even with the fix, because a
/// store's pack *load order* is its packs' mtime order, several pushes into a
/// fixture land inside one timestamp tick, and a duplicated object then
/// resolves to a different pack. That is a second, separate nondeterminism,
/// it is inside gix's odb slot assignment rather than in this module, and it is
/// NOT fixed here. What is fixed and what is guarded is the thread race; two
/// stores that differ only in pack mtimes may still repack differently.
#[test]
fn two_copy_repacks_of_one_store_produce_the_same_pack() {
assert!(git_available(), "this test requires `git` on PATH");
// ONE store, built once. Three copies of it, not three fixtures: the
// question is whether one input has one output.
let origin = many_packs(Kind::Sha1, 24, 24, 6);
let packs_in = origin.packs().len();
let mut results = Vec::new();
for run in 0..3 {
let dir = tempfile::tempdir().expect("temporary directory");
let git_dir = dir.path().join("repo.git");
// `cp -a`: contents AND timestamps, so the pack load order is the same
// in all three copies and the only remaining variable is scheduling.
let out = std::process::Command::new("cp")
.arg("-a")
.arg(&origin.git_dir)
.arg(&git_dir)
.output()
.expect("cp");
assert!(out.status.success(), "cp -a failed: {out:?}");
let f = Fixture {
_dir: dir,
git_dir,
hash: Kind::Sha1,
};
assert_eq!(
f.packs().len(),
packs_in,
"run {run}: the copy does not hold the same packs as the original"
);
let outcome = repack(
&f,
&RepackOptions::new().delta_policy(DeltaPolicy::Preserve),
);
assert!(outcome.rewrote, "run {run}: nothing was consolidated");
assert_eq!(
f.packs().len(),
1,
"run {run}: {packs_in} packs did not fold into one"
);
git(&f.git_dir, &["fsck", "--full", "--strict"]);
let pack = f.packs().into_iter().next().expect("one pack");
let bytes = std::fs::read(&pack).expect("read the pack");
results.push((
bytes.len(),
pack.file_name().unwrap().to_string_lossy().into_owned(),
));
}
// The pack's NAME is its own content hash, so comparing names compares
// every byte; the length is carried alongside so a failure says how far
// apart the two answers were rather than only that they differed.
let first = &results[0];
for (i, r) in results.iter().enumerate().skip(1) {
assert_eq!(
r, first,
"run {i} produced a different pack from run 0 ({r:?} against \
{first:?}); `gunnar gc` is copying pack entries in a \
thread-dependent order, and gunnar's serve path copies those \
entries verbatim, so this is a nondeterministic wire format"
);
}
eprintln!("three copy repacks of one {packs_in}-pack store agreed: {first:?}");
}
// ── 7. the default ordering, asserted as an applied output ───────────────────
/// **`RepackOptions::default()` re-encodes under `TransferV2`, and the proof is
/// the pack it writes.**
///
/// `assert_eq!(DeltaOrder::default(), DeltaOrder::TransferV2)` would be a state
/// round-trip: it would pass just as well if `write_recoded_pack_file` ignored
/// the field, which is the failure this file has caught twice. So the default
/// is compared against two *explicit* repacks of the same fixture — it must
/// produce the transfer ordering's pack and not the previous default's.
///
/// The `NameHashV2` half is the negative control. Without it, a fixture on
/// which the two orderings happened to agree would make the assertion vacuous
/// and it would stay green for ever.
///
/// **Seen red on 2026-08-04**: move `#[default]` back to `NameHashV2` and this
/// fails on the first assertion, with the default's bytes equal to the
/// name-hash pack's.
#[test]
fn the_default_ordering_is_the_transfer_ordering_in_the_pack_it_writes() {
assert!(git_available(), "this test requires `git` on PATH");
let mut out = Vec::new();
for (label, options) in [
// The default: `delta_order` deliberately NOT named.
(
"default",
RepackOptions::new().delta_policy(DeltaPolicy::Recompute),
),
(
"transfer-v2",
RepackOptions::new()
.delta_policy(DeltaPolicy::Recompute)
.delta_order(DeltaOrder::TransferV2),
),
(
"name-hash-v2",
RepackOptions::new()
.delta_policy(DeltaPolicy::Recompute)
.delta_order(DeltaOrder::NameHashV2),
),
] {
let f = loose_only(Kind::Sha1, 16, 10);
let outcome = repack(&f, &options);
assert!(outcome.rewrote, "{label}: nothing was re-encoded");
git(&f.git_dir, &["fsck", "--full", "--strict"]);
let bytes = f.pack_bytes();
eprintln!("{label}: {bytes} pack bytes");
out.push((label, bytes));
}
assert_eq!(
out[0].1, out[1].1,
"the default repack produced {} bytes and an explicit `TransferV2` \
repack produced {}, so `DeltaOrder::default()` is not what a default \
repack actually runs: {out:?}",
out[0].1, out[1].1
);
assert_ne!(
out[0].1, out[2].1,
"the default repack and an explicit `NameHashV2` repack produced the \
same {} bytes, so this fixture cannot tell the two orderings apart and \
the assertion above proves nothing: {out:?}",
out[0].1
);
}