Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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"
    );
}