crates/gunnar-wire/tests/thin_voucher.rs
//! **What a thin fetch tells the engine the client already holds** (lane
//! GUN-PERF-FETCH, 2026-10-31).
//!
//! `GitServe::emit_pack`'s `have` is the voucher a thin delta may name its base
//! against, and both engines close over it: everything reachable from those ids
//! is "held". Handing it the negotiated common **commits** made that closure the
//! client's whole history. MEASURED on oden, git.git (81 188 commits, 1 006
//! refs), one new commit fetched: the gix engine walked every object of the
//! repository to build that set, **about 22 s of the 45 s** the fetch took,
//! for a pack of three objects.
//!
//! git does not do that. `pack-objects --thin` takes its preferred bases from
//! the **edge** — the commits the client holds that sit directly behind one it
//! is being sent (`mark_edges_uninteresting` → `show_edge` →
//! `add_preferred_base`) — and from their trees only, never from their
//! ancestry. So the voucher is now that: the edge commits' root trees and
//! everything under them. It names no commit, so no engine can close it over
//! history, and it still holds the previous version of every path the new
//! commits touch, which is where a pushed delta's base lives.
//!
//! Both selection paths are held to it: the store's own `select` (the
//! in-memory store answers) and the walk ([`common::Unindexed`] declines).
mod common;
use std::collections::BTreeSet;
use std::sync::Mutex;
use gix_hash::{Kind, ObjectId};
use gunnar_store::git_storage_trait::{
Caps, Extent, GcReport, GitOps, GitServe, ObjType, Oid, PackStats, ReachSet, RefCas, RefRow,
RefUpdate, Stored, TxId,
};
use gunnar_wire::upload_pack::FetchRequest;
use gunnar_wire::UploadPack;
use common::{Fixture, Unindexed};
/// A store that records the `have` every `emit_pack` call was handed.
struct Recorder<'a> {
inner: &'a dyn GitServe,
vouched: Mutex<Vec<Vec<Vec<u8>>>>,
}
impl<'a> Recorder<'a> {
fn new(inner: &'a dyn GitServe) -> Self {
Recorder {
inner,
vouched: Mutex::new(Vec::new()),
}
}
/// The one `emit_pack` call's `have`, as ids.
fn only_voucher(&self) -> BTreeSet<ObjectId> {
let calls = self.vouched.lock().expect("recorder lock");
assert_eq!(calls.len(), 1, "one fetch, one emitted pack");
calls[0]
.iter()
.map(|raw| ObjectId::try_from(raw.as_slice()).expect("a well-sized oid"))
.collect()
}
}
impl GitOps for Recorder<'_> {
fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> anyhow::Result<TxId> {
self.inner.put(pack, refs)
}
fn put_pack(&self, bytes: &[u8]) -> anyhow::Result<TxId> {
self.inner.put_pack(bytes)
}
fn put_refs(&self, updates: &[RefUpdate]) -> anyhow::Result<TxId> {
self.inner.put_refs(updates)
}
fn get(&self, oid: Oid<'_>) -> anyhow::Result<Option<Stored>> {
self.inner.get(oid)
}
fn has(&self, oid: Oid<'_>) -> anyhow::Result<bool> {
self.inner.has(oid)
}
fn size(&self, oid: Oid<'_>) -> anyhow::Result<Option<u64>> {
self.inner.size(oid)
}
fn extents(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<Extent>>> {
self.inner.extents(oids)
}
fn refs(&self) -> anyhow::Result<Vec<RefRow>> {
self.inner.refs()
}
fn update_ref(
&self,
name: &str,
old: Option<Oid<'_>>,
new: Option<Oid<'_>>,
) -> anyhow::Result<TxId> {
self.inner.update_ref(name, old, new)
}
fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> anyhow::Result<TxId> {
self.inner.put_refs_cas(edits)
}
fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Vec<Vec<u8>>> {
self.inner.reachable(want, have)
}
fn gc(&self) -> anyhow::Result<GcReport> {
self.inner.gc()
}
}
impl GitServe for Recorder<'_> {
fn read(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, Vec<u8>)>> {
self.inner.read(oid)
}
fn header(&self, oid: Oid<'_>) -> anyhow::Result<Option<(ObjType, u64)>> {
self.inner.header(oid)
}
fn sizes(&self, oids: &[Oid<'_>]) -> anyhow::Result<Vec<Option<u64>>> {
self.inner.sizes(oids)
}
fn head(&self) -> anyhow::Result<Option<RefRow>> {
self.inner.head()
}
fn set_head(&self, target: &str) -> anyhow::Result<TxId> {
self.inner.set_head(target)
}
fn select(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> anyhow::Result<Option<ReachSet>> {
self.inner.select(want, have)
}
fn emit_pack(
&self,
objects: &[Oid<'_>],
have: &[Oid<'_>],
caps: &Caps,
out: &mut dyn std::io::Write,
) -> anyhow::Result<PackStats> {
self.vouched
.lock()
.expect("recorder lock")
.push(have.iter().map(|raw| raw.to_vec()).collect());
self.inner.emit_pack(objects, have, caps, out)
}
}
/// The root tree of `commit` and the one blob under it (every fixture commit
/// carries one file).
fn tree_and_blob(fixture: &Fixture, commit: ObjectId) -> BTreeSet<ObjectId> {
let (_, data) = fixture
.git
.read(commit.as_bytes())
.expect("read a commit")
.expect("the commit is here");
let tree = gix_object::CommitRef::from_bytes(&data, fixture.kind)
.expect("parse a commit")
.tree();
let (_, data) = fixture
.git
.read(tree.as_bytes())
.expect("read a tree")
.expect("the tree is here");
let mut out: BTreeSet<ObjectId> = gix_object::TreeRef::from_bytes(&data, fixture.kind)
.expect("parse a tree")
.entries
.iter()
.map(|entry| entry.oid.to_owned())
.collect();
out.insert(tree);
out
}
/// One new commit on `main`, fetched thin by a client that holds `main`, `side`
/// and the root — three negotiated common commits, one edge.
fn fetch_one_new_commit(fixture: &Fixture, store: &dyn GitServe) -> ObjectId {
let five = fixture.extend(fixture.four, 1, "five");
fixture.force_ref("refs/heads/main", five);
let upload = UploadPack::new(store, fixture.kind);
let mut out = Vec::new();
let outcome = upload
.fetch(
&FetchRequest {
wants: vec![five],
haves: vec![fixture.four, fixture.three, fixture.one],
done: true,
thin_pack: true,
ofs_delta: true,
..Default::default()
},
&mut out,
)
.expect("serve the fetch");
assert_eq!(
outcome.objects_selected, 3,
"one commit, its tree and its blob: the voucher must change how a base is named, \
never what is selected"
);
five
}
fn assert_edge_voucher(fixture: &Fixture, vouched: &BTreeSet<ObjectId>) {
let expected = tree_and_blob(fixture, fixture.four);
for id in vouched {
let (kind, _) = fixture
.git
.read(id.as_bytes())
.expect("read a vouched object")
.expect("every vouched object is in the store");
assert_ne!(
kind,
ObjType::Commit,
"the voucher names commit {id}: an engine closes over it and walks the client's whole \
history to answer which bases a three-object pack may name"
);
}
assert_eq!(
vouched, &expected,
"the voucher is the edge's (`four`'s) root tree and everything under it — git's \
preferred bases — and nothing from the client's other tips"
);
}
#[test]
fn a_thin_fetch_vouches_for_the_edge_tree_and_not_for_the_clients_history() {
let fixture = Fixture::build(Kind::Sha1);
let store = Recorder::new(&fixture.git);
fetch_one_new_commit(&fixture, &store);
assert_edge_voucher(&fixture, &store.only_voucher());
}
#[test]
fn the_walk_hands_the_engine_the_same_edge_voucher() {
let fixture = Fixture::build(Kind::Sha1);
let unindexed = Unindexed(&fixture.git);
let store = Recorder::new(&unindexed);
fetch_one_new_commit(&fixture, &store);
assert_edge_voucher(&fixture, &store.only_voucher());
}
/// Without `thin-pack` nothing may be vouched for: the client did not consent
/// to name bases it holds, so the engine must close the pack over its bases.
#[test]
fn a_fetch_without_thin_pack_vouches_for_nothing() {
let fixture = Fixture::build(Kind::Sha1);
let store = Recorder::new(&fixture.git);
let five = fixture.extend(fixture.four, 1, "five");
fixture.force_ref("refs/heads/main", five);
let upload = UploadPack::new(&store, fixture.kind);
let mut out = Vec::new();
upload
.fetch(
&FetchRequest {
wants: vec![five],
haves: vec![fixture.four],
done: true,
thin_pack: false,
ofs_delta: true,
..Default::default()
},
&mut out,
)
.expect("serve the fetch");
assert!(
store.only_voucher().is_empty(),
"a fetch that did not ask for a thin pack must hand the engine no voucher"
);
}