crates/storage-git-gix/src/serve.rs
//! **`GitServe` over `gix`** — the reading contract's gix arm.
//!
//! `GitOps` is the storage contract and it provably cannot serve the wire:
//! `Stored::bytes` is *"the pack entry, header and all, byte for byte as the
//! client sent them"* — possibly a delta — and none of the eleven returns an
//! inflated object. This module is the other half: [`GitServe::read`] and
//! [`GitServe::header`] for the two consumers that decode
//! (`gunnar-wire`'s `Graph::load`, `gunnar-policy`'s ancestry walk and signature
//! gate), and [`GitServe::emit_pack`] / [`GitServe::select`] for the serving
//! half.
//!
//! # The ordering rule, honoured
//!
//! `GitServe` is defined by what the **Arrow arm** needs to serve a clone well;
//! this arm then implements it. Nothing here asked for a method, and nothing
//! here reads a `.bitmap`. Where the two arms answer the same question by
//! different means the *semantics* are matched deliberately, method by method,
//! against `znippy-plugin-git/src/serve.rs` — the notes below say where and why.
//!
//! # `emit_pack` copies. It does not re-deflate, and it does not walk.
//!
//! Two separate properties, and both are the kind that no exit code can see:
//!
//! * **A copy, not a re-deflate.** A naive emitter inflates every object in
//! order to deflate it again; the result passes `git index-pack --strict`
//! *and* `git fsck` while sending a measured **18.4×** the wire bytes. That is
//! why [`PackStats`] carries `copied` and `recompressed` and why this module
//! counts them off what happened rather than asserting them. Every payload
//! here is a slice of a mapped `.pack` written straight to `out`. The only
//! thing rewritten is an `OFS_DELTA`'s header, and only because it carries a
//! distance relative to a position in the *input* pack.
//! * **An exact set, not a closure.** `emit_pack`'s first parameter is
//! `objects`, and it is *"a set to emit, not a set of tips to close over"*.
//! Upload-pack's caller holds a selection that is already post-filter,
//! post-shallow and post-`include-tag`, and deliberately **not** closed. An
//! engine that closes over it adds back exactly what `--filter=blob:none` or
//! `--depth=N` excluded — an over-send that passes every guard we own. This
//! module walks no graph on that path. [`GitServe::select`] owns the
//! close-over-tips half; `emit_pack` does not.
//!
//! **There is no delta-base closure either, and that is the 2026-08-14 fix.**
//! This module used to add a delta's base when the base fell outside the
//! request, on the reasoning that an `OFS_DELTA` names its base by in-pack
//! distance and so cannot be encoded without it. That reasoning is correct about
//! the *pack format* and wrong about the *clone*:
//!
//! * a base pulled in is an object the client did not ask for, and
//! * if it is a **tree**, it arrives owing children the pack does not contain.
//!
//! `git index-pack --check-self-contained-and-connected` — what `git clone`
//! runs — walks every received object's links and then demands each one exist,
//! so the clone dies with `fatal: did not receive expected object <oid>` while
//! this server logs a successful `upload-pack`. Every store that has ever
//! refused a ref update holds unreachable objects (a lost compare-and-swap, a
//! reset branch, a `git fast-import`), and `pack-objects` stores the newest
//! version of a path whole and the older ones as deltas against it — so after a
//! reset the object that is still reachable is routinely a delta against one
//! that is not. That is the whole of the intermittent `gunnar.year` failure and
//! the deterministic `gunnar.import_export` one; a different oid each run
//! because which reachable object hangs off an unreachable base is a property of
//! how that week's push happened to deltify. The identical defect was found and
//! fixed on the Arrow arm on 2026-08-11.
//!
//! So `stats.objects` is exactly `objects.len()`, deduplicated, and the base
//! decides how an entry is **encoded** rather than what the pack contains — base
//! inside the request, copy the stored bytes; base outside it, rebuild the
//! object whole and count it in `recompressed`. That is stock `pack-objects`'
//! rule. `recompressed` is 0 for a whole-repository clone of a store with no
//! unreachable history, because such a request contains every base, and both
//! numbers are counted off what happened rather than asserted.
//!
//! # No rayon, no pool, no fan-out — `P-004`
//!
//! *"Pure `std::thread::scope`; no global pool, no rayon."* Nothing here spawns
//! a thread at all: the serving tier stays serial by decision, concurrency comes
//! from serving many requests, and parallelising one clone at saturation is the
//! same work rearranged. What is emphatically **not** here is gix's pack
//! pipeline — no counting pass with a serial reduce, no `sort_by` over counts,
//! no `BTreeMap` reorder, no hashing of every byte on a consuming thread. That
//! is `P-018`'s serial tail, and the reason none of it is needed is that the
//! bytes on disk are already deflated and already delta-encoded, so emitting is
//! a copy and an addition.
//!
//! # Blocking
//!
//! Every method blocks. Never call one from an async task without
//! `tokio::task::spawn_blocking`.
use std::collections::{HashMap, HashSet};
use anyhow::{anyhow, bail, Context, Result};
use git_storage_trait::{
Caps, GitOps, GitServe, ObjType, Oid, PackStats, ReachSet, RefRow, RefUpdate, TxId,
};
use gix_hash::ObjectId;
use crate::packs::{BaseRef, EmitParts, Pack, PackSet};
use crate::{refs, GixStore};
/// `HEAD`, which is **not** a row in [`GitOps::refs`] — see the accessor pair
/// below.
const HEAD: &str = "HEAD";
/// gix's decoded object kind, as the contract's type.
///
/// Total, and it cannot produce `OfsDelta` or `RefDelta` by construction:
/// `gix_object::Kind` has four variants and none of them is a delta, which is
/// exactly the *"resolved kind"* guarantee [`GitServe::read`] and
/// [`GitServe::header`] both make. Writing it as a match rather than as a cast
/// is what makes that a compiler-checked claim.
fn resolved_type(kind: gix_object::Kind) -> ObjType {
match kind {
gix_object::Kind::Commit => ObjType::Commit,
gix_object::Kind::Tree => ObjType::Tree,
gix_object::Kind::Blob => ObjType::Blob,
gix_object::Kind::Tag => ObjType::Tag,
}
}
// ── the emitter ──────────────────────────────────────────────────────────────
/// One entry the emitter has decided to write.
///
/// Borrows its pack's mmap for the length of one `emit_pack`, which is why the
/// pack-set read guard is taken once at the top and held: `parts.payload` is a
/// slice of a mapped `.pack`, and nothing between here and `out` copies it.
struct Planned<'a> {
oid: ObjectId,
body: Body<'a>,
/// **What this entry names as its delta base, and where that base lives.**
/// See [`DeltaBase`]. The base decides how an entry is *encoded*, never
/// whether the object id is named by an offset — the offset would be the
/// obvious key and it is the wrong one. An object can sit in two packs at
/// once in this store *by construction*: `index_thin_pack` copies a push's
/// delta bases into the new pack so that what lands on disk is
/// self-contained. So the same object is reachable as `(pack A, offset x)`
/// and as `(pack B, offset y)`, and keying on the site would emit it twice —
/// a duplicate oid, which `git index-pack` refuses.
///
/// Keying on the id is also *correct* rather than merely deduplicating: git
/// resolves an `OFS_DELTA` by decoding whatever entry starts at
/// `here - distance`, recursively, so any entry that decodes to the base
/// object will do. It does not have to be the same representation the delta
/// was originally written against.
delta_base: DeltaBase,
}
/// Where the base a [`Planned`] delta names lives, which is what decides the
/// spelling [`GixStore::write_emission`] emits — a property of the *request*,
/// not of what happens to be on disk.
///
/// The clone path and the fetch path route through the same three cases, so
/// there is one copy-eligibility decision and not two that can drift (LAW 5):
/// the only fetch-specific input is `External`, and it is reached exactly when
/// the client vouched for the base.
enum DeltaBase {
/// This entry names nothing: a non-delta, a rebuilt entry, or a stored
/// `REF_DELTA` left **thin** because its base is not in this store and the
/// client advertised `thin-pack`. The stored bytes go out header and all.
None,
/// The base travels **in this pack**, for either stored spelling. It is
/// ordered before its delta and re-headed into whichever spelling the
/// client can read (`OFS_DELTA` by position, or `REF_DELTA` by id).
InPack(ObjectId),
/// The base does **not** travel in this pack, but the client already holds
/// it — it is reachable from the negotiated common tips, and `thin-pack`
/// was advertised. The stored delta stream goes out verbatim under a
/// `REF_DELTA` header naming the base by id; the client resolves it out of
/// its own object database. This is the fetch-path twin of [`Self::InPack`]:
/// a boundary delta the clone path would copy against an in-pack base is
/// copied here against a base the receiver vouched for, instead of being
/// rebuilt whole and paying deflate CPU for bytes the client already has.
External(ObjectId),
}
/// The bytes one entry contributes, and it is one of exactly two things.
///
/// The distinction is the whole of the 2026-08-14 fix and it is the same rule
/// stock `pack-objects` follows: a stored delta is reused **only when its base
/// is also being packed**. The base decides how an entry is *encoded*; it never
/// decides what the pack *contains*.
enum Body<'a> {
/// The stored entry, out of the mapped `.pack`. Its header may be re-encoded
/// to name the base in the spelling this client reads; the deflate stream is
/// never touched. This is the overwhelming majority of every request and the
/// reason a clone costs no CPU.
Copied(EmitParts<'a>),
/// **Rebuilt whole**, because the stored entry is a delta against an object
/// the request does not carry.
///
/// Adding the base instead is what shipped broken clones: it satisfies the
/// pack *format* and violates *connectivity*, because an out-of-set base
/// that is a tree arrives owing children the pack does not contain, and
/// `git index-pack --check-self-contained-and-connected` — which is what a
/// clone runs — then dies with `did not receive expected object`. See
/// [`GixStore::plan_emission`].
///
/// `size` is the **resolved** object size, because this is no longer a delta
/// entry and a non-delta header states the inflated length.
Rebuilt {
obj_type: ObjType,
size: u64,
deflated: Vec<u8>,
},
/// A [`Body::Rebuilt`] that [`GixStore::rebuild_bodies`] has not filled in
/// yet, and which [`GixStore::write_emission`] **refuses**.
///
/// It exists so that a fan-out which dropped a unit cannot be a quiet
/// under-send. The alternative — an empty `Rebuilt` placeholder — is a
/// zero-length deflate stream, which is a pack `git index-pack` rejects
/// somewhere far away with a message about the wrong object, if it rejects
/// it at all. This one names the entry and the stage.
PendingRebuild,
}
impl Body<'_> {
/// The placeholder [`GixStore::plan_emission`] parks in the plan while the
/// rebuild is deferred to the fan-out.
fn pending_rebuild() -> Self {
Body::PendingRebuild
}
}
/// Deferred rebuilds below which [`GixStore::rebuild_bodies`] does **not** fan
/// out.
///
/// **Eight, and the number is measured rather than picked.** MEASURED on oden
/// 2026-08-14, a whole-repository clone of the `nornir` mirror through this arm:
/// 12 216 objects asked for, of which **55 were rebuilds**, and `perf` put
/// `zlib_rs::deflate::*` at 28.5 % and `zlib_rs::inflate::*` at 7.7 % of the
/// request's ~100 ms of CPU. That is **~0.6 ms per rebuilt object** against a
/// `gatling` worker's ~10–20 µs to spawn: four spawns cost 3 % of *one* unit, so
/// the crossover is a handful of units and not a large batch.
///
/// This is deliberately nothing like the 4096 `znippy-plugin-git`'s emit phase 1
/// uses. Its unit is a bounds check and a varint and it needs thousands of them
/// to be worth a thread; this unit is an inflate, a delta-chain walk and a whole
/// deflate. **A threshold copied from there would have been wrong here** — 64
/// was the first value tried and this clone's 55 rebuilds fell under it, so the
/// fan-out never ran on the workload it was written for.
///
/// Note what it counts: **rebuilds**, not requested objects. A whole-repository
/// clone of a store with no unreachable history rebuilds nothing at all and
/// never reaches this test — which is why a threshold on `asked.len()` would
/// have been the wrong measurement of the same thing.
const FAN_OUT_AT: usize = 8;
/// Ceiling on rebuild workers **per request**.
///
/// The serving tier admits many concurrent transfers and the fan-out primitive
/// has no reentrancy detection, so an uncapped fan here is `admitted × ncores`
/// threads. Four leaves the admission gate in charge of the machine, and is the
/// same bound and the same argument as `znippy-plugin-git`'s emit phase 1.
const MAX_REBUILD_WORKERS: usize = 4;
/// Workers for the rebuild fan: [`MAX_REBUILD_WORKERS`], or `GIX_EMIT_WORKERS`.
///
/// **An operator knob, not an arm.** It selects no implementation and changes no
/// byte of the emitted pack — the fan writes each result back into the plan slot
/// it came from, and `write_emission` walks the plan in one serial order however
/// many workers filled it. `0` is read as "as many as this box has cores", which
/// is `gatling_for_each`'s own convention.
fn rebuild_workers() -> usize {
match std::env::var("GIX_EMIT_WORKERS") {
Ok(v) => v.trim().parse::<usize>().unwrap_or(MAX_REBUILD_WORKERS),
Err(_) => MAX_REBUILD_WORKERS,
}
}
/// Order `plan` so that every delta follows the base it names.
///
/// An `OFS_DELTA` names its base by a **backwards** distance, so a base that has
/// not been written yet has no distance to name: an order that puts a delta
/// first is not merely inefficient, it cannot be encoded at all.
///
/// A `REF_DELTA` names its base by id and imposes no order of its own — but a
/// stored `REF_DELTA` whose base is here is emitted as an `OFS_DELTA` whenever
/// the client can read one, so it is ordered on exactly the same rule. Ordering
/// it when the client turns out to lack `ofs-delta` costs nothing: a correct
/// order is still a correct order.
///
/// Iterative rather than recursive — a delta chain is allowed to be 50 deep by
/// default and nothing forbids a pathological one, so the depth belongs on the
/// heap where it cannot take the thread's stack with it. Returns the ordered
/// entries and any base that was named but is not in the set; a caller that gets
/// a non-empty second value has a bug rather than a thin pack, because a base
/// that is deliberately left external is recorded as `None` and never named
/// here.
fn topological_order(plan: Vec<Planned<'_>>) -> (Vec<Planned<'_>>, Vec<ObjectId>) {
let present: HashMap<ObjectId, usize> =
plan.iter().enumerate().map(|(i, p)| (p.oid, i)).collect();
let mut missing = Vec::new();
let mut done: HashSet<usize> = HashSet::with_capacity(plan.len());
let mut order: Vec<usize> = Vec::with_capacity(plan.len());
for start in 0..plan.len() {
if done.contains(&start) {
continue;
}
let mut stack = vec![start];
let mut on_path: HashSet<usize> = HashSet::new();
while let Some(&i) = stack.last() {
if done.contains(&i) {
stack.pop();
continue;
}
let pending = match plan[i].delta_base {
// Nothing to order against: a non-delta, a rebuilt entry, or a
// thin entry whose base is external — [`DeltaBase::External`]
// names a base this pack does not carry (the client resolves it),
// so it is deliberately NOT a `missing` base and imposes no order.
DeltaBase::None | DeltaBase::External(_) => None,
DeltaBase::InPack(base) => match present.get(&base) {
Some(&b) if !done.contains(&b) => {
// A cycle is impossible in a well-formed pack — a base
// always precedes its delta, so offsets strictly
// decrease — but a corrupt one could claim otherwise,
// and looping for ever is a worse answer than emitting
// in an order the encoder then refuses.
if on_path.contains(&b) {
None
} else {
Some(b)
}
}
Some(_) => None,
None => {
missing.push(base);
None
}
},
};
match pending {
Some(b) => {
on_path.insert(i);
stack.push(b);
}
None => {
stack.pop();
on_path.remove(&i);
done.insert(i);
order.push(i);
}
}
}
}
// Permute in place rather than cloning: `Planned` borrows the mmap, so a
// clone would be cheap, but building the output by index keeps the move
// obvious and does not need `Planned: Clone`.
let mut slots: Vec<Option<Planned<'_>>> = plan.into_iter().map(Some).collect();
let ordered = order.into_iter().filter_map(|i| slots[i].take()).collect();
(ordered, missing)
}
/// The type/size varint — byte 0 is `[continue:1][type:3][size low nibble:4]`,
/// each continuation carries 7 more size bits, least significant group first.
///
/// Written out rather than borrowed from gix because gix's pack **writer** is
/// the pipeline this module exists to not use. It is checked against gix's own
/// reader in `tests/`: everything this encodes is read back by
/// `gix_pack::data::File::entry`.
fn encode_type_and_size(out: &mut Vec<u8>, obj_type: ObjType, size: u64) {
let mut byte = (obj_type.code() << 4) | ((size & 0x0f) as u8);
let mut rest = size >> 4;
while rest > 0 {
out.push(byte | 0x80);
byte = (rest & 0x7f) as u8;
rest >>= 7;
}
out.push(byte);
}
/// An `OFS_DELTA`'s backwards distance.
///
/// **Not the same varint as the one above**, and that is the whole reason it is
/// spelled out separately: each continuation subtracts one before shifting, so
/// the encoding has no redundant representations. Off by one here produces a
/// pack `git index-pack --strict` rejects — or worse, one it accepts and
/// misreads, because the delta would be applied against the wrong base. Emitted
/// most-significant group first, which is why it is built backwards.
fn encode_ofs_distance(out: &mut Vec<u8>, distance: u64) {
let mut buf = [0u8; 10];
let mut i = buf.len() - 1;
let mut d = distance;
buf[i] = (d & 0x7f) as u8;
while d >= 0x80 {
d >>= 7;
d -= 1;
i -= 1;
buf[i] = 0x80 | (d & 0x7f) as u8;
}
out.extend_from_slice(&buf[i..]);
}
/// `out`, plus the running pack checksum and the byte count `OFS_DELTA`
/// distances are measured against.
///
/// The two facts a `Vec<u8>` accumulator would really be kept for, held as three
/// fields instead — so a clone **streams** and nothing sizes an allocation by
/// the repository. Serving a 2 GiB clone must not mean holding 2 GiB.
struct Trailing<'a> {
inner: &'a mut dyn std::io::Write,
hasher: gix_hash::Hasher,
written: u64,
}
impl<'a> Trailing<'a> {
fn new(inner: &'a mut dyn std::io::Write, hash: gix_hash::Kind) -> Self {
Trailing {
inner,
hasher: gix_hash::hasher(hash),
written: 0,
}
}
/// Bytes written so far — the output coordinate an `OFS_DELTA` distance is
/// relative to.
fn written(&self) -> u64 {
self.written
}
fn put(&mut self, bytes: &[u8]) -> Result<()> {
self.inner.write_all(bytes).context("write pack bytes")?;
self.hasher.update(bytes);
self.written += bytes.len() as u64;
Ok(())
}
/// The trailer is over everything written, which is what a reader checks.
fn finish(self) -> Result<u64> {
let id = self
.hasher
.try_finalize()
.context("finalise the pack trailer")?;
self.inner
.write_all(id.as_slice())
.context("write pack trailer")?;
self.inner.flush().context("flush pack")?;
Ok(self.written + id.as_slice().len() as u64)
}
}
impl GixStore {
/// **Emit a packfile whose objects are exactly `oids`**, deduplicated, and
/// closed over nothing at all.
///
/// Inherent as well as on the trait for the same reason the Arrow arm's
/// `emit_set` is: a caller that [`GitServe::select`] told to *"walk it
/// yourself"* hands the walked set straight back here, and it should not
/// have to name a trait to do it.
///
/// # Three steps
///
/// 1. **Decide copy or rebuild, per entry**, on whether the base a stored
/// delta names is in the request. The set is never widened —
/// [`GixStore::plan_emission`] has the 🔴 note on what widening it cost.
/// 2. **Order topologically**, because backwards distances require every base
/// to precede the delta naming it.
/// 3. **Write.** A copied delta's *payload* goes out byte for byte and only
/// its **header** is re-encoded, to name its base in the spelling this
/// client can read — see [`GixStore::write_emission`].
///
/// `copied + recompressed == objects`, counted rather than assumed, and
/// `recompressed` is 0 for a whole-repository clone of a store with no
/// unreachable history, because such a request contains every base.
/// Re-heading a delta does not touch the deflate stream, so it costs bytes
/// on the wire and no CPU at all.
pub fn emit_oids(
&self,
oids: &[Oid<'_>],
caps: &Caps,
out: &mut dyn std::io::Write,
) -> Result<PackStats> {
self.emit_oids_with(oids, caps, out, None)
}
/// [`GixStore::emit_oids`], with the rebuild fan-out's worker count **named**
/// instead of decided by [`FAN_OUT_AT`] / [`rebuild_workers`].
///
/// One body, not two (LAW 5): `emit_oids` is this with `None`, so the tested
/// path and the shipped path are the same code. It exists because the
/// production threshold is 64 rebuilds and the fixtures in this file rebuild
/// one or two — a guard going through the front door would exercise the
/// serial arm every time and the gatling arm never, which is a guard that
/// cannot fail. `the_fan_out_rebuilds_the_identical_pack_the_serial_pass_does`
/// calls it with `Some(1)` and `Some(4)` over one set and requires the two
/// packs byte for byte.
///
/// It takes a count rather than an environment variable on purpose: `cargo
/// test` runs these in threads of one process, and a guard that has to set a
/// process-global to choose its arm is a guard that races its neighbours.
pub fn emit_oids_with(
&self,
oids: &[Oid<'_>],
caps: &Caps,
out: &mut dyn std::io::Write,
workers: Option<usize>,
) -> Result<PackStats> {
// No `held`: an inherent caller has no negotiated `have` closure to
// vouch a boundary base with, so a cut delta chain rebuilds whole
// exactly as a clone's does. `GitServe::emit_pack` is the one path that
// has the closure and passes it — see [`GixStore::emit_core`].
self.emit_core(oids, caps, out, workers, None)
}
/// The shared body of every emission (LAW 5): [`GixStore::emit_oids_with`]
/// passes `held = None`; [`GitServe::emit_pack`] passes the closure of the
/// client's negotiated `have` tips, so a boundary delta whose base the
/// client already holds is copied verbatim as a thin `REF_DELTA` rather than
/// rebuilt whole. The copy-versus-rebuild decision lives in one place —
/// [`GixStore::plan_emission`] — so clone and fetch cannot disagree about
/// what is copyable.
fn emit_core(
&self,
oids: &[Oid<'_>],
caps: &Caps,
out: &mut dyn std::io::Write,
workers: Option<usize>,
held: Option<&HashSet<ObjectId>>,
) -> Result<PackStats> {
// One guard for the whole emission. Every `payload` below is a slice of
// a pack mapped under it, so it must outlive the last `write_all`, and a
// `reload_packs` landing mid-clone must not swap the mmap out from under
// a half-written pack.
let packs = self
.packs
.read()
.map_err(|_| anyhow!("pack set lock poisoned"))?;
let plan = self.plan_emission(&packs, oids, caps, workers, held)?;
let (ordered, missing) = topological_order(plan);
if !missing.is_empty() {
bail!(
"the emit set is not closed: {} delta base(s) absent, first {} — refusing to \
emit a pack whose closure does not hold",
missing.len(),
missing[0]
);
}
self.write_emission(&ordered, caps, out)
}
/// Step 1: turn `oids` into entries. **Nothing is added.**
///
/// The set that comes out is the set that went in, deduplicated. Nothing
/// walks a commit, a tree or a tag, there is no call into [`crate::graph`] on
/// this path, and — since 2026-08-14 — a delta base outside the request is
/// not pulled in either.
///
/// # 🔴 It used to close over the delta bases, and that shipped broken clones
///
/// See the module header for the mechanism and for the two bench arms it was
/// measured on. In one line: an out-of-set base that happens to be a tree
/// arrives owing children the pack does not contain, and the clone's own
/// receiver refuses the pack by naming one of them.
///
/// # What decides copy versus rebuild
///
/// For each requested object, exactly one of:
///
/// * **not a delta** → the stored entry goes out header and all;
/// * **base inside the request** → the stored entry is copied and only its
/// *header* is re-encoded, into `OFS_DELTA` or `REF_DELTA` as the client
/// can read. Re-heading a stored `REF_DELTA` is not an optimisation:
/// `gix_pack::index::write` refuses *every* `REF_DELTA` outright — *"Ref
/// delta objects are not supported as there is no way to look them up"* —
/// whether or not the base is right there in the pack, so a clone that
/// copied `--fix-thin`'s ref-deltas verbatim served stock git and not
/// gitoxide;
/// * **base outside the request, but in this store** → [`Body::Rebuilt`].
/// The object is inflated, resolved through its chain and deflated whole.
/// This is the only thing on the serving path that costs CPU, it fires
/// only where a request cuts a delta chain, and it is counted in
/// `recompressed` rather than being hidden;
/// * **base outside this store entirely** → a genuinely thin entry. There is
/// nothing to rebuild *from*, so the client must supply the base: kept
/// verbatim when it advertised `thin-pack`, refused by name when it did
/// not.
///
/// A rebuilt entry can itself be some other requested entry's base, which is
/// why [`GixStore::write_emission`] records the output position of every
/// entry and not only of the copied ones.
fn plan_emission<'p>(
&self,
packs: &'p PackSet,
oids: &[Oid<'_>],
caps: &Caps,
workers: Option<usize>,
held: Option<&HashSet<ObjectId>>,
) -> Result<Vec<Planned<'p>>> {
// The request, deduplicated, in the caller's order. A repeated oid must
// not become a repeated entry — `git index-pack` refuses a duplicate —
// and the order is kept because a stable, explicable one makes two runs
// byte-identical, which is worth having for free. Nothing depends on it:
// the emission is ordered by `topological_order`.
let mut requested: HashSet<ObjectId> = HashSet::with_capacity(oids.len());
let mut asked: Vec<ObjectId> = Vec::with_capacity(oids.len());
for raw in oids {
let id = self.oid(raw)?;
if requested.insert(id) {
asked.push(id);
}
}
let mut plan: Vec<Planned<'p>> = Vec::with_capacity(asked.len());
// Built on demand, once per pack that actually carries an `OFS_DELTA`
// whose base has to be named. See [`Pack::offset_to_oid`].
let mut reverse: HashMap<u64, HashMap<u64, ObjectId>> = HashMap::new();
// `(index into plan, the entry, the base it names)` for every entry that
// has to be rebuilt whole. Collected here and resolved by
// [`GixStore::rebuild_bodies`] after this loop, because the rebuild is
// the expensive half and it has no ordering dependency at all.
let mut rebuilds: Vec<(usize, ObjectId, ObjectId)> = Vec::new();
for id in asked {
let (pack, offset) = self.locate_for_emission(packs, &id)?;
let parts = pack.emit_parts_at(offset).ok_or_else(|| {
anyhow!(
"the entry for {id} at offset {offset} of {} could not be read; its pack \
disagrees with its index and a verbatim copy of it would put a truncated \
deflate stream on the wire",
pack.data_path.display()
)
})?;
// Which object this entry names as its base, in the one coordinate
// system the decision below is taken in. An `OFS_DELTA` knows only
// where its base sits, so the pack's own index gives it a name; a
// `REF_DELTA` carries the name already.
let named: Option<ObjectId> = match parts.base {
BaseRef::None => None,
BaseRef::Ofs(base_offset) => {
let map = reverse
.entry(pack.id)
.or_insert_with(|| pack.offset_to_oid());
Some(*map.get(&base_offset).ok_or_else(|| {
anyhow!(
"{id} is an ofs-delta naming a base at offset {base_offset} of {}, \
which is not an entry that pack's index knows about — the pack is \
corrupt",
pack.data_path.display()
)
})?)
}
BaseRef::Ref(base) => Some(base),
};
let (body, delta_base) = match named {
// Not a delta: the stored bytes go out untouched.
None => (Body::Copied(parts), DeltaBase::None),
// The base travels with it, so it can be named by position.
Some(base) if requested.contains(&base) => {
(Body::Copied(parts), DeltaBase::InPack(base))
}
// The base does not travel with it. This is the whole fix.
Some(base) => {
// **The client already holds this base.** It is not in the
// request because the negotiation put it on the client's
// side of the cut, and `held` is the closure of the common
// tips — so this is exactly the fetch-path twin of the
// in-pack copy above: the stored delta stream goes out
// verbatim under a `REF_DELTA` naming the base by id, and
// the client resolves it out of its own object database. No
// inflate, no deflate. Whether this store *also* holds the
// base is irrelevant — thinness is a property of the emitted
// pack, not of the server — so this is decided before the
// `find_entry` rebuild path and rebuilds nothing the client
// could name for itself. Reached only with `caps.thin`,
// because `held` is only the true client set then (the
// caller withholds it for a shallow, filtered or
// no-common-history request, where closing over the tips
// would vouch for objects the receiver may never have seen).
if held.is_some_and(|h| h.contains(&base)) {
(Body::Copied(parts), DeltaBase::External(base))
} else if packs.find_entry(&base).is_some() {
// Here, but not asked for and not vouched for. Rebuilding
// the object is the only answer that is both encodable and
// connected; adding the base is what shipped broken clones.
//
// **Deferred, not done here** — see
// [`GixStore::rebuild_bodies`]. The rebuild is an
// inflate, a delta-chain walk and a whole-object
// deflate, and it is the only thing on this path that
// costs CPU; leaving it in this loop is what made a full
// clone serial. The placeholder carries the base so the
// fan-out can name it, and it is replaced before this
// function returns.
rebuilds.push((plan.len(), id, base));
(Body::pending_rebuild(), DeltaBase::None)
} else if caps.thin {
// Genuinely external — this store does not hold the base
// at all, so there is nothing to rebuild from and no
// position to name it by. The entry keeps its stored
// `REF_DELTA` header, base id and all, and the client
// resolves it out of its own object database.
(Body::Copied(parts), DeltaBase::None)
} else {
bail!(
"{id} is a ref-delta against {base}, which this store does not hold, \
and this client did not advertise thin-pack support; emitting it \
anyway would produce a pack the client cannot resolve"
);
}
}
};
plan.push(Planned {
oid: id,
body,
delta_base,
});
}
self.rebuild_bodies(&mut plan, &rebuilds, workers)?;
Ok(plan)
}
/// **Fill in every deferred [`Body::Rebuilt`], across `gatling` workers.**
///
/// # Why this one and not the addressing pass beside it
///
/// PROFILED on oden 2026-08-14, `bench/examples/stage-probe fetch` with
/// `GUNNAR_ENGINE=gix` over the `nornir` mirror (12 216 objects, whole-repo
/// clone, `perf record -F 9999`, 102 ms of process CPU):
///
/// | what | share |
/// |---|---|
/// | `zlib_rs::deflate::*` | **28.5 %** |
/// | `sha1_checked::compress` + `Sha1::update` (the pack trailer) | 24.1 % |
/// | `zlib_rs::inflate::*` | 7.7 % |
/// | `HashMap` lookups on `&oid` / `&usize` | 6.0 % |
/// | `gix_pack::data::Entry::from_bytes` + `index::access::lookup` | **4.2 %** |
///
/// The obvious target — the per-entry addressing that
/// [`GixStore::locate_for_emission`] and `Pack::emit_parts_at` do, two
/// binary searches over a mapped `.idx` — is that last row: **4.2 %**.
/// Fanning it would have been a fan-out over a bounds check. The 36 % that
/// is inflate + deflate is all *here*, in the entries whose stored delta
/// names a base the request does not carry, and it was sitting in the middle
/// of `plan_emission`'s serial `for id in asked` loop one object at a time.
///
/// # Why it is safe to fan, stated as a property and not as an intention
///
/// * The `.pack` mmaps are held open by the caller's `packs.read()` guard
/// for the whole emission, and nothing in this crate writes into a mapped
/// pack — `put_pack` writes a **new** file and reloads the set. So the
/// bytes every worker reads are immutable for the life of the call and
/// there is no lock here at all.
/// * Each unit is one object: [`GixStore::resolved_for_rebuild`] (which goes
/// through the odb pool, itself already `Sync`) and a deflate of the
/// result. No unit reads another unit's output, and none of them touches
/// `plan` except the slot it owns.
/// * The **output is positional**: `gatling_for_each` returns results in
/// index order, and each result is written back into the `plan` index it
/// came from. A pack emitted at one worker and at sixteen is therefore
/// byte-identical — the worker count decides nothing but who did the
/// deflate.
///
/// # The two bounds, for the reason `znippy-plugin-git` states them
///
/// The constellation's fan-out primitive has no reentrancy detection, and
/// the serving tier admits many transfers at once, so an uncapped fan here
/// is `admitted × ncores` threads. [`MAX_REBUILD_WORKERS`] caps it at four;
/// [`FAN_OUT_AT`] means a request with only a handful of rebuilds runs on
/// the calling thread exactly as it did before, because a thread spawn is
/// ~10–20 µs and a request that rebuilds three objects should not pay four
/// of them.
fn rebuild_bodies(
&self,
plan: &mut [Planned<'_>],
rebuilds: &[(usize, ObjectId, ObjectId)],
workers: Option<usize>,
) -> Result<()> {
// `Some(n)` is a caller naming the count — see
// [`GixStore::emit_oids_with`]. `None` is the shipped policy: fan only
// once the rebuilds are worth the spawns.
let workers = workers.unwrap_or({
if rebuilds.len() < FAN_OUT_AT {
1
} else {
rebuild_workers()
}
});
if rebuilds.is_empty() {
return Ok(());
}
let one = |i: usize| -> Result<(usize, ObjType, u64, Vec<u8>)> {
let (slot, id, base) = &rebuilds[i];
let (obj_type, data) = self.resolved_for_rebuild(id, base)?;
let size = data.len() as u64;
Ok((*slot, obj_type, size, deflate(&data)?))
};
let done: Vec<(usize, ObjType, u64, Vec<u8>)> = if workers <= 1 {
(0..rebuilds.len()).map(one).collect::<Result<Vec<_>>>()?
} else {
gatling::gatling_forkjoin::gatling_for_each(rebuilds.len(), workers, one)
.into_iter()
.collect::<Result<Vec<_>>>()?
};
for (slot, obj_type, size, deflated) in done {
plan[slot].body = Body::Rebuilt {
obj_type,
size,
deflated,
};
}
Ok(())
}
/// The resolved object behind a boundary entry, for [`Body::Rebuilt`].
///
/// [`GitServe::read`] is the one reader in this crate that walks a delta
/// chain, so it is what this uses (LAW 5) rather than a second resolver. It
/// goes through the odb pool's retry loop, which matters here: the base of a
/// cut chain is exactly the kind of object that can sit in a pack written
/// after this handle's snapshot.
///
/// A `None` is impossible — the entry was located in a pack a moment ago —
/// so it is reported as the store contradicting itself rather than turned
/// into a skipped object, which would be the same silent under-send in a
/// different hat.
fn resolved_for_rebuild(&self, id: &ObjectId, base: &ObjectId) -> Result<(ObjType, Vec<u8>)> {
GitServe::read(self, id.as_slice())
.with_context(|| {
format!(
"{id} is a delta against {base}, which this request does not carry, so it \
has to be rebuilt whole"
)
})?
.ok_or_else(|| {
anyhow!(
"{id} has a pack entry in this store and yet resolves to nothing; it cannot \
be rebuilt and its stored delta names {base}, which this request does not \
carry"
)
})
}
/// Find the entry to copy for `id`, or say precisely why there is none.
///
/// The two failures are different and a caller can act on only one of them,
/// so they are not collapsed into "not found":
///
/// * **Loose.** A loose object has no pack entry, so there are no verbatim
/// bytes to copy. This backend refuses rather than falling back to a
/// whole-object writer, which would inflate and re-deflate — `recompressed`
/// would stop being 0 and the `P-001` receipt would stop meaning anything.
/// Nothing this crate writes is ever loose (`put_pack` writes packs), so
/// this is reachable only for a repository populated by something else.
/// * **Absent.** The caller named an object that is not here at all, which is
/// a selection bug upstream and not something to paper over.
fn locate_for_emission<'p>(
&self,
packs: &'p PackSet,
id: &ObjectId,
) -> Result<(&'p Pack, u64)> {
if let Some(found) = packs.find_entry(id) {
return Ok(found);
}
use gix_object::Exists as _;
if self.odb.with_handle(|odb| odb.exists(id)) {
bail!(
"{id} is a LOOSE object: it is in this repository but has no pack entry, so there \
are no stored bytes to copy. This backend emits packs by copying entries and \
refuses rather than inflating and re-deflating an object, which would make the \
`copied`/`recompressed` receipt a fiction"
);
}
bail!("{id} was selected for emission and this store does not hold it")
}
/// Step 3: assemble the ordered entries into a packfile on `out`.
///
/// # The one place that decides how a delta names its base
///
/// A delta entry is `[header][deflated delta stream]`, and the two spellings
/// differ **only in the header**: `OBJ_OFS_DELTA` carries a backwards
/// distance, `OBJ_REF_DELTA` carries the base's raw object id. The stream is
/// the same bytes either way, so re-heading one as the other is a header
/// rewrite over an untouched payload — bytes on the wire, no CPU, and
/// `recompressed` stays 0.
///
/// Which one comes out is a property of the **request**, not of what is on
/// disk. `ofs-delta` is a client capability; `OBJ_REF_DELTA` needs no
/// capability at all and is the older of the two forms. So:
///
/// * **`caps.ofs_delta`** — every delta whose base travels here goes out as
/// an `OFS_DELTA`, whichever way it was stored. Re-heading a stored
/// `REF_DELTA` is not an optimisation: `gix_pack::index::write` refuses
/// every `REF_DELTA` outright, base in the pack or not, so a pack that
/// copied them verbatim was servable to stock git and not to gitoxide.
/// * **not `caps.ofs_delta`** — every such delta goes out as a `REF_DELTA`,
/// naming the base by id. The alternative is not "send an `OFS_DELTA`
/// anyway": it is either that (a pack the client cannot parse) or
/// inflating and re-deflating the whole clone, which was measured at
/// **18.4×** the wire bytes. Naming the base by id costs one hash width
/// per delta entry against a 1-3 byte varint, and nothing else.
///
/// A `REF_DELTA` left deliberately **thin** — `delta_base` is `None` because
/// this store does not hold the base and the client advertised `thin-pack` —
/// has no position to be named by and keeps its stored header untouched.
fn write_emission(
&self,
ordered: &[Planned<'_>],
caps: &Caps,
out: &mut dyn std::io::Write,
) -> Result<PackStats> {
let count = u32::try_from(ordered.len()).map_err(|_| {
anyhow!(
"a pack holds at most u32::MAX entries, was given {}",
ordered.len()
)
})?;
let mut sink = Trailing::new(out, self.hash_kind);
sink.put(b"PACK")?;
sink.put(&2u32.to_be_bytes())?;
sink.put(&count.to_be_bytes())?;
// One scratch buffer for entry headers, reused. A header is a handful of
// bytes and there is one per object, so allocating per entry would be
// the allocation this whole path exists to not make.
let mut hdr: Vec<u8> = Vec::with_capacity(32);
// Where each object landed in the OUTPUT. Built as we go, which is
// exactly why the order has to be topological.
let mut placed: HashMap<ObjectId, u64> = HashMap::with_capacity(ordered.len());
let mut copied = 0u64;
let mut recompressed = 0u64;
for p in ordered {
let here = sink.written();
// **A rebuilt entry names no base**, by construction in
// `plan_emission`: it exists precisely because the base it used to
// name is not in this pack. So it is written first and the
// copy arm below never sees one.
if let Body::Rebuilt {
obj_type,
size,
deflated,
} = &p.body
{
hdr.clear();
// The RESOLVED size, unlike the delta arm below: a base-type
// header states the inflated length of the object.
encode_type_and_size(&mut hdr, *obj_type, *size);
sink.put(&hdr)?;
sink.put(deflated)?;
// Recorded like any other entry: a rebuilt object can be the
// base another requested entry names by position.
placed.insert(p.oid, here);
recompressed += 1;
continue;
}
// **The one thing that can reach here and must not.** A
// `PendingRebuild` means [`GixStore::rebuild_bodies`] did not fill a
// slot it was given — a fan-out that dropped a unit — and the entry
// it belongs to has no bytes at all. Refusing by name is the whole
// reason the placeholder is its own variant rather than an empty
// `Rebuilt`: an empty deflate stream would go out as a well-formed
// pack entry that decodes to a zero-length object, and the failure
// would surface as a client-side connectivity error naming some
// other oid entirely.
if matches!(p.body, Body::PendingRebuild) {
bail!(
"{} was planned as a rebuilt entry and its bytes were never produced; the \
rebuild fan-out dropped a unit, and emitting the pack anyway would send an \
empty object under a real object id",
p.oid
);
}
let Body::Copied(parts) = &p.body else {
unreachable!("the rebuilt and pending arms returned above");
};
match p.delta_base {
DeltaBase::InPack(base) => {
hdr.clear();
// **The size is the one the ENTRY states, not the resolved
// one.** For a delta they differ: the header carries the
// length of the delta stream and the index carries the size
// after the chain is applied. Re-encoding with the resolved
// size produces a stream `index-pack --strict` calls
// `inflate returned 1`. It is the same number in both
// spellings, which is precisely why one can become the other.
if caps.ofs_delta {
let base_at = *placed.get(&base).ok_or_else(|| {
anyhow!(
"{} deltas against {base}, which has not been written yet — the \
set was not ordered by `topological_order`",
p.oid
)
})?;
encode_type_and_size(&mut hdr, ObjType::OfsDelta, parts.stated_size);
// Backwards distance in OUTPUT coordinates. `here` is
// always greater, because the base was written first.
encode_ofs_distance(&mut hdr, here - base_at);
} else {
encode_type_and_size(&mut hdr, ObjType::RefDelta, parts.stated_size);
// The raw hash, not hex: a pack entry names its base in
// `hash_kind.len_in_bytes()` binary bytes.
hdr.extend_from_slice(base.as_slice());
}
sink.put(&hdr)?;
sink.put(parts.payload)?;
}
// **A thin delta against a base the client holds.** The base is
// not in this pack, so an `OFS_DELTA` has no position to name and
// this goes out as a `REF_DELTA` by id whatever the client's
// `ofs-delta` capability — the client resolves it out of its own
// object database. Only the header is rewritten: `parts.payload`
// is the stored delta stream, copied byte for byte, so this costs
// no inflate/deflate and counts as `copied`. It re-heads a stored
// `OFS_DELTA` (whose distance would now be meaningless) or a
// stored `REF_DELTA` alike, since both carry the same delta
// stream and differ only in the header.
DeltaBase::External(base) => {
hdr.clear();
encode_type_and_size(&mut hdr, ObjType::RefDelta, parts.stated_size);
hdr.extend_from_slice(base.as_slice());
sink.put(&hdr)?;
sink.put(parts.payload)?;
}
// A base type carries no back-reference at all, and a `REF_DELTA`
// left thin names a base this pack does not contain — neither has
// anything to re-point at, so the stored bytes go out header and
// all, untouched. That is stronger than re-encoding: it cannot
// disagree with what the pack actually holds, including a
// non-canonical size varint git accepts and would not rewrite.
DeltaBase::None => sink.put(parts.stored)?,
}
placed.insert(p.oid, here);
copied += 1;
}
Ok(PackStats {
bytes: sink.finish()?,
objects: u64::from(count),
// Both counted off what happened rather than asserted: a byte count
// and a wall clock cannot tell a pack-copy from a re-deflate, and
// both produce a pack that indexes clean. `recompressed` used to be
// the literal `0`, which stopped being true the moment a boundary
// entry could be rebuilt — and a receipt that is a literal is not a
// receipt.
copied,
recompressed,
})
}
}
/// zlib-deflate one whole object, for [`Body::Rebuilt`].
///
/// `Compression::default()` is level 6, which is zlib's default and therefore
/// git's — `core.compression` is `-1` out of the box and `pack.compression`
/// inherits it — so a rebuilt entry is the size stock `pack-objects` would have
/// made it.
fn deflate(data: &[u8]) -> Result<Vec<u8>> {
use std::io::Write as _;
let mut encoder = flate2::write::ZlibEncoder::new(
// Deflated output is smaller than its input for anything git bothers to
// store, so this is a ceiling that is never grown past in practice and
// never over-reserves by much when it is wrong.
Vec::with_capacity(data.len() / 2 + 64),
flate2::Compression::default(),
);
encoder
.write_all(data)
.context("deflate an object being rebuilt for the wire")?;
encoder
.finish()
.context("finish deflating an object being rebuilt for the wire")
}
impl GitServe for GixStore {
/// **The object, inflated and delta-resolved** — what [`GitOps::get`]
/// deliberately is not.
///
/// Through the pool's retry loop rather than a bare handle: an error is not
/// a hit, and `gix-odb` reports an unresolvable ref-delta base as
/// `DeltaBaseMissing` rather than as `Ok(None)`. A base sitting in a pack
/// written after this handle's snapshot is precisely what the retry is for,
/// and a stale `None` on this path is a refused fetch.
///
/// # Exactly one allocation, and it is given away rather than copied
///
/// The contract owns its `Vec<u8>`. `gix_object::Find::try_find` inflates
/// into a caller-supplied buffer and returns `Data<'a>` borrowed from **that
/// buffer** — the signature says so, since `'a` comes only from the buffer —
/// so the buffer *is* the answer and handing it over costs nothing. A
/// `to_vec()` here would be `P-025` in miniature: one heap allocation and one
/// memcpy per object read, on the path `gunnar-policy` walks a whole ancestry
/// with.
///
/// The one thing that is checked rather than assumed is that the slice
/// covers the whole buffer. It cannot fail today — gix builds the `Data` as
/// `buffer.as_slice()` — and if a future gix returned a sub-slice this would
/// say so instead of silently truncating an object to the wrong bytes.
fn read(&self, oid: Oid<'_>) -> Result<Option<(ObjType, Vec<u8>)>> {
let id = self.oid(oid)?;
self.odb
.lookup(
|odb| {
use gix_object::Find as _;
let mut buf = Vec::new();
// The borrow of `buf` must end before `buf` can be moved
// out, so nothing survives the call but two `Copy` facts.
let Some((kind, len)) = odb
.try_find(&id, &mut buf)?
.map(|data| (data.kind, data.data.len()))
else {
return Ok(None);
};
if buf.len() != len {
return Err(gix_error::ErrorExt::raise(gix_error::Message::new(format!(
"gix answered {id} with {len} bytes out of a {} byte buffer; this \
backend hands the buffer over rather than copying it, which is only \
sound while the two are the same",
buf.len()
))));
}
Ok(Some((resolved_type(kind), buf)))
},
|r: &gix_error::Result<_>| matches!(r, Ok(Some(_))),
)
.map_err(|e| anyhow!("read {id}: {e}"))
}
/// **Kind and post-resolution size, without the payload.** The type probe.
///
/// `try_header` walks entry headers only and never inflates a payload it
/// does not need, which is what makes this cheaper than [`GitServe::read`]
/// rather than merely narrower. The kind it reports is the **resolved** one,
/// so it is never a delta — see [`resolved_type`].
fn header(&self, oid: Oid<'_>) -> Result<Option<(ObjType, u64)>> {
let id = self.oid(oid)?;
// The inherent `GixStore::header` — the trait method of the same name is
// this one, and inherent methods win name resolution, so the call is
// spelled explicitly to say which was meant.
Ok(GixStore::header(self, &id)?.map(|h| (resolved_type(h.kind()), h.size())))
}
/// Bulk [`GitServe::header`], positionally.
///
/// **This arm has no bulk path and loops**, which the contract asks a backend
/// without one to say in its own docs. The Arrow arm answers `sizes` as a
/// single index pass, and that is what collapses a measured 34 124
/// per-object header walks on one clone; here there is no such column, and a
/// pack index does not carry post-resolution sizes at all — a delta's real
/// size is only knowable by walking its chain. Inventing a bulk shape over
/// the same per-object walk would hide that difference from the head-to-head
/// this crate exists for.
///
/// `None` at a position means **unknown — go ask `header`**, never "fine". A
/// caller treating an unknown size as under a ceiling would skip the ceiling
/// for exactly the objects a push had just introduced.
fn sizes(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<u64>>> {
let mut out = Vec::with_capacity(oids.len());
for raw in oids {
// Positional: a miss occupies its slot. Filtering here would shift
// every later answer by one and still return a plausible count.
out.push(GitServe::header(self, raw)?.map(|(_, size)| size));
}
Ok(out)
}
/// `HEAD`.
///
/// **`HEAD` is not a row in [`GitOps::refs`]** — `all()` walks `refs/` and
/// *"deliberately excludes the pseudo-refs such as `HEAD`, which is exactly
/// the contract `RefStore` states and every other backend honours"*. The
/// reason is a type constraint rather than taste: a name type that admits
/// `HEAD` also admits `MERGE_HEAD` and `FETCH_HEAD`, so putting pseudo-refs
/// in the row stream means either widening the name type or filtering at
/// every consumer. This is half of the accessor pair it gets instead.
///
/// Reported **as stored**, which is the same thing [`GitOps::refs`] does for
/// every other row: a symbolic `HEAD` answers `symref_target` and no `oid`.
/// Resolving it here would make this row the one row in the namespace shaped
/// differently from its neighbours, and the two arms have to agree.
///
/// `None` for a repository that has never pointed one.
fn head(&self) -> Result<Option<RefRow>> {
let found = self
.refs
.try_find(HEAD)
.with_context(|| "read HEAD".to_string())?;
Ok(found.map(|r| refs::row(HEAD.to_string(), &r.target, |id| self.peel(id))))
}
/// Point `HEAD`. The other half of the pair; `point_head_at` is the live
/// consumer.
///
/// # The two shapes of a target, and how they are told apart
///
/// The contract's parameter is one `&str` for both, so — matching the Arrow
/// arm exactly, because two engines that disagree about what `HEAD` accepts
/// is the class of defect a conformance harness exists for:
///
/// * a target starting with `refs/` is a **symbolic** ref, written as one;
/// * anything else must be a hex oid of exactly this store's width, written
/// as a direct target.
///
/// The two cannot be confused: a ref name and a 40- or 64-character hex
/// string are disjoint. Anything that is neither is **refused** rather than
/// guessed at — a `HEAD` pointing at a name nothing resolves is an unclonable
/// repository, and it is cheaper to say so here.
///
/// A symbolic target is deliberately **not** checked for existence.
/// `git init` points `HEAD` at `refs/heads/main` before that branch exists,
/// and refusing it would make an empty repository unrepresentable.
fn set_head(&self, target: &str) -> Result<TxId> {
let update = if target.starts_with("refs/") {
RefUpdate::symbolic(HEAD, target)
} else {
let hex_len = self.hash_kind.len_in_bytes() * 2;
let is_hex = target.len() == hex_len && target.bytes().all(|b| b.is_ascii_hexdigit());
if !is_hex {
bail!(
"HEAD can be pointed at a ref name under `refs/` or at a {hex_len}-character \
hex oid; `{target}` is neither, and guessing which was meant is how a \
repository ends up unclonable"
);
}
RefUpdate::set(HEAD, target.to_ascii_lowercase())
};
self.put_refs(&[update])
}
/// **Emit a packfile containing exactly `objects`**, and nothing else at all.
///
/// One line of delegation to [`GixStore::emit_oids`], and the absence of a
/// second line is the contract: **this must not compute a closure.** The
/// caller is upload-pack holding a selection that is already post-filter,
/// post-shallow, post-`include-tag` and deliberately not closed. An engine
/// that treats it as tips serves a `--filter=blob:none` fetch exactly the
/// objects it asked to be left out, and nothing we own can see it: the
/// over-sent pack passes `index-pack --strict` *and* `fsck`, the clone
/// succeeds and the exit code is zero.
///
/// # `have` is thin-pack base selection, and never membership
///
/// `have` is the negotiated common **tips**, *"used for thin-pack base
/// selection, never to derive membership"*. This arm now takes that
/// optimisation, matching the Arrow arm and stock `pack-objects --thin`: a
/// boundary entry whose stored delta names a base the request does not carry
/// but the **client already holds** stays a byte copy, emitted as a thin
/// `REF_DELTA` naming that base, instead of being rebuilt whole. On a
/// repacked server every requested object has a pack entry, so before this
/// the boundary of a divergent fetch — the deltas whose base sits on the
/// client's side of the negotiation cut — was the entire recompress count,
/// paying inflate+deflate for bytes the receiver already has.
///
/// The closure of these tips is what the client holds, and closing over them
/// is safe *exactly* when `caps.thin` is set: the caller advertises thin
/// only for a non-shallow, unfiltered fetch with common history, the
/// conditions under which a `have` tip's closure is truly held (a shallow or
/// `blob:none` client reaches objects through those tips that it was
/// deliberately never sent). So the closure is computed only under
/// `caps.thin`, and [`GixStore::plan_emission`] vouches each individual base
/// against it — a base outside the closure is still rebuilt whole.
///
/// `have` is deliberately **not** repurposed into an exclusion set. Deriving
/// membership from it is the opposite mistake: it would *remove* objects the
/// caller's finished selection had decided to send, an under-send, the
/// failure that exits zero. It informs only *how* a boundary delta is
/// encoded, never *whether* an object is emitted.
fn emit_pack(
&self,
objects: &[Oid<'_>],
have: &[Oid<'_>],
caps: &Caps,
out: &mut dyn std::io::Write,
) -> Result<PackStats> {
// The closure of the client's `have` tips — everything it held before
// this transfer — computed once and vouched against per boundary base in
// `plan_emission`. Only under `caps.thin`, both because that is the one
// request shape where the closure is what the client truly holds and
// because it is the client's consent to receive a thin pack at all.
let held = if caps.thin && !have.is_empty() {
let have_ids: Vec<ObjectId> =
have.iter().map(|raw| self.oid(raw)).collect::<Result<_>>()?;
Some(self.odb.with_handle(|odb| {
crate::graph::have_closure(odb, self.hash_kind, &have_ids)
})?)
} else {
None
};
self.emit_core(objects, caps, out, None, held.as_ref())
}
/// Negotiation: `want` minus `have`, with the two derived facts a caller
/// cannot recompute cheaply.
///
/// # The bitmap first, the walk always
///
/// This used to say *"a graph **walk**, because gitoxide has no bitmap
/// support of any kind"*, and the premise is still true — gitoxide has none.
/// edda's does: [`crate::bitmap::select`] answers from the `.bitmap`
/// [`GitOps::gc`] wrote, in one `OR` per want and one `AND NOT`, reading no
/// object at all when every want is bitmapped.
///
/// **The walk is not a legacy path.** A bitmap covers one pack, so anything
/// pushed since the last `gc` is reached through a bounded walk, and a
/// request the bitmap cannot answer exactly — no `.bitmap`, a damaged one,
/// one describing a different pack, a `want` that is not in it — falls all
/// the way back here. The two are held to the same **object set** by
/// `tests/bitmap_and_walk_agree.rs`; that equality is the whole safety
/// argument, and it is why the fallback is a `match` and not a `?`.
///
/// Which one ran is invisible in the answer by construction, so it is
/// counted: [`crate::GixStore::bitmap_selection_counts`].
///
/// # `Ok(None)` is load-bearing, and this arm reaches it
///
/// [`GitOps::reachable`] treats a `want` it cannot find as contributing
/// **itself and nothing else** — safe for `live_set`, which over-keeps, and a
/// silent **under-send** here: a clone served that way exits zero with one
/// object per branch. So every tip is checked first, and a tip this store
/// does not hold declines the whole request rather than answering it badly.
/// That is the same statement the Arrow arm makes when its reachability
/// projection cannot cover a request, reached by a different route.
///
/// Do not "improve" this into always answering. An engine that always
/// answers has to answer wrongly somewhere, and this is the shape of wrong
/// that no exit code and no `fsck` can see.
fn select(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> Result<Option<ReachSet>> {
let to_ids = |s: &[Oid<'_>]| -> Result<Vec<ObjectId>> {
s.iter().map(|raw| self.oid(raw)).collect()
};
let want_ids = to_ids(want)?;
for id in &want_ids {
if !self.has(id.as_slice())? {
return Ok(None);
}
}
// A `have` this store does not hold is NOT a refusal: it is the client
// telling you about its own commits, which is the ordinary case in a
// negotiation and the reason `has` deliberately does not buy a `statx`
// for it. The walk skips what it cannot find, which for the stop set
// means "stop at nothing", and over-sending is the safe direction here.
let have_ids = to_ids(have)?;
let sel = self.selection(&want_ids, &have_ids)?;
let raw = |v: Vec<ObjectId>| v.into_iter().map(|id| id.as_slice().to_vec()).collect();
Ok(Some(ReachSet {
objects: raw(sel.objects),
commits: raw(sel.commits),
// **The voucher goes over flat**, not as a `Vec` per oid. It is the
// one field of the three that is repository-sized on an ordinary
// fetch — the closure of what the client already held — and a
// `Vec<u8>` per entry is a heap allocation this engine pays for
// nothing, exactly as the znippy engine did on the other side of the
// same contract. See `git_storage_trait::OidList`.
client_has: sel.client_has.iter().map(ObjectId::as_slice).collect(),
}))
}
}
impl GixStore {
/// `want` minus `have`: the bitmap if it can answer exactly, the walk
/// otherwise.
///
/// **The single seam.** Both arms return the same
/// [`crate::graph::Selection`], so nothing downstream can tell them apart —
/// which is the point (an engine whose two reachability paths disagree
/// serves a pack that indexes, `fsck`s and applies, and leaves the client's
/// working tree wrong). Routing every selection through one function is what
/// makes `tests/bitmap_and_walk_agree.rs` a statement about the shipping
/// code rather than about two functions a test called by hand.
///
/// Exposed on the store so a test can drive it directly and assert which arm
/// answered; `GitServe::select` is one line of translation over it.
pub fn selection(
&self,
want: &[ObjectId],
have: &[ObjectId],
) -> Result<crate::graph::Selection> {
use std::sync::atomic::Ordering;
if let Some(bm) = self.bitmaps.get(&self.pack_dir(), self.hash_kind) {
let answered = self
.odb
.with_handle(|odb| crate::bitmap::select(&bm, odb, self.hash_kind, want, have))?;
if let Some((sel, cost)) = answered {
crate::BITMAP_SELECTIONS.fetch_add(1, Ordering::Relaxed);
crate::BITMAP_COMMITS_WALKED
.fetch_add(cost.commits_walked as u64, Ordering::Relaxed);
return Ok(sel);
}
}
crate::WALK_SELECTIONS.fetch_add(1, Ordering::Relaxed);
self.odb
.with_handle(|odb| crate::graph::select(odb, self.hash_kind, want, have))
}
}
/// A compile-time assertion that the reading contract is implemented too, so a
/// signature drifting from the frozen `GitServe` is a build failure here rather
/// than a surprise at a call site. The `GitOps` twin is in `lib.rs`.
const _: () = {
fn assert_impl<T: GitServe>() {}
fn check() {
assert_impl::<GixStore>();
}
let _ = check;
};
#[cfg(test)]
mod tests {
use super::*;
/// **Both encoders are checked against gix's own decoder**, over the
/// boundaries the varints actually turn on rather than a handful of small
/// numbers.
///
/// This is the one place in the crate that writes pack bytes by hand, so it
/// is the one place a silent format bug can start. `git index-pack --strict`
/// in `tests/serving_the_wire.rs` catches it end to end; this catches it at
/// the boundary and says which of the two varints was wrong. The oracle is
/// `gix_pack::data::Entry::from_bytes`, which is the same reader
/// [`crate::packs`] uses, so an encoder and a decoder that agree with each
/// other but not with git cannot both pass.
#[test]
fn the_entry_encoders_are_read_back_by_gix_itself() {
// The size varint turns at 4 bits and then every 7: 0x0f/0x10 is the
// first continuation, 0x7ff/0x800 the second, and so on.
let sizes = [
0u64,
1,
15,
16,
127,
128,
2047,
2048,
1 << 20,
u32::MAX as u64,
];
for &size in &sizes {
for kind in [ObjType::Commit, ObjType::Tree, ObjType::Blob, ObjType::Tag] {
let mut buf = Vec::new();
encode_type_and_size(&mut buf, kind, size);
// A payload the decoder will not read, so a header that claims
// more bytes than it wrote is visible as a decode failure.
buf.extend_from_slice(&[0u8; 8]);
let e = gix_pack::data::Entry::from_bytes(&buf, 0, gix_hash::Kind::Sha1)
.expect("gix must read back what we wrote");
assert_eq!(e.decompressed_size, size, "size round trip for {kind:?}");
assert_eq!(e.header.as_type_id(), kind.code(), "type round trip");
}
}
// The distance varint is the *other* one: each continuation subtracts
// one before shifting, so it has no redundant representations, and off
// by one here produces a pack that resolves a delta against the wrong
// base.
let distances = [1u64, 2, 127, 128, 129, 16511, 16512, 1 << 20, 1 << 40];
for &d in &distances {
let mut buf = Vec::new();
encode_type_and_size(&mut buf, ObjType::OfsDelta, 42);
encode_ofs_distance(&mut buf, d);
buf.extend_from_slice(&[0u8; 8]);
// `pack_offset` has to exceed the distance or the entry names a
// base before the start of the pack.
let e = gix_pack::data::Entry::from_bytes(&buf, d + 12, gix_hash::Kind::Sha1)
.expect("gix must read back the distance we wrote");
assert_eq!(e.decompressed_size, 42);
match e.header {
gix_pack::data::entry::Header::OfsDelta { base_distance } => {
assert_eq!(base_distance, d, "distance round trip")
}
other => panic!("expected an ofs-delta, got {other:?}"),
}
}
}
}