crates/gunnar-client/tests/push_deltas.rs
//! **The pack a push sends must carry deltas**, judged by stock git.
//!
//! # What went wrong, and why nothing caught it
//!
//! `drive_push` called `gunnar_pack::write_pack`, whose own doc says it *"emits
//! no deltas at all"*. Every correctness test in the tree stayed green, because
//! an undeltified pack is a perfectly correct pack: the object closure is
//! complete, `git fsck --strict` passes, the oids match. It is simply four to
//! twelve times too big, and the server's pack-copy then reproduces that
//! size into every later clone, forever.
//!
//! MEASURED 2026-08-03, 19 real nordisk repositories: the server stored
//! 407.08 MiB for gunnar's client and 88.14 MiB for stock git — 4.62×, and
//! 11.80× on `nornir` alone.
//!
//! # The instrument
//!
//! `git verify-pack -v`. It is the only thing that can see this, and everything
//! here is read out of it: a non-delta entry is `<oid> <type> <size>
//! <size-in-pack> <offset>` and a delta entry appends `<depth> <base-oid>`, so
//! the deltified share is a property of the pack **stock git wrote to its own
//! disk**, not of anything gunnar reports about itself.
//!
//! # Every assertion here is proved red
//!
//! The metric is the same shape as the bug: "0.0%" and "not measured" look
//! alike. So [`the_old_whole_object_writer_is_what_zero_percent_looks_like`]
//! runs the identical object set through `gunnar_pack::write_pack` — the writer
//! that was being called — and requires the counter to come back **0**. If the
//! parser were blind, or if `deltified_share` counted fields wrong, that test
//! would fail with the same number the good one passes with, and one of the two
//! is always lying.
mod common;
use std::path::{Path, PathBuf};
use common::*;
use gunnar_client::{push_over, PushOptions, RefSpec};
/// The one `.pack` in a repository's object store.
fn the_pack(bare: &Path) -> PathBuf {
let dir = bare.join("objects/pack");
let mut packs: Vec<PathBuf> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read {dir:?}: {e}"))
.map(|e| e.expect("dir entry").path())
.filter(|p| p.extension().is_some_and(|x| x == "pack"))
.collect();
packs.sort();
assert_eq!(
packs.len(),
1,
"expected exactly one pack in {dir:?}, found {packs:?}. A push that landed as \
loose objects has no pack to measure, and `receive.unpackLimit` is what \
decides that"
);
packs.pop().expect("one pack")
}
/// `(objects, deltified objects, pack bytes)` as **stock git** reports them.
///
/// `verify-pack -v` also reconstructs and re-hashes every object on its way
/// through, so a pack that gets a number out of this is a pack git accepted.
fn verify_pack(bare: &Path) -> (usize, usize, u64) {
let pack = the_pack(bare);
let bytes = std::fs::metadata(&pack).expect("stat the pack").len();
let out = git(bare, &["verify-pack", "-v", &pack.to_string_lossy()]);
let (mut total, mut deltas) = (0usize, 0usize);
for line in out.lines() {
let f: Vec<&str> = line.split_whitespace().collect();
// `<oid> <type> <size> <size-in-pack> <offset> [<depth> <base-oid>]`.
// Everything else `verify-pack -v` prints — the chain-length summary,
// the "non delta:" line — starts with a word that is not an oid.
if f.len() < 5 || !f[0].chars().all(|c| c.is_ascii_hexdigit()) {
continue;
}
if !matches!(f[1], "blob" | "tree" | "commit" | "tag") {
continue;
}
total += 1;
if f.len() >= 7 {
deltas += 1;
}
}
assert!(
total > 0,
"`git verify-pack -v` named no objects at all in {pack:?}. An empty parse is \
shaped exactly like a pack with no deltas, so this is refused rather than \
reported as 0%"
);
(total, deltas, bytes)
}
fn share(deltas: usize, total: usize) -> f64 {
100.0 * deltas as f64 / total as f64
}
/// A history with real redundancy in it: one file rewritten many times, which is
/// what a delta window is for, plus enough objects that `receive-pack` keeps a
/// pack instead of exploding it into loose objects.
fn build_history(src: &Path) {
let mut body = String::new();
for i in 0..400 {
body.push_str(&format!(
"line {i}: the quick brown fox jumps over the lazy dog\n"
));
}
std::fs::write(src.join("a.txt"), &body).unwrap();
git(src, &["add", "a.txt"]);
git(src, &["commit", "-q", "-m", "seed"]);
for rev in 0..40 {
body.push_str(&format!("revision {rev} appended this line\n"));
std::fs::write(src.join("a.txt"), &body).unwrap();
// A second file so trees change shape too, not only one blob.
std::fs::create_dir_all(src.join("sub")).unwrap();
std::fs::write(src.join(format!("sub/f{rev}.txt")), &body).unwrap();
git(src, &["add", "-A"]);
git(src, &["commit", "-q", "-m", &format!("rev {rev}")]);
}
}
/// Keep the pack. `receive-pack` explodes a push of 100 objects or fewer into
/// loose objects by default, and a repository with no pack cannot be measured.
fn keep_packs(bare: &Path) {
git(bare, &["config", "receive.unpackLimit", "1"]);
git(bare, &["config", "transfer.unpackLimit", "1"]);
}
fn push_with_gunnar(src: &Path, bare: &Path) -> gunnar_client::PushOutcome {
let repo = open(src);
let mut t = GitChild::receive_pack(bare);
let specs = RefSpec::parse_all(&["refs/heads/main:refs/heads/main"]).unwrap();
let outcome = push_over(&repo, &mut t, &specs, &PushOptions::new()).expect("push");
t.finish();
assert!(outcome.is_ok(), "the push was rejected: {outcome:?}");
outcome
}
/// The headline: our push is deltified, and it is in stock git's league.
#[test]
fn a_push_carries_computed_deltas_and_git_accepts_them() {
for kind in BOTH_HASH_KINDS {
let f = Fixture::new(kind);
build_history(&f.src());
keep_packs(&f.bare());
let outcome = push_with_gunnar(&f.src(), &f.bare());
// ── the client's own applied output ─────────────────────────────────
assert!(
outcome.objects_deltified > 0,
"{kind:?}: the client wrote {} objects and computed 0 deltas. That is the \
defect this test exists for, exactly as it shipped",
outcome.objects_sent
);
// ── and the same claim, read back out of stock git's own pack ───────
let (total, deltas, bytes) = verify_pack(&f.bare());
assert!(
deltas > 0,
"{kind:?}: `git verify-pack -v` says {deltas}/{total} of the objects the \
server stored are deltas — 0.0%, which is what the defect measured"
);
// Not a tight bound: it is a floor far below what was measured, so that
// it fails on a regression to whole objects and not on a heuristic
// tweak. Observed on this fixture: see the printout below.
assert!(
share(deltas, total) > 25.0,
"{kind:?}: only {:.1}% of the pack is deltas ({deltas}/{total})",
share(deltas, total)
);
// The client's count and git's must agree about the same pack. They are
// two independent measurements of one artefact; if they disagree, one of
// the two numbers in this file is fiction.
assert_eq!(
outcome.objects_deltified, deltas,
"{kind:?}: the client says it computed {} deltas, `git verify-pack -v` \
counts {deltas} in the pack the server wrote",
outcome.objects_deltified
);
// ── the correctness bar, and it comes first ─────────────────────────
//
// A smaller pack that reconstructs one byte wrong is not an
// improvement, it is a corrupt repository discovered at some later
// clone. `verify-pack -v` above already rebuilt and re-hashed every
// entry; these add the object closure and the connectivity check.
assert_eq!(
git(&f.bare(), &["rev-parse", "refs/heads/main"]),
git(&f.src(), &["rev-parse", "refs/heads/main"]),
"{kind:?}: the pushed tip differs"
);
assert_eq!(
objects_via_git(&f.bare(), "refs/heads/main"),
objects_via_git(&f.src(), "refs/heads/main"),
"{kind:?}: the deltified pack must carry the COMPLETE object closure"
);
let fsck = git_try(&f.bare(), &["fsck", "--full", "--strict", "--no-progress"]);
assert!(fsck.is_ok(), "{kind:?}: git fsck rejected it: {fsck:?}");
println!(
"{kind:?}: {deltas}/{total} deltified ({:.1}%), {bytes} pack bytes",
share(deltas, total)
);
}
}
/// The control. Same history, pushed by **stock git** into its own bare
/// repository, on the same machine in the same run.
///
/// This is the ratio the bench arm reports at corpus scale (4.62× before the
/// fix), reduced to something a unit test can hold. The bound is deliberately
/// loose — git orders objects by type, path and recency and this crate orders by
/// size alone, so parity is not the claim — but a 4× blow-up is not a heuristic
/// difference and must fail here.
#[test]
fn our_pack_is_in_stock_gits_league_not_four_times_it() {
let kind = gix::hash::Kind::Sha1;
let f = Fixture::new(kind);
build_history(&f.src());
keep_packs(&f.bare());
push_with_gunnar(&f.src(), &f.bare());
let (ours_n, ours_d, ours_bytes) = verify_pack(&f.bare());
// A second bare repository, pushed by git itself.
let control = f.dir.path().join("control.git");
init_bare(&control, kind);
keep_packs(&control);
git(
&f.src(),
&[
"push",
"--quiet",
&control.to_string_lossy(),
"refs/heads/main:refs/heads/main",
],
);
let (git_n, git_d, git_bytes) = verify_pack(&control);
assert_eq!(
ours_n, git_n,
"the two pushes did not carry the same object set, so their sizes are not \
comparable"
);
let ratio = ours_bytes as f64 / git_bytes as f64;
println!(
"ours {ours_bytes} B ({:.1}% deltified), git {git_bytes} B ({:.1}% deltified), \
ratio {ratio:.3}x",
share(ours_d, ours_n),
share(git_d, git_n)
);
assert!(
ratio < 2.0,
"our pack is {ratio:.2}x stock git's ({ours_bytes} B vs {git_bytes} B). Before \
the encoder was wired in this was 4.62x across 19 real repositories"
);
}
/// **The negative control**, and the reason the numbers above can be believed.
///
/// The identical object set, written by `gunnar_pack::write_pack` — the writer
/// `drive_push` used to call — must come back at **0 deltas**. If the parser in
/// this file counted fields wrongly, or counted the summary lines, or counted
/// nothing at all, this test would report a number other than zero and the
/// suite would be seen to contradict itself.
#[test]
fn the_old_whole_object_writer_is_what_zero_percent_looks_like() {
let kind = gix::hash::Kind::Sha1;
let f = Fixture::new(kind);
build_history(&f.src());
keep_packs(&f.bare());
// Push with the OLD writer, by driving send-pack with our own pack function.
let repo = open(&f.src());
let mut t = GitChild::receive_pack(&f.bare());
let adv = {
let (r, _) = gunnar_client::Transport::io(&mut t);
gunnar_client::read_advertisement(r).expect("advertisement")
};
let head = git(&f.src(), &["rev-parse", "refs/heads/main"]);
let new = gix::hash::ObjectId::from_hex(head.as_bytes()).unwrap();
let commands = vec![gunnar_client::PushCommand::create("refs/heads/main", new)];
let plan = gunnar_client::objects::plan_objects(&repo, &[new], &[]).expect("plan");
let opts = gunnar_pack::WriteOptions::new(kind);
let report = gunnar_client::send_pack(
&mut t,
&adv,
&commands,
kind,
Some(
|w: &mut dyn std::io::Write| -> Result<(), gunnar_client::Error> {
// The exact call `crates/gunnar-client/src/push.rs:527` used to make.
let outcome = gunnar_pack::write_pack(
&plan.ids,
&gunnar_client::pack_source::PlumbingObjects(&repo),
w,
&opts,
)?;
assert_eq!(
outcome.objects_deltified, 0,
"`write_pack` has no encoder; a non-zero count here means the field is \
being filled in by something other than the code that computed a delta"
);
Ok(())
},
),
&Default::default(),
)
.expect("send-pack");
t.finish();
assert!(report.is_ok(), "the control push was rejected: {report:?}");
let (total, deltas, bytes) = verify_pack(&f.bare());
assert_eq!(
deltas, 0,
"the whole-object writer produced {deltas}/{total} deltas. Either it grew an \
encoder, or `verify_pack` in this file is counting something that is not a \
delta — and in the second case every other number here is worthless"
);
println!("control (write_pack): {total} objects, 0 deltas, {bytes} pack bytes");
}
/// ★ **A PACKED source is sent the way stock git sends it: its deltas COPIED, not recomputed**
/// (turn 2026-10-12: the kit's seed of workspace_nordisk, 78 MB from a mirror, took 23 s
/// optimised and 243 s in a dev build recomputing every delta; `git push` reused them in 7 s).
/// After `git gc` the history lives in one pack with git's own deltas: the push copies them,
/// stock git stores a deltified pack, and the closure and fsck hold.
#[test]
fn a_packed_source_copies_its_deltas_instead_of_recomputing_them() {
for kind in BOTH_HASH_KINDS {
let f = Fixture::new(kind);
build_history(&f.src());
git(&f.src(), &["gc", "-q", "--aggressive", "--prune=now"]);
keep_packs(&f.bare());
let outcome = push_with_gunnar(&f.src(), &f.bare());
assert!(
outcome.objects_copied as u32 == outcome.objects_sent && outcome.objects_sent > 0,
"{kind:?}: a packed source copies every object: {outcome:?}"
);
assert_eq!(
outcome.objects_deltified, 0,
"{kind:?}: no delta was recomputed"
);
let (total, deltas, _) = verify_pack(&f.bare());
assert!(
share(deltas, total) > 25.0,
"{kind:?}: git's deltas survive the copy ({deltas}/{total})"
);
assert_eq!(
objects_via_git(&f.bare(), "refs/heads/main"),
objects_via_git(&f.src(), "refs/heads/main"),
"{kind:?}: the complete closure"
);
assert!(
git_try(&f.bare(), &["fsck", "--full", "--strict", "--no-progress"]).is_ok(),
"{kind:?}: fsck"
);
}
}
/// MEASURE (ignored): `PUSH_MEASURE_SRC=<a packed mirror>` — gunnar's push of every head of it
/// into a fresh bare repository through stock `git receive-pack`, timed against `git push` of
/// the same refs. Run with `--ignored --nocapture`.
#[test]
#[ignore]
fn measure_a_packed_mirror_against_git() {
let Some(src) = std::env::var_os("PUSH_MEASURE_SRC").map(PathBuf::from) else {
return;
};
let kind = gix::open(&src).expect("open the source").object_hash();
let specs_all = RefSpec::parse_all(&["refs/heads/*:refs/heads/*"]);
let tmp = std::env::temp_dir().join(format!("push-measure-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
for (who, bare) in [("gunnar", tmp.join("g.git")), ("git", tmp.join("s.git"))] {
std::fs::create_dir_all(&bare).unwrap();
let fmt = if kind == gix::hash::Kind::Sha1 {
"sha1"
} else {
"sha256"
};
git(
&bare,
&["init", "-q", "--bare", &format!("--object-format={fmt}")],
);
let t = std::time::Instant::now();
if who == "gunnar" {
let repo = open(&src);
let heads: Vec<String> =
git(&src, &["for-each-ref", "--format=%(refname)", "refs/heads"])
.lines()
.map(|r| format!("{r}:{r}"))
.collect();
let specs = RefSpec::parse_all(&heads).unwrap();
let mut tr = GitChild::receive_pack(&bare);
let o = push_over(&repo, &mut tr, &specs, &PushOptions::new()).expect("push");
tr.finish();
println!(
"gunnar: {:?} — {} objects, {} copied, {} computed deltas, {} pack bytes",
t.elapsed(),
o.objects_sent,
o.objects_copied,
o.objects_deltified,
o.pack_bytes
);
} else {
git(
&src,
&[
"push",
"-q",
&bare.to_string_lossy(),
"refs/heads/*:refs/heads/*",
],
);
println!("git: {:?}", t.elapsed());
}
}
let _ = specs_all;
let _ = std::fs::remove_dir_all(&tmp);
}