znippy_plugin_git/serve.rs
1//! **The reading contract, answered out of Arrow IPC.** No gix, anywhere.
2//!
3//! ZNIPPY-GIT APACHE ARROW IPC IS LAW. [`crate::store`] holds the eleven — what
4//! a caller *stores*; this file holds [`GitServe`] — what a caller *reads*, and
5//! what a clone is served from. The split is the contract's, not this crate's:
6//! `GitOps::get` hands back the pack entry **byte for byte as the client sent
7//! it**, which for a delta is a delta, and nothing on the eleven inflates one.
8//!
9//! ```text
10//! read / header / sizes → the objects table, then §14's exploded table
11//! head / set_head → the ref log; HEAD is NOT a row in refs()
12//! select → roaring bitmaps over the store's ordinals
13//! emit_pack → emit_set → topological_order → byte-range copy
14//! ```
15//!
16//! # 🚨 The trap this file exists to avoid, stated once
17//!
18//! The working reference for every method below is `gunnar-coldtier`'s
19//! `git_store.rs`, and it carries **87 `gix_` references**. It reads objects
20//! through `gix_pack::Find` / `decode_entry` / `decode_header`, and it emits
21//! packs through `iter_from_counts` / `InOrderIter` / `FromEntriesIter`.
22//! **Its intent is ported here. Not one line of its code is.**
23//!
24//! `znippy-plugin-git` links **zero gix in `[dependencies]`** — the four gix
25//! crates it names are `[dev-dependencies]`, the differential oracle in
26//! [`crate::pack_walk`] that proves this crate's parser agrees with gitoxide
27//! *without shipping it*. That separation is the entire point of two
28//! implementations behind one contract, and no method here may create an edge
29//! into it. Not for a parser, not for `gix-hash`, not temporarily.
30//!
31//! Nor would copying it be desirable. That pack pipeline **is** `P-018`'s serial
32//! tail — counting's serial reduce, a `sort_by` over counts, `InOrderIter`'s
33//! `BTreeMap` reorder, `FromEntriesIter` hashing every byte on the consuming
34//! thread — plus `P-025`'s residue, `data.to_owned()`, one heap allocation per
35//! object served. The Arrow arm's answer to all of it is a **byte-range copy out
36//! of the archive**, and [`crate::pack_walk::emit_pack`] is where it lives.
37//!
38//! # The serving tier is serial — `P-004`, DECIDED, with one bounded exception
39//!
40//! `sizes` does not fan out. `select` does not fan out. The two-tier split is
41//! settled: **request concurrency, no intra-request fan-out**, one core's worth
42//! per transfer behind an admission gate. At saturation, parallelising *one*
43//! clone across N cores does not raise clones per second — it is the same work
44//! rearranged — and the constellation's fan-out primitive has no reentrancy
45//! detection, so a fan-out here inside a fan-out there silently spawns W².
46//! `gatling` belongs in [`crate::indexer`]'s background tier, which is where
47//! nearly all of it is.
48//!
49//! The exception, added 2026-08-14, is **phase 1 of `emit_oids`** —
50//! [`GitStore::resolve_emit_payloads`](crate::git_ops::GitStore::resolve_emit_payloads),
51//! which turns each entry's archive address into a slice of the mapped archive.
52//! It is allowed for the reason the decision above is stated in terms of and not
53//! against it:
54//!
55//! * it is **bounded at 4 workers**, not at ncores, so 32 admitted transfers
56//! are 128 threads at worst rather than 32 × ncores — the admission gate
57//! stays in charge of the machine;
58//! * it **does not run at all** below 4096 entries, so every request small
59//! enough for thread-spawn to dominate is byte for byte the old serial path;
60//! * it takes **no lock**, because the archive is append-only and immutable
61//! behind a snapshot, so it is not the "same work rearranged" — it is work
62//! that has no serial dependency to rearrange around.
63//!
64//! Phase 2 — output offsets, `OFS_DELTA` distances, the writes and the running
65//! hash — stays strictly serial, and that is not a bound anyone chose: an
66//! `OFS_DELTA` names its base by distance back in the *output* pack, so entry
67//! *n* cannot be encoded until every earlier entry's length is known.
68//!
69//! Every method **blocks**. Never call one from an async task without
70//! `tokio::task::spawn_blocking`.
71
72use anyhow::{anyhow, bail, Context as _, Result};
73
74pub use git_storage_trait::{Caps, GitServe, PackStats, ReachSet};
75
76use crate::git_ops::{GitOps, GitStore, RefRow, TxId};
77use crate::index_layout::{ObjType, ObjectIndex};
78use crate::object::GitObjectKind;
79use crate::refs::RefUpdate;
80use crate::store::Oid;
81
82/// `HEAD`, the one pseudo-ref this contract names.
83///
84/// It is **not** a row in [`GitOps::refs`] — see [`GitStore::head`] for the
85/// argument, which is a type constraint rather than a preference.
86pub const HEAD: &str = "HEAD";
87
88/// The resolved kind, in the contract's vocabulary.
89///
90/// A total function, and that is the guarantee: [`GitObjectKind`] has exactly
91/// the four real types and no delta variants, so a value that came out of a
92/// resolver **cannot** be labelled `OfsDelta` or `RefDelta` by accident. The
93/// contract's `ObjType` has six codes and this is the one place the narrowing is
94/// written down — [`crate::git_ops::GitStore::emit_set`] rebuilds whole entries
95/// and needs the same narrowing, and calls this rather than repeating it.
96pub(crate) fn resolved_type(kind: GitObjectKind) -> ObjType {
97 match kind {
98 GitObjectKind::Commit => ObjType::Commit,
99 GitObjectKind::Tree => ObjType::Tree,
100 GitObjectKind::Blob => ObjType::Blob,
101 GitObjectKind::Tag => ObjType::Tag,
102 }
103}
104
105/// How many bytes of an entry are read to find its type and its base.
106///
107/// A type/size varint is at most 10 bytes and an `OFS_DELTA` distance at most 9,
108/// so 64 covers every header plus a 32-byte `REF_DELTA` base oid with room over.
109/// It is a **ceiling on a `pread`**, not a promise about the entry: a shorter
110/// entry reads short and the grammar still terminates.
111const HEADER_PROBE: u64 = 64;
112
113impl<S: ObjectIndex + 'static> GitStore<S> {
114 /// The chain walk behind [`GitServe::header`]: the **resolved** type of the
115 /// entry at `offset`, reading entry headers only.
116 ///
117 /// # Why this is not "just read the column"
118 ///
119 /// `objects.obj_type` is the entry's type **in the pack**, so a delta's is
120 /// `OfsDelta` or `RefDelta` and the resolved kind is one or more entries
121 /// away. `objects.uncompressed_size` has no such problem — it is written
122 /// after delta application — which is why [`GitServe::sizes`] is a pure
123 /// index pass and this is not.
124 ///
125 /// # What it costs, exactly
126 ///
127 /// One 64-byte `pread` per link of the chain, and **no inflate, no delta
128 /// application, no allocation sized by the object**. gix answers the same
129 /// question with `decode_header`, which walks the same links; the difference
130 /// is that this one reads the offsets straight out of §13's `delta_base`
131 /// column for the first hop rather than reconstructing them, and does not
132 /// need a pack handle, a zlib scratch or a delta cache to do it.
133 ///
134 /// Chains are bounded by the depth the pushing client's `pack.depth` chose —
135 /// 50 by default — but nothing in a *pushed* pack is under this server's
136 /// control, so the walk carries its own ceiling and refuses rather than
137 /// looping. A cycle in a pack's back-references is a corrupt pack, and
138 /// saying so beats spinning.
139 fn resolved_type_at(&self, offset: u64, obj_type: ObjType, delta_base: u64) -> Result<ObjType> {
140 /// git's own `pack.depth` ceiling, doubled. A chain longer than this is
141 /// not a deep delta, it is a loop.
142 const MAX_LINKS: usize = 100;
143
144 // One mapping for the whole chain walk. A probe used to be one `pread`
145 // per link — an allocation and a kernel→user copy for 64 bytes that are
146 // already in the page cache; through the mapping it is a bounds check
147 // and a pointer, and the `Cow` is `Borrowed` on every link the snapshot
148 // covers.
149 let snap = self.archive_snapshot()?;
150 let mut at = offset;
151 let mut t = obj_type;
152 let mut base = delta_base;
153 for _ in 0..MAX_LINKS {
154 match t {
155 ObjType::Commit | ObjType::Tree | ObjType::Blob | ObjType::Tag => return Ok(t),
156 ObjType::OfsDelta => {
157 // §13's column already holds the base's **absolute archive
158 // offset**, resolved at absorb time. No distance arithmetic
159 // and no second parse of this entry.
160 if base == 0 {
161 bail!(
162 "the entry at archive offset {at} is an ofs-delta whose base column is \
163 0, which is the no-base sentinel — the index disagrees with itself"
164 );
165 }
166 at = base;
167 }
168 ObjType::RefDelta => {
169 // A ref-delta's `delta_base` column is 0 by construction —
170 // its base is named by oid, inside the entry, directly after
171 // the type/size varint. The same few bytes
172 // `GitStore::emit_set` reads for the same reason.
173 let head = self.extent(&snap, at, HEADER_PROBE)?;
174 let (_, _, n) = crate::pack_walk::type_and_size_of(&head)?;
175 let oid_len = self.hash_kind().oid_len();
176 let base_oid = head.get(n..n + oid_len).ok_or_else(|| {
177 anyhow!(
178 "the ref-delta entry at archive offset {at} has no base oid after its \
179 header"
180 )
181 })?;
182 let Some(row) = self.index().lookup(base_oid) else {
183 bail!(
184 "the ref-delta entry at archive offset {at} names a base this \
185 repository does not have — its type cannot be resolved"
186 );
187 };
188 at = row.offset;
189 t = row.obj_type;
190 base = row.delta_base;
191 continue;
192 }
193 }
194 // One header read at the new position. The type column would be a
195 // second lookup keyed the wrong way (by offset, and the index is
196 // keyed by oid), so the entry's own grammar answers it.
197 let head = self.extent(&snap, at, HEADER_PROBE)?;
198 let (next_t, _, n) = crate::pack_walk::type_and_size_of(&head)?;
199 t = next_t;
200 base = match next_t {
201 ObjType::OfsDelta => {
202 let (distance, _) = crate::pack_walk::ofs_distance_of(&head[n..])?;
203 at.checked_sub(distance).ok_or_else(|| {
204 anyhow!(
205 "the ofs-delta at archive offset {at} names a base {distance} bytes \
206 back, which is before the start of the archive"
207 )
208 })?
209 }
210 _ => 0,
211 };
212 }
213 bail!(
214 "resolving the type of the entry at archive offset {offset} followed more than \
215 {MAX_LINKS} delta links — the pack's back-references form a cycle"
216 )
217 }
218
219 /// **Emit a packfile for an explicit object set**, closed over its delta
220 /// bases — and over **nothing else**.
221 ///
222 /// This is the whole of [`GitServe::emit_pack`] as of 2026-08-10: that
223 /// method is this call and a discarded `have`. It was written as the
224 /// *inherent* escape route for a trait method that closed over its input,
225 /// so that a caller told *"walk it yourself"* by [`GitServe::select`]'s
226 /// `Ok(None)` had somewhere to put the set it had walked. The trait method
227 /// stopped closing over its input, so the escape route and the front door
228 /// are now the same door. It stays inherent because the gix arm needs no
229 /// equivalent and the contract does not grow a method for it.
230 ///
231 /// # 🔴 No closure at all any more, and that is the 2026-08-11 fix
232 ///
233 /// [`GitStore::emit_set`] used to add a delta's base when the base fell
234 /// outside the request, on the reasoning that an `OFS_DELTA` names its base
235 /// by position and so cannot be encoded without it. That reasoning is
236 /// correct about the *pack format* and wrong about the *clone*: a base
237 /// pulled in is an object the client did not ask for, and if it is a tree it
238 /// arrives owing children the pack does not contain. `git index-pack
239 /// --check-self-contained-and-connected` — what a clone runs — then dies
240 /// with `did not receive expected object`, while this server logs
241 /// `git.upload_pack.served`. See `emit_set` for the measured numbers.
242 ///
243 /// So the set is now **exactly** the caller's, and the base decides how each
244 /// entry is *encoded* rather than what the pack contains: base inside the
245 /// request, copy the stored bytes; base outside it, rebuild the object
246 /// whole. That is stock `pack-objects`' rule, and `recompressed` counts the
247 /// second case honestly instead of being a literal zero.
248 ///
249 /// # Three steps, all of them znippy's own
250 ///
251 /// 1. [`GitStore::emit_set`] reads each requested entry and decides copy or
252 /// rebuild, never adding an object;
253 /// 2. [`crate::pack_walk::topological_order`] puts every base before the
254 /// delta naming it, which backwards distances require;
255 /// 3. [`crate::pack_walk::emit_pack`] re-encodes each entry header — the one
256 /// thing that must change, because a distance is relative to a position
257 /// in the *input* pack — and streams every payload **byte for byte**.
258 ///
259 /// `copied + recompressed == objects` always, and for a whole-repository
260 /// clone `recompressed` is 0 because such a request contains every base.
261 /// Both are counted off what happened rather than asserted: a byte count and
262 /// a wall clock cannot tell a pack-copy from a re-deflate, and both pass
263 /// `index-pack --strict`.
264 pub fn emit_oids(
265 &self,
266 oids: &[Oid<'_>],
267 have: &[Oid<'_>],
268 caps: &Caps,
269 out: &mut dyn std::io::Write,
270 ) -> Result<PackStats> {
271 // **Both halves, or nothing.** `caps.thin` is the client's *consent* to
272 // receive a delta whose base it must supply itself; `have` is the
273 // negotiation's answer to *which* bases those may be. Consent with no
274 // negotiated tips is a clone — the client holds nothing, so there is
275 // nothing a base could safely point at — and tips without consent is a
276 // client that never agreed to run `index-pack --fix-thin`. Either alone
277 // is a failed fetch rather than a smaller one, so either alone is `None`
278 // and the boundary entries go out whole exactly as before.
279 let thin_haves = (caps.thin && !have.is_empty()).then_some(have);
280
281 let entries = self
282 .emit_set(oids, caps.ofs_delta, thin_haves)
283 .context("building the emit set")?;
284 let (ordered, missing) = crate::pack_walk::topological_order(entries);
285 if !missing.is_empty() {
286 bail!(
287 "the emit set is not closed: {} delta base(s) absent, first at archive offset {} \
288 — refusing to emit a pack whose closure does not hold",
289 missing.len(),
290 missing[0]
291 );
292 }
293
294 // **The one capability that changes which bytes come out**, and it is
295 // acted on in `emit_set` rather than here. A client that did not
296 // advertise `ofs-delta` used to be REFUSED by name at this point,
297 // because this engine copies stored entries and had no way to re-name a
298 // base. It has one now: an `OFS_DELTA` whose base is in the request is
299 // re-headed as a `REF_DELTA` carrying the same compressed delta stream,
300 // so that client is served a pack it can parse without a single byte
301 // being re-deflated. What remains here is the assertion that it worked —
302 // emitting an `OFS_DELTA` to such a client is the failure that looks
303 // like a working server.
304 if !caps.ofs_delta {
305 if let Some(e) = ordered.iter().find(|e| e.obj_type == ObjType::OfsDelta) {
306 bail!(
307 "this client did not advertise ofs-delta and the entry at archive offset {} \
308 is still an ofs-delta after the emit set was built; refusing rather than \
309 answering with a pack the client cannot parse",
310 e.offset
311 );
312 }
313 }
314 // `caps.thin` IS acted on now, in `emit_set`, and the pass it needed
315 // turned out to belong exactly where the note that used to sit here said
316 // it would: beside the negotiation that produced `have`. What it does is
317 // narrow, and deliberately so — it changes nothing about *which* objects
318 // are sent, only whether a boundary entry may name a base the pack does
319 // not carry instead of being rebuilt whole.
320
321 // **Borrowed, not owned.** Every entry a clone sends is an
322 // `EntryBytes::Extent` — an address, 16 bytes — which `emit_ordered`
323 // resolves to a slice of one mapping of the archive (phase 1, parallel)
324 // before streaming it (phase 2, serial). The `to_vec()` that used to sit
325 // on this path is `P-025` exactly: one heap allocation per object
326 // served, on the path whose whole claim is that it copies stored bytes
327 // without touching them.
328 let report = self
329 .emit_ordered(&ordered, out)
330 .context("emitting the pack")?;
331
332 Ok(PackStats {
333 bytes: report.bytes,
334 objects: u64::from(report.written),
335 copied: u64::from(report.copied),
336 // **Counted, not asserted.** This was the literal `0` for as long as
337 // nothing on the path could re-deflate; `emit_set` can now, for
338 // exactly the entries whose delta base the request does not contain,
339 // and the receipt says how many rather than continuing to claim
340 // none. A whole-repository clone still reports 0 — and that is a
341 // measurement of a full selection, not a promise.
342 recompressed: u64::from(report.recompressed),
343 })
344 }
345}
346
347impl<S: ObjectIndex + 'static> GitServe for GitStore<S> {
348 /// **The object, inflated and delta-resolved** — §14's exploded table, one
349 /// point lookup.
350 ///
351 /// This is the question [`GitOps::get`] deliberately does not answer, and
352 /// [`GitStore::content`] has answered it since §14 landed; the trait method
353 /// is that call plus the kind narrowing. Two paths under it, returning
354 /// identical bytes and distinguishable only by
355 /// [`crate::exploded::ExplodedStats`]: the table, or a re-derivation from
356 /// the verbatim truth when the table has been dropped or has not caught up.
357 ///
358 /// The returned [`ObjType`] can never be a delta — see [`resolved_type`].
359 fn read(&self, oid: Oid<'_>) -> Result<Option<(ObjType, Vec<u8>)>> {
360 Ok(self
361 .content(oid)?
362 .map(|(kind, bytes)| (resolved_type(kind), bytes)))
363 }
364
365 /// **Kind and post-resolution size, without the payload.**
366 ///
367 /// The size is a column, and the *right* column: `uncompressed_size` is
368 /// written after delta application, so it is the fact a `.idx` and a `.rev`
369 /// together cannot answer, and it is read rather than measured.
370 ///
371 /// The kind is not a column — `obj_type` is the entry's type in the pack —
372 /// so for the common case (a non-delta entry) this touches nothing else, and
373 /// for a delta it walks the chain's **headers** through
374 /// [`GitStore::resolved_type_at`]. Still no inflate and still no payload.
375 fn header(&self, oid: Oid<'_>) -> Result<Option<(ObjType, u64)>> {
376 // Same preamble as `has`/`extents`, and for the same reason: a store
377 // holding a pack whose bytes are durable and whose objects are not
378 // indexed yet cannot answer "absent" without lying.
379 if self.unindexed_packs() > 0 {
380 self.absorb_pending()?;
381 }
382 let Some(row) = self.index().lookup(oid) else {
383 return Ok(None);
384 };
385 let kind = self
386 .resolved_type_at(row.offset, row.obj_type, row.delta_base)
387 .with_context(|| format!("resolving the type of {}", hex::encode(oid)))?;
388 Ok(Some((kind, row.uncompressed_size)))
389 }
390
391 /// **Post-resolution sizes in bulk: one index pass, no chain touched.**
392 ///
393 /// The half of [`header`](GitServe::header) that is already data. On the
394 /// `h2h` fixture one clone paid **34 124** per-object header walks, every
395 /// one of which this collapses into a single `lookup_batch`, to establish
396 /// that a repository whose largest blob is 16 KiB holds nothing over the
397 /// 1 GiB default ceiling.
398 ///
399 /// # The preamble is not optional
400 ///
401 /// This reaches [`GitStore::index`] directly, so it absorbs pending index
402 /// work itself. Without it a pack that landed since the last drain reads as
403 /// **absent**, and an absent size that a caller treated as a pass would
404 /// silently skip the ceiling for exactly the objects a push had just
405 /// introduced. `None` at a position is *"unknown, go ask `header`"* and
406 /// never *"fine"*.
407 fn sizes(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<u64>>> {
408 if self.unindexed_packs() > 0 {
409 self.absorb_pending()?;
410 }
411 Ok(self
412 .index()
413 .lookup_batch(oids)
414 .into_iter()
415 .map(|row| row.map(|r| r.uncompressed_size))
416 .collect())
417 }
418
419 /// **`HEAD`, which is not a row in [`GitOps::refs`].**
420 ///
421 /// The gix arm's `iter()` *"walks `refs/` (loose and packed) and deliberately
422 /// excludes the pseudo-refs such as `HEAD`, which is exactly the contract
423 /// every other backend honours"* — and as of this change so does this arm
424 /// (see [`GitOps::refs`]). The reason is a type constraint rather than
425 /// taste: a name type that admits `HEAD` also admits `MERGE_HEAD` and
426 /// `FETCH_HEAD`, so putting pseudo-refs in the row stream means either
427 /// widening the name type or filtering at every consumer.
428 ///
429 /// So it is read here, off the same fold [`GitOps::refs`] reads, rather than
430 /// from a second place that could disagree with it. `None` for a repository
431 /// that has never pointed one — an empty repository has no `HEAD`.
432 fn head(&self) -> Result<Option<RefRow>> {
433 let state = self.ref_state()?;
434 let Some(s) = state.get(HEAD) else {
435 return Ok(None);
436 };
437 let decode = |h: &Option<String>| -> Result<Option<Vec<u8>>> {
438 match h {
439 Some(h) => Ok(Some(hex::decode(h).map_err(|e| {
440 anyhow!("HEAD holds `{h}`, which is not a hex oid: {e}")
441 })?)),
442 None => Ok(None),
443 }
444 };
445 Ok(Some(RefRow {
446 name: HEAD.to_string(),
447 oid: decode(&s.target)?,
448 peeled: decode(&s.peeled)?,
449 symref_target: s.symref_target.clone(),
450 }))
451 }
452
453 /// Point `HEAD`, through the same log a push writes to.
454 ///
455 /// # The two shapes of a target, and how they are told apart
456 ///
457 /// `HEAD` is symbolic in every repository anyone serves — `ref:
458 /// refs/heads/main` — and detached in the one case git also supports. The
459 /// contract's parameter is one `&str` for both, so:
460 ///
461 /// * a target starting with `refs/` is a **symbolic** ref, written as one;
462 /// * anything else must be a hex oid of exactly this store's width, written
463 /// as a direct target.
464 ///
465 /// The two cannot be confused: a ref name and a 40- or 64-character hex
466 /// string are disjoint. Anything that is neither is **refused** rather than
467 /// guessed at — a `HEAD` pointing at a name nothing resolves is an
468 /// unclonable repository, and it is cheaper to say so here.
469 ///
470 /// A symbolic target is deliberately **not** checked for existence.
471 /// `git init` points `HEAD` at `refs/heads/main` before that branch exists,
472 /// and refusing it would make an empty repository unrepresentable. A direct
473 /// target *is* checked, by [`GitOps::put_refs`], because a detached `HEAD`
474 /// naming an absent object is dangling in exactly the sense that refuses.
475 fn set_head(&self, target: &str) -> Result<TxId> {
476 let update = if target.starts_with("refs/") {
477 RefUpdate::symbolic(HEAD, target)
478 } else {
479 let hex_len = self.hash_kind().hex_len();
480 if target.len() != hex_len || hex::decode(target).is_err() {
481 bail!(
482 "HEAD can be pointed at a ref name under `refs/` or at a {hex_len}-character \
483 hex oid; `{target}` is neither, and guessing which was meant is how a \
484 repository ends up unclonable"
485 );
486 }
487 RefUpdate::set(HEAD, target.to_ascii_lowercase())
488 };
489 self.put_refs(&[update])
490 }
491
492 /// **Emit a packfile containing exactly `objects`.**
493 ///
494 /// One line of delegation to [`GitStore::emit_oids`], and the absence of a
495 /// second line is the contract.
496 ///
497 /// # 🔴 This must NOT compute a closure. Fixed 2026-08-10.
498 ///
499 /// Until that date this method opened with `self.select(objects, have)?` and
500 /// emitted *that* — it **closed over its own input**. The caller's set came
501 /// back larger than it went in, and the caller is upload-pack holding
502 /// `selection.objects`: already post-filter, post-shallow, post-`include-tag`
503 /// and *deliberately not closed*. So a `--filter=blob:none`,
504 /// `--filter=tree:0` or `--depth=N` fetch was served **exactly the objects it
505 /// had asked to be left out**.
506 ///
507 /// The reason it needed a rename and a guard rather than a shrug is that
508 /// **nothing we own could see it**: the over-sent pack passes
509 /// `git index-pack --strict` *and* `git fsck`, the clone succeeds, the exit
510 /// code is zero, and the client just silently receives more than it asked
511 /// for. `P-027`'s shape — a change no test can see. Pinned now by
512 /// `emit_pack_emits_exactly_the_set_it_is_given_and_never_its_closure`,
513 /// which asserts a **count** (seen RED at 4 against an expected 1).
514 ///
515 /// It had a second, louder failure mode too, and it is the one that proves
516 /// the two questions were never the same question: [`select`](GitServe::select)'s tip check
517 /// declines a `want` that is not a commit in the graph, so a selection
518 /// containing a tree — which every real one does — could not be emitted at
519 /// all.
520 ///
521 /// # Where the closure went
522 ///
523 /// Nowhere. [`select`](GitServe::select) still owns the close-over-tips half
524 /// and still answers `Ok(None)` when its projection cannot cover a request;
525 /// that hatch is untouched and still load-bearing. The two halves are now
526 /// cleanly separated: *what to send* is asked of `select`, *send exactly
527 /// this* is asked of here. A caller that `select` told to walk the graph
528 /// itself hands the walked set straight back to this method.
529 ///
530 /// There is no closure left on this path at all: since 2026-08-11 the base
531 /// decides how an entry is *encoded*, never what the pack contains.
532 ///
533 /// # `have` is READ, and only for thin-pack base selection
534 ///
535 /// The contract defines `have` as the negotiated common **tips**, *"used for
536 /// thin-pack base selection, never to derive membership"*, and that is
537 /// exactly and only what this arm does with it. Paired with `caps.thin`, it
538 /// lets a boundary entry go out as a `REF_DELTA` naming a base the receiver
539 /// already holds — carrying the stored delta stream unchanged — instead of
540 /// being rebuilt whole. See [`GitStore::client_bases`] for the closure and
541 /// for why the caller has to be able to stand behind the voucher.
542 ///
543 /// It is deliberately **not** repurposed into an exclusion set. Deriving
544 /// membership from `have` is precisely the closure this method stopped
545 /// doing, in the opposite direction: it would *remove* objects the caller's
546 /// finished selection had already decided to send, which is an under-send,
547 /// which is the failure that exits zero. The narrowing below changes the
548 /// *encoding* of an entry and never whether it is emitted.
549 fn emit_pack(
550 &self,
551 objects: &[Oid<'_>],
552 have: &[Oid<'_>],
553 caps: &Caps,
554 out: &mut dyn std::io::Write,
555 ) -> Result<PackStats> {
556 self.emit_oids(objects, have, caps, out)
557 }
558
559 /// **`want` minus `have`, out of §13's bitmaps, with no object opened** —
560 /// plus the two facts the caller cannot recompute cheaply.
561 ///
562 /// # The refusal is the part that matters
563 ///
564 /// [`GitOps::reachable`] treats a `want` it has no bitmap for as contributing
565 /// **itself and nothing else**. That is the safe direction for its own
566 /// caller — `live_set` over-keeps, and over-keeping is harmless — and a
567 /// silent **under-send** for this one. A clone served that way exits zero
568 /// with one object per branch.
569 ///
570 /// So every tip is checked against the store's commit graph first, and a tip
571 /// that is not in it declines the whole request.
572 ///
573 /// # 🔴 *"In the graph"* and *"has a bitmap"* are NO LONGER the same fact
574 ///
575 /// They were, and this paragraph used to say so, because the live table was
576 /// built with no commit cap precisely so that they would be. That cost 530
577 /// 218 allocations a fetch (see [`crate::git_ops`]'s `LIVE_REACH_COMMITS`),
578 /// and since 2026-08-14 the table is **sampled** at 512 like the sealed
579 /// archive's.
580 ///
581 /// The check below is unchanged and is still exact, but it is now exact for
582 /// a different reason. It is *"in the graph"* that matters, not *"has a
583 /// bitmap"*: [`crate::reach::accumulate`] walks a bitmapless commit down to
584 /// the first bitmapped one behind it, so every graph commit is answerable,
585 /// with or without a bitmap of its own. A tip that is not a graph row is
586 /// still refused — there is nothing to walk — and that is the case this
587 /// check exists for.
588 ///
589 /// `Ok(None)` is therefore reachable in three ways, and all three are the
590 /// same statement — *the projection does not cover this request*:
591 /// an empty graph (which a clean restart produces), a tip that is not a
592 /// commit in it, and a graph row whose oid does not parse.
593 ///
594 /// # The selection is NOT re-ordered, and that is measured
595 ///
596 /// `reachable` answers in ordinal order, which is oid-lexicographic and
597 /// therefore unrelated to where the bytes are. Sorting by archive offset so
598 /// a delta's base precedes it was written and could not be made to fail:
599 /// **measured 2026-08-08 on a 154-object fixture — with the sort, without
600 /// it, and with the whole selection deliberately reversed, all three give
601 /// `copied=154 recompressed=0` and the identical object graph**, because
602 /// [`crate::pack_walk::topological_order`] orders the emission itself. The
603 /// sort was a batch index lookup per request buying a property the emitter
604 /// already owns (LAW 5), and its absence is something no guard can see
605 /// (LAW 2). It is gone.
606 fn select(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> Result<Option<ReachSet>> {
607 // The cheapest form of the same refusal, and the one a clean restart
608 // takes: with no commit graph there is no bitmap for any want, so
609 // `reachable` would contribute the wants alone.
610 if self.commit_count() == 0 {
611 return Ok(None);
612 }
613
614 // The graph as raw oids. Needed twice — to refuse a tip it does not
615 // cover, and to name the commits in the answer.
616 //
617 // **Folded, not rebuilt per request.** This used to be
618 // `graph_snapshot()` — a deep clone of every `CommitNode`, oid `String`,
619 // parent list and all — followed by a `hex::decode` into a fresh
620 // `Vec<u8>` per commit, on every single `select`. The set cannot change
621 // between folds, so [`crate::git_ops::Derived::commit_raw`] holds it and
622 // this borrows. `None` is the identical refusal the decode arm used to
623 // produce: a graph row whose oid does not parse is a corrupt derivation,
624 // and declining is safe because the caller's own walk still serves the
625 // request.
626 let Some(commits) = self.commit_oids_raw()? else {
627 return Ok(None);
628 };
629 if want.iter().any(|tip| !commits.contains(*tip)) {
630 return Ok(None);
631 }
632
633 let objects = self
634 .reachable(want, have)
635 .context("selecting want minus have")?;
636
637 // What the client held BEFORE this transfer: the closure of `have`, and
638 // only it. Empty for a clone, which is the request with no excludes at
639 // all and therefore the one that pays nothing for this.
640 //
641 // **Raw bytes, in one buffer.** This is the repository-sized half of the
642 // answer — the closure of what the client already holds, 12 112 oids to
643 // serve a 69-object fetch on the measured corpus — and it used to be
644 // built as oid hex and `hex::decode`d back into a `Vec<u8>` per object
645 // on its way into a `Vec<Vec<u8>>` the caller only ever counts and
646 // scans. `git_storage_trait::OidList` carries it in one allocation; see
647 // `Derived::oids_raw`.
648 let client_has = if have.is_empty() {
649 git_storage_trait::OidList::new()
650 } else {
651 self.reachable_raw(have, &[])
652 .context("closing over what the client already holds")?
653 };
654
655 let selected_commits = objects
656 .iter()
657 .filter(|oid| commits.contains(*oid))
658 .cloned()
659 .collect();
660
661 Ok(Some(ReachSet {
662 objects,
663 commits: selected_commits,
664 client_has,
665 }))
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672 use crate::object::{canonical, GitHashKind};
673 use crate::store::tests::{one_blob_pack, real_pack, tmpdir};
674 use git_storage_trait::{Observed, RefCas, RefRejection};
675
676 /// **`header` names the RESOLVED kind for every entry in a real pack —
677 /// deltas included — and it does it with no gix and no inflate.**
678 ///
679 /// The oracle is [`crate::resolve::Resolved`], which carries both
680 /// `stored_type` (what the entry is in the pack) and `kind` (what the chain
681 /// resolves to). Asserting against `kind` is the whole point: a `header`
682 /// that read the type column would agree on every non-delta and be wrong on
683 /// exactly the deltas, which is the failure a spot-check misses.
684 ///
685 /// The test refuses to pass on a corpus that would not have caught that: it
686 /// asserts the pack actually contained deltas first.
687 #[test]
688 fn header_resolves_every_delta_to_its_real_kind() {
689 let dir = tmpdir("serve-header");
690 let store = GitStore::open(&dir, "rickard").unwrap();
691 let (pack, rows) = real_pack();
692 store.put(&pack, &[]).unwrap();
693 store.wait_indexed();
694 store.absorb_pending().unwrap();
695
696 let deltas = rows
697 .iter()
698 .filter(|r| matches!(r.stored_type, ObjType::OfsDelta | ObjType::RefDelta))
699 .count();
700 assert!(
701 deltas > 0,
702 "this corpus pack has no delta entries, so it cannot tell a chain walk from a column \
703 read and the test proves nothing"
704 );
705
706 for r in &rows {
707 let (kind, size) = GitServe::header(&store, &r.oid)
708 .unwrap()
709 .unwrap_or_else(|| panic!("{} is in the pack but header said absent", hex::encode(&r.oid)));
710 assert_eq!(
711 kind,
712 resolved_type(r.kind),
713 "{} is stored as {:?} and resolves to {:?}; header said {:?}",
714 hex::encode(&r.oid),
715 r.stored_type,
716 r.kind,
717 kind
718 );
719 assert_eq!(
720 size,
721 r.uncompressed_size,
722 "{} post-resolution size",
723 hex::encode(&r.oid)
724 );
725 }
726 }
727
728 /// `sizes` answers the same numbers as `header`, in one pass, positionally.
729 #[test]
730 fn sizes_is_header_in_bulk_and_stays_positional() {
731 let dir = tmpdir("serve-sizes");
732 let store = GitStore::open(&dir, "rickard").unwrap();
733 let (pack, rows) = real_pack();
734 store.put(&pack, &[]).unwrap();
735 store.wait_indexed();
736
737 // An oid nothing has, deliberately in the MIDDLE: a `sizes` that
738 // filtered misses rather than answering `None` in place would shift
739 // every later answer by one and still return the right count.
740 let absent = vec![0xABu8; 20];
741 let mut oids: Vec<&[u8]> = rows.iter().map(|r| r.oid.as_slice()).collect();
742 let middle = oids.len() / 2;
743 oids.insert(middle, &absent);
744
745 let got = GitServe::sizes(&store, &oids).unwrap();
746 assert_eq!(got.len(), oids.len(), "sizes must be positional");
747 assert_eq!(got[middle], None, "an absent oid answers None IN PLACE");
748 for (i, r) in rows.iter().enumerate() {
749 let at = if i < middle { i } else { i + 1 };
750 assert_eq!(got[at], Some(r.uncompressed_size), "row {i}");
751 }
752 }
753
754 /// **`HEAD` is not a row in `refs()`, and it has its own accessor** — §3f
755 /// Q1, and the conformance check the suite could not previously make.
756 ///
757 /// Seen RED by deleting the filter in [`GitOps::refs`]: `HEAD` appears in
758 /// the ref stream and the first assertion fails. The two halves are both
759 /// needed — a `refs()` that dropped `HEAD` and a `head()` that could not
760 /// find it would pass the first assertion and leave the repository with no
761 /// default branch to advertise.
762 #[test]
763 fn head_is_not_a_ref_row_but_is_reachable_through_its_own_accessor() {
764 let dir = tmpdir("serve-head");
765 let store = GitStore::open(&dir, "rickard").unwrap();
766 let (pack, oid) = one_blob_pack(b"a blob to point at");
767 store.put(&pack, &[]).unwrap();
768 store.wait_indexed();
769 store
770 .put_refs(&[RefUpdate::set("refs/heads/main", hex::encode(&oid))])
771 .unwrap();
772
773 GitServe::set_head(&store, "refs/heads/main").unwrap();
774
775 let rows = store.refs().unwrap();
776 assert!(
777 rows.iter().all(|r| r.name != HEAD),
778 "HEAD is a pseudo-ref and must not appear in the ref stream: {:?}",
779 rows.iter().map(|r| &r.name).collect::<Vec<_>>()
780 );
781 assert!(
782 rows.iter().any(|r| r.name == "refs/heads/main"),
783 "filtering HEAD must not have filtered anything else"
784 );
785
786 let head = GitServe::head(&store).unwrap().expect("HEAD was just set");
787 assert_eq!(head.name, HEAD);
788 assert_eq!(head.symref_target.as_deref(), Some("refs/heads/main"));
789 assert_eq!(head.oid, None, "a symbolic HEAD has no direct target");
790
791 // A store that has never been pointed has no HEAD, and that is `None`
792 // rather than an error or an empty row.
793 let dir2 = tmpdir("serve-head-empty");
794 let fresh = GitStore::open(&dir2, "rickard").unwrap();
795 assert!(GitServe::head(&fresh).unwrap().is_none());
796 }
797
798 /// `set_head` refuses a target that is neither a ref name nor an oid rather
799 /// than guessing, and takes a detached oid when given one.
800 #[test]
801 fn set_head_takes_a_ref_or_an_oid_and_refuses_anything_else() {
802 let dir = tmpdir("serve-sethead");
803 let store = GitStore::open(&dir, "rickard").unwrap();
804 let (pack, oid) = one_blob_pack(b"detached");
805 store.put(&pack, &[]).unwrap();
806 store.wait_indexed();
807
808 let err = GitServe::set_head(&store, "main").unwrap_err();
809 assert!(
810 format!("{err:#}").contains("is neither"),
811 "a bare branch name must be refused, not guessed at: {err:#}"
812 );
813
814 GitServe::set_head(&store, &hex::encode(&oid)).unwrap();
815 let head = GitServe::head(&store).unwrap().unwrap();
816 assert_eq!(head.oid.as_deref(), Some(oid.as_slice()));
817 assert_eq!(head.symref_target, None);
818 }
819
820 /// **`select` declines rather than under-sending when it has no graph — and
821 /// `emit_pack` is not affected by that at all.**
822 ///
823 /// The load-bearing `Ok(None)`. A store holding one blob and no commit has
824 /// no reachability projection, and the wrong answer here is not an error —
825 /// it is a `Some` containing the want alone, which serves a clone that exits
826 /// zero and is short by everything.
827 ///
828 /// # What the second half of this test used to assert, and why it changed
829 ///
830 /// Until 2026-08-10 it asserted that `emit_pack` **refused** on the back of
831 /// this same decline, because `emit_pack` began by calling `select`. It no
832 /// longer does — that call was the over-send bug, see
833 /// [`GitServe::emit_pack`] — so the assertion would now be asserting the
834 /// defect. It is **replaced rather than deleted**, by the stronger statement
835 /// the separation makes true: a store that cannot select *anything* still
836 /// emits exactly the set it is handed. The refusal is not lost, it has an
837 /// owner: `select` says `None`, and the caller decides.
838 #[test]
839 fn select_declines_when_the_projection_cannot_cover_the_request() {
840 let dir = tmpdir("serve-select-none");
841 let store = GitStore::open(&dir, "rickard").unwrap();
842 let (pack, oid) = one_blob_pack(b"no commits here");
843 store.put(&pack, &[]).unwrap();
844 store.wait_indexed();
845 store.absorb_pending().unwrap();
846
847 assert_eq!(store.commit_count(), 0, "the fixture must have no commits");
848 assert!(
849 GitServe::select(&store, &[&oid], &[]).unwrap().is_none(),
850 "a store with no commit graph must decline, not answer with the tip alone"
851 );
852
853 // And `emit_pack` is INDEPENDENT of that decline: it was handed one
854 // object and it emits one object, out of the very store whose
855 // projection cannot answer a thing. A single blob is a legitimate set
856 // to emit — it is what a `--filter` fetch of one path looks like — and
857 // an engine that consulted `select` here would refuse it.
858 let mut sink = Vec::new();
859 let stats = GitServe::emit_pack(&store, &[&oid], &[], &Caps::modern(), &mut sink).unwrap();
860 assert_eq!(
861 stats.objects, 1,
862 "emit_pack emits what it is given; it does not ask select whether it may"
863 );
864 assert_eq!(
865 u32::from_be_bytes(sink[8..12].try_into().unwrap()),
866 1,
867 "the emitted pack header must count what the receipt counts"
868 );
869 }
870
871 /// One entry of a hand-built pack.
872 ///
873 /// [`one_blob_pack`] cannot express a graph and [`real_pack`] is whatever
874 /// this machine happens to have. Two properties below need a pack whose
875 /// **exact** shape is chosen rather than found: which objects point at which,
876 /// and — for the narrowed-clone test — which entry is stored as a delta
877 /// against which other entry.
878 enum Item {
879 Whole(GitObjectKind, Vec<u8>),
880 /// An `OFS_DELTA` against the item at index `base`, reconstructing
881 /// `body`.
882 OfsDelta { base: usize, body: Vec<u8> },
883 }
884
885 impl Item {
886 /// The resolved body, which is what a delta against this item states as
887 /// its source size.
888 fn body(&self) -> &[u8] {
889 match self {
890 Item::Whole(_, b) | Item::OfsDelta { body: b, .. } => b,
891 }
892 }
893 }
894
895 fn deflate(bytes: &[u8]) -> Vec<u8> {
896 use std::io::Write;
897 let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
898 e.write_all(bytes).unwrap();
899 e.finish().unwrap()
900 }
901
902 /// git's delta-header varint — LEB128, unlike the `OFS_DELTA` distance.
903 fn delta_varint(out: &mut Vec<u8>, mut n: u64) {
904 loop {
905 let mut b = (n & 0x7f) as u8;
906 n >>= 7;
907 if n > 0 {
908 b |= 0x80;
909 }
910 out.push(b);
911 if n == 0 {
912 return;
913 }
914 }
915 }
916
917 /// A delta stream that rebuilds `target` by **pure insertion**.
918 ///
919 /// Legal git delta encoding, and the reason it is used here rather than a
920 /// copy-based one: the base's *content* becomes irrelevant while its
921 /// *identity* stays load-bearing, so the fixture can name any base it likes
922 /// and still produce a target of its own choosing. `source_size` must still
923 /// match the base exactly — git checks it and refuses otherwise.
924 fn insert_only_delta(base_len: usize, target: &[u8]) -> Vec<u8> {
925 let mut d = Vec::new();
926 delta_varint(&mut d, base_len as u64);
927 delta_varint(&mut d, target.len() as u64);
928 for chunk in target.chunks(0x7f) {
929 d.push(chunk.len() as u8);
930 d.extend_from_slice(chunk);
931 }
932 d
933 }
934
935 /// Assemble `items` into a packfile, in the order given.
936 ///
937 /// The trailer is zeroed: nothing on the absorb path verifies it, and a
938 /// fixture that had to be re-hashed on every edit would be a second thing to
939 /// get wrong.
940 fn build_pack(items: &[Item]) -> Vec<u8> {
941 let mut pack = b"PACK".to_vec();
942 pack.extend_from_slice(&2u32.to_be_bytes());
943 pack.extend_from_slice(&(items.len() as u32).to_be_bytes());
944 let mut offsets: Vec<u64> = Vec::with_capacity(items.len());
945 for (i, item) in items.iter().enumerate() {
946 offsets.push(pack.len() as u64);
947 match item {
948 Item::Whole(kind, body) => {
949 crate::pack_walk::encode_type_and_size(
950 &mut pack,
951 resolved_type(*kind),
952 body.len() as u64,
953 );
954 pack.extend_from_slice(&deflate(body));
955 }
956 Item::OfsDelta { base, body } => {
957 let delta = insert_only_delta(items[*base].body().len(), body);
958 crate::pack_walk::encode_type_and_size(
959 &mut pack,
960 ObjType::OfsDelta,
961 delta.len() as u64,
962 );
963 crate::pack_walk::encode_ofs_distance(&mut pack, offsets[i] - offsets[*base]);
964 pack.extend_from_slice(&deflate(&delta));
965 }
966 }
967 }
968 pack.extend_from_slice(&[0u8; 20]);
969 pack
970 }
971
972 /// A whole-object pack: every entry whole — no `OFS_DELTA`, no `REF_DELTA` —
973 /// which is what makes "the emitted count equals the requested count" a
974 /// statement about reachability closure and not about delta-base handling.
975 fn whole_object_pack(objects: &[(GitObjectKind, Vec<u8>)]) -> Vec<u8> {
976 let items: Vec<Item> = objects
977 .iter()
978 .map(|(k, b)| Item::Whole(*k, b.clone()))
979 .collect();
980 build_pack(&items)
981 }
982
983 /// One commit, its tree, and two blobs under that tree — the smallest graph
984 /// in which the closure of a subset is strictly larger than the subset.
985 ///
986 /// Returns `(pack, commit_oid, tree_oid, blob_oids)`, all raw (not hex).
987 fn commit_tree_two_blobs() -> (Vec<u8>, Vec<u8>, Vec<u8>, Vec<Vec<u8>>) {
988 let hash = GitHashKind::Sha1;
989 let bodies: [&[u8]; 2] = [b"first blob", b"second blob"];
990 let mut blobs = Vec::new();
991 let mut objects = Vec::new();
992 let mut tree_payload = Vec::new();
993 for (name, body) in ["a.txt", "b.txt"].iter().zip(bodies) {
994 let canon = canonical(GitObjectKind::Blob, body);
995 let oid = hash.oid_of(&canon);
996 tree_payload.extend_from_slice(format!("100644 {name}\0").as_bytes());
997 tree_payload.extend_from_slice(&oid);
998 objects.push((GitObjectKind::Blob, body.to_vec()));
999 blobs.push(oid);
1000 }
1001 let tree_canon = canonical(GitObjectKind::Tree, &tree_payload);
1002 let tree_oid = hash.oid_of(&tree_canon);
1003
1004 let body = format!(
1005 "tree {}\nauthor A <a@x> 1700000000 +0000\ncommitter A <a@x> 1700000000 +0000\n\n\
1006 the closure fixture\n",
1007 hex::encode(&tree_oid)
1008 );
1009 let commit_canon = canonical(GitObjectKind::Commit, body.as_bytes());
1010 let commit_oid = hash.oid_of(&commit_canon);
1011
1012 objects.push((GitObjectKind::Tree, tree_payload));
1013 objects.push((GitObjectKind::Commit, body.into_bytes()));
1014 (whole_object_pack(&objects), commit_oid, tree_oid, blobs)
1015 }
1016
1017 /// 🔴 **`emit_pack` emits the set it is HANDED, and never that set's
1018 /// reachability closure.**
1019 ///
1020 /// This is the guard for the defect the parameter rename names. Until
1021 /// 2026-08-10 this method read
1022 ///
1023 /// ```text
1024 /// let Some(set) = self.select(objects, have)? else { bail!(..) };
1025 /// self.emit_oids(&set.objects, caps, out)
1026 /// ```
1027 ///
1028 /// — it **closed over its own input**. Upload-pack hands it
1029 /// `selection.objects`, which is already post-filter, post-shallow and
1030 /// post-`include-tag` and is *deliberately not closed*, so the closure added
1031 /// back exactly what `--filter=blob:none`, `--filter=tree:0` or `--depth=N`
1032 /// had excluded.
1033 ///
1034 /// **Nothing else in the tree can see that.** The over-sent pack passes
1035 /// `git index-pack --strict` and `git fsck`, the clone succeeds, the exit
1036 /// code is zero, and the client silently receives objects it asked not to
1037 /// have — `P-027`'s shape, a change no test can see. So the assertion is on
1038 /// a **count**: never on bytes, never on a clock.
1039 ///
1040 /// Two shapes, because the closure broke the method in two different
1041 /// directions and both had to stop:
1042 ///
1043 /// 1. `[commit]` alone — a real `--filter=tree:0` fetch. **RED before the
1044 /// fix: `objects` was 4, not 1** (the tree and both blobs came back).
1045 /// 2. `[commit, tree]` — a set whose members are not all commits. **RED
1046 /// before the fix with a refusal, not a count**: the old body routed
1047 /// through `select`, whose tip check rejects a non-commit tip, so a
1048 /// perfectly ordinary partial-clone selection could not be emitted at
1049 /// all.
1050 ///
1051 /// The fixture asserts its own premise first — that the closure really is
1052 /// strictly larger than either input — or it would prove nothing on a
1053 /// repository where the two happened to coincide.
1054 #[test]
1055 fn emit_pack_emits_exactly_the_set_it_is_given_and_never_its_closure() {
1056 let dir = tmpdir("serve-emit-exact");
1057 let store = GitStore::open(&dir, "rickard").unwrap();
1058 let (pack, commit, tree, blobs) = commit_tree_two_blobs();
1059 store.put(&pack, &[]).unwrap();
1060 store.wait_indexed();
1061 store.absorb_pending().unwrap();
1062
1063 // The premise. Without it the two assertions below are satisfied by a
1064 // store that happens to hold nothing beneath the commit.
1065 assert_eq!(
1066 store.commit_count(),
1067 1,
1068 "the fixture must have a commit graph"
1069 );
1070 let closure = GitServe::select(&store, &[&commit], &[])
1071 .unwrap()
1072 .expect("the projection covers a commit it folded");
1073 assert_eq!(
1074 closure.objects.len(),
1075 4,
1076 "the fixture's closure must be strictly larger than the subsets emitted below, or \
1077 this test cannot tell an exact emission from a closed one: {:?}",
1078 closure.objects.iter().map(hex::encode).collect::<Vec<_>>()
1079 );
1080 for b in &blobs {
1081 assert!(
1082 closure.objects.contains(b),
1083 "the closure must reach the blobs — that is what must NOT be emitted"
1084 );
1085 }
1086 // The mechanism behind shape 2 below, pinned as a live fact rather than
1087 // left as history: `select` declines a tip that is not a commit in its
1088 // graph, which is correct for `select` and is exactly why routing
1089 // `emit_pack` through it made an ordinary selection unservable.
1090 assert!(
1091 GitServe::select(&store, &[&commit, &tree], &[])
1092 .unwrap()
1093 .is_none(),
1094 "select is a close-over-TIPS question and a tree is not a tip it can answer for"
1095 );
1096
1097 // 1. `--filter=tree:0`: the commit and nothing else.
1098 let mut out = Vec::new();
1099 let stats =
1100 GitServe::emit_pack(&store, &[&commit], &[], &Caps::modern(), &mut out).unwrap();
1101 assert_eq!(
1102 stats.objects, 1,
1103 "emit_pack was handed ONE object and must emit ONE; a closure here re-adds exactly \
1104 what the filter excluded, and index-pack --strict and fsck both accept the result"
1105 );
1106 // The receipt and the bytes must agree: the pack's own object-count
1107 // field is bytes 8..12, and reading it back is the check that the
1108 // counter is not simply reporting what it was asked for.
1109 assert_eq!(
1110 u32::from_be_bytes(out[8..12].try_into().unwrap()),
1111 1,
1112 "the emitted pack header must count what the receipt counts"
1113 );
1114
1115 // 2. A post-filter selection whose members are not all commits.
1116 let mut out = Vec::new();
1117 let stats =
1118 GitServe::emit_pack(&store, &[&commit, &tree], &[], &Caps::modern(), &mut out).unwrap();
1119 assert_eq!(
1120 stats.objects, 2,
1121 "emit_pack was handed TWO objects and must emit TWO"
1122 );
1123 assert_eq!(
1124 u32::from_be_bytes(out[8..12].try_into().unwrap()),
1125 2,
1126 "the emitted pack header must count what the receipt counts"
1127 );
1128 assert_eq!(stats.copied, stats.objects, "still a byte-range copy");
1129 assert_eq!(stats.recompressed, 0, "still nothing re-deflated");
1130 }
1131
1132 /// Two branches whose trees are near-identical, with the **second branch's
1133 /// tree stored as a delta against the first branch's tree**.
1134 ///
1135 /// That one fact is the whole fixture. Narrow a request to `base` and it
1136 /// contains a delta whose base is not in it — and the base is a *tree*, so
1137 /// pulling it in would drag an object owing children the request does not
1138 /// contain. It is the `h2h-linear-sha1-2048c-1024f-16k` failure in six
1139 /// objects.
1140 ///
1141 /// `shared_entries` is how many tree entries the two trees hold in **common**
1142 /// before the one they differ in. It is `0` for the connectivity test, which
1143 /// wants the smallest fixture that has the defect in it, and large for the
1144 /// thin-pack test, which has to be able to see the delta *win* in bytes: at
1145 /// 0 the whole tree is 33 bytes and a `REF_DELTA`'s 20-byte base oid costs
1146 /// more than the delta saves, so a byte assertion there would measure the
1147 /// fixture rather than the mechanism. The shared entries all name `b_main`,
1148 /// so nothing new becomes reachable and no tree owes a child that is not in
1149 /// one of the two closures.
1150 ///
1151 /// Returns the pack and, in order, `(c_main, t_main, b_main, c_base,
1152 /// t_base, b_base)`, all raw.
1153 #[allow(clippy::type_complexity)]
1154 fn two_branches_with_a_cross_branch_delta(shared_entries: usize) -> (Vec<u8>, [Vec<u8>; 6]) {
1155 let hash = GitHashKind::Sha1;
1156 let oid_of = |kind, body: &[u8]| hash.oid_of(&canonical(kind, body));
1157
1158 let b_main = b"the payload that only main can reach\n".to_vec();
1159 let b_main_oid = oid_of(GitObjectKind::Blob, &b_main);
1160 let b_base = b"the payload that only base can reach\n".to_vec();
1161 let b_base_oid = oid_of(GitObjectKind::Blob, &b_base);
1162
1163 let tree_for = |blob: &[u8]| {
1164 let mut t = Vec::new();
1165 // Sorted, and `zz.txt` sorts after every `fNN.txt`: an unsorted tree
1166 // is what `fsck --strict` rejects, and the oracles below run it.
1167 for i in 0..shared_entries {
1168 t.extend_from_slice(format!("100644 f{i:03}.txt\0").as_bytes());
1169 t.extend_from_slice(&b_main_oid);
1170 }
1171 t.extend_from_slice(b"100644 zz.txt\0");
1172 t.extend_from_slice(blob);
1173 t
1174 };
1175 let t_main = tree_for(&b_main_oid);
1176 let t_main_oid = oid_of(GitObjectKind::Tree, &t_main);
1177 let t_base = tree_for(&b_base_oid);
1178 let t_base_oid = oid_of(GitObjectKind::Tree, &t_base);
1179
1180 let commit_for = |tree: &[u8], msg: &str| {
1181 format!(
1182 "tree {}\nauthor A <a@x> 1700000000 +0000\ncommitter A <a@x> 1700000000 +0000\n\n\
1183 {msg}\n",
1184 hex::encode(tree)
1185 )
1186 .into_bytes()
1187 };
1188 let c_main = commit_for(&t_main_oid, "main");
1189 let c_main_oid = oid_of(GitObjectKind::Commit, &c_main);
1190 let c_base = commit_for(&t_base_oid, "base");
1191 let c_base_oid = oid_of(GitObjectKind::Commit, &c_base);
1192
1193 // `t_base` is entry 4 and deltas against entry 1, `t_main`. Both trees
1194 // are 33 bytes and differ in 20 of them, which is what makes this the
1195 // delta a real packer would also choose.
1196 let pack = build_pack(&[
1197 Item::Whole(GitObjectKind::Blob, b_main),
1198 Item::Whole(GitObjectKind::Tree, t_main),
1199 Item::Whole(GitObjectKind::Commit, c_main),
1200 Item::Whole(GitObjectKind::Blob, b_base),
1201 Item::OfsDelta {
1202 base: 1,
1203 body: t_base,
1204 },
1205 Item::Whole(GitObjectKind::Commit, c_base),
1206 ]);
1207 (
1208 pack,
1209 [
1210 c_main_oid, t_main_oid, b_main_oid, c_base_oid, t_base_oid, b_base_oid,
1211 ],
1212 )
1213 }
1214
1215 /// 🔴 **A NARROWED clone gets a pack that is self-contained *and connected*
1216 /// — the delta base is not smuggled in with it.**
1217 ///
1218 /// The defect, seen live on 2026-08-11 against
1219 /// `h2h-linear-sha1-2048c-1024f-16k`:
1220 ///
1221 /// ```text
1222 /// git clone --bare <url> → 36 172 objects, OK
1223 /// git clone --bare --single-branch --branch base → FAILS
1224 /// fatal: did not receive expected object 8601ec33920b7d701c9887fa04916501136d6e90
1225 /// fatal: fetch-pack: invalid index-pack output
1226 /// event="git.upload_pack.served" objects=33773 ← the server logged SUCCESS
1227 /// ```
1228 ///
1229 /// `base` reaches 31 805 objects; `emit_set`'s delta-base closure added
1230 /// 1 968 more, giving exactly the 33 773 the server logged. **507 of the
1231 /// additions were trees**, and between them they named 204 objects the pack
1232 /// did not contain. `git index-pack --check-self-contained-and-connected` —
1233 /// what a clone runs, and which turns on `strict` — walks every received
1234 /// object's links and demands each one exist, so it died on the first, and
1235 /// nothing on the server ever knew.
1236 ///
1237 /// # Why the assertions are shaped the way they are
1238 ///
1239 /// The premise is asserted first and it is the load-bearing one: the request
1240 /// must actually contain a delta whose base is outside it. On a corpus where
1241 /// that happens not to hold — which is every full clone, by construction —
1242 /// this test cannot fail no matter what the code does.
1243 ///
1244 /// Then the **count**, not an exit code: the emitted pack must hold exactly
1245 /// the three objects `base` reaches. **RED before the fix at 4**, the fourth
1246 /// being `t_main`.
1247 ///
1248 /// Then stock git, in a **repository**: `index-pack --strict` outside one
1249 /// segfaults instead of judging (exit 139, no output), which is a green that
1250 /// was never looked at. Inside a fresh bare repo it is the exact arbiter that
1251 /// failed in production, and before the fix it prints `did not receive
1252 /// expected object <b_main>`.
1253 #[test]
1254 fn a_narrowed_clone_is_served_a_connected_pack_not_its_delta_bases() {
1255 let dir = tmpdir("serve-narrowed");
1256 let store = GitStore::open(&dir, "rickard").unwrap();
1257 let (pack, [c_main, t_main, b_main, c_base, t_base, b_base]) =
1258 two_branches_with_a_cross_branch_delta(0);
1259 store.put(&pack, &[]).unwrap();
1260 store.wait_indexed();
1261 store.absorb_pending().unwrap();
1262 assert_eq!(store.commit_count(), 2, "both branches must be in the graph");
1263
1264 // ── the narrowed request ──────────────────────────────────────────────
1265 let set = GitServe::select(&store, &[&c_base], &[])
1266 .unwrap()
1267 .expect("the projection covers a commit it folded");
1268 let mut want = set.objects.clone();
1269 want.sort();
1270 let mut expected = vec![c_base.clone(), t_base.clone(), b_base.clone()];
1271 expected.sort();
1272 assert_eq!(
1273 want,
1274 expected,
1275 "`base` reaches its own commit, tree and blob and nothing of main's"
1276 );
1277
1278 // ── THE PREMISE, or this test proves nothing ──────────────────────────
1279 //
1280 // `t_base` must really be stored as a delta against `t_main`, and
1281 // `t_main` must really be outside the request. Without both, an
1282 // implementation that pulls bases in is indistinguishable from one that
1283 // does not.
1284 let row = store
1285 .index()
1286 .lookup(&t_base)
1287 .expect("the tree base reaches is indexed");
1288 assert_eq!(
1289 row.obj_type,
1290 ObjType::OfsDelta,
1291 "the fixture must store base's tree as a delta or there is no base to leave out"
1292 );
1293 let base_row = store
1294 .index()
1295 .lookup(&t_main)
1296 .expect("the tree main reaches is indexed");
1297 assert_eq!(
1298 row.delta_base, base_row.offset,
1299 "the fixture's delta must name main's tree as its base"
1300 );
1301 assert!(
1302 !set.objects.contains(&t_main),
1303 "the base must be OUTSIDE the request, or nothing is being closed over"
1304 );
1305
1306 // ── the count ─────────────────────────────────────────────────────────
1307 let oids: Vec<Oid<'_>> = set.objects.iter().map(Vec::as_slice).collect();
1308 let mut out = Vec::new();
1309 let stats = store.emit_oids(&oids, &[], &Caps::modern(), &mut out).unwrap();
1310 assert_eq!(
1311 stats.objects, 3,
1312 "a narrowed clone must be served exactly the objects it reaches; adding the delta \
1313 base makes it 4 and the fourth owes children the pack does not carry"
1314 );
1315 assert_eq!(
1316 u32::from_be_bytes(out[8..12].try_into().unwrap()),
1317 3,
1318 "the emitted pack header must count what the receipt counts"
1319 );
1320 // Applied output for the mechanism itself: two entries copied byte for
1321 // byte, and exactly the one whose base was left out rebuilt.
1322 assert_eq!(stats.copied, 2, "everything but the boundary entry is copied");
1323 assert_eq!(
1324 stats.recompressed, 1,
1325 "the entry whose base is outside the request is the one that must be rebuilt whole"
1326 );
1327 assert_eq!(stats.copied + stats.recompressed, stats.objects);
1328
1329 // ── and the object main reaches must not be in the bytes ──────────────
1330 let walked = crate::pack_walk::walk(&out, 20).expect("our own walk reads what we emitted");
1331 assert_eq!(walked.entries.len(), 3);
1332 assert!(
1333 walked.entries.iter().all(|e| e.obj_type != ObjType::OfsDelta),
1334 "the boundary entry must be whole, not a delta naming a base that is not here"
1335 );
1336
1337 // ── stock git, inside a repository, as the arbiter ────────────────────
1338 let scratch = tmpdir("serve-narrowed-idx");
1339 crate::git_oracle::assert_git_accepts(
1340 &scratch,
1341 "dst.git",
1342 &out,
1343 crate::git_oracle::Strictness::Connected,
1344 );
1345
1346 // The full clone is unaffected, and that is asserted rather than
1347 // assumed: every base is inside a whole-repository request, so nothing
1348 // is rebuilt and the copy claim survives intact.
1349 let all = vec![
1350 c_main.as_slice(),
1351 t_main.as_slice(),
1352 b_main.as_slice(),
1353 c_base.as_slice(),
1354 t_base.as_slice(),
1355 b_base.as_slice(),
1356 ];
1357 let mut full = Vec::new();
1358 let stats = store.emit_oids(&all, &[], &Caps::modern(), &mut full).unwrap();
1359 assert_eq!(stats.objects, 6);
1360 assert_eq!(
1361 stats.recompressed, 0,
1362 "a full clone contains every base, so it must still be a pure byte-range copy"
1363 );
1364 assert_eq!(stats.copied, 6);
1365 }
1366
1367 /// **Three revisions of one 16 KiB blob, the middle one off-branch**, which
1368 /// is the `h2h-linear-sha1-2048c-1024f-16k` boundary entry in nine objects.
1369 ///
1370 /// The chain is `v1 <- v2 <- v3`, stored in that order, and only `v2` is
1371 /// off-branch. So a request for `base` holds `v1` and `v3` and not `v2`:
1372 /// `v3`'s stored base is outside it, and `v3`'s **chain ancestor `v1` is
1373 /// inside it**. That second fact is the whole fixture — without it the
1374 /// re-delta has nothing to aim at, and the measured production shape is
1375 /// exactly this one (859 of 961 boundary entries have such an ancestor).
1376 ///
1377 /// The blob bodies are incompressible noise with a small splice between
1378 /// revisions, deliberately: on compressible filler a whole rebuild is nearly
1379 /// free and a byte assertion would be measuring zlib rather than the delta.
1380 /// At 16 KiB of noise, whole costs ~16.5 KB on the wire and a delta costs a
1381 /// few hundred bytes, so the two cannot be confused.
1382 ///
1383 /// Returns the pack and `(c_base_tip, t3, v3, c1, t1, v1, c_main, t2, v2)`.
1384 #[allow(clippy::type_complexity)]
1385 fn three_revisions_with_the_middle_one_off_branch() -> (Vec<u8>, [Vec<u8>; 9]) {
1386 let hash = GitHashKind::Sha1;
1387 let oid_of = |kind, body: &[u8]| hash.oid_of(&canonical(kind, body));
1388
1389 // xorshift, so the bytes are reproducible and do not deflate.
1390 let noise = |n: usize, seed: u64| -> Vec<u8> {
1391 let mut s = seed | 1;
1392 (0..n)
1393 .map(|_| {
1394 s ^= s << 13;
1395 s ^= s >> 7;
1396 s ^= s << 17;
1397 (s >> 33) as u8
1398 })
1399 .collect::<Vec<u8>>()
1400 };
1401 let v1 = noise(16 * 1024, 0xC0FFEE);
1402 let mut v2 = v1.clone();
1403 v2[2048..2112].copy_from_slice(&noise(64, 2));
1404 let mut v3 = v2.clone();
1405 v3[8192..8256].copy_from_slice(&noise(64, 3));
1406
1407 let v1_oid = oid_of(GitObjectKind::Blob, &v1);
1408 let v2_oid = oid_of(GitObjectKind::Blob, &v2);
1409 let v3_oid = oid_of(GitObjectKind::Blob, &v3);
1410
1411 let tree_for = |blob: &[u8]| {
1412 let mut t = b"100644 f.bin\0".to_vec();
1413 t.extend_from_slice(blob);
1414 t
1415 };
1416 let (t1, t2, t3) = (tree_for(&v1_oid), tree_for(&v2_oid), tree_for(&v3_oid));
1417 let (t1_oid, t2_oid, t3_oid) = (
1418 oid_of(GitObjectKind::Tree, &t1),
1419 oid_of(GitObjectKind::Tree, &t2),
1420 oid_of(GitObjectKind::Tree, &t3),
1421 );
1422
1423 let commit_for = |tree: &[u8], parent: Option<&[u8]>, msg: &str| {
1424 let mut c = format!("tree {}\n", hex::encode(tree));
1425 if let Some(p) = parent {
1426 c.push_str(&format!("parent {}\n", hex::encode(p)));
1427 }
1428 c.push_str(
1429 "author A <a@x> 1700000000 +0000\ncommitter A <a@x> 1700000000 +0000\n\n",
1430 );
1431 c.push_str(msg);
1432 c.push('\n');
1433 c.into_bytes()
1434 };
1435 let c1 = commit_for(&t1_oid, None, "r1");
1436 let c1_oid = oid_of(GitObjectKind::Commit, &c1);
1437 // `main`'s commit is parentless, so nothing of `base` becomes reachable
1438 // through it and the two closures stay the disjoint-plus-v1 shape the
1439 // assertions below rely on.
1440 let c_main = commit_for(&t2_oid, None, "r2 on main only");
1441 let c_main_oid = oid_of(GitObjectKind::Commit, &c_main);
1442 let c3 = commit_for(&t3_oid, Some(&c1_oid), "r3");
1443 let c3_oid = oid_of(GitObjectKind::Commit, &c3);
1444
1445 // Entry 0 is `v1` whole; entry 1 is `v2` as an ofs-delta on it; entry 2
1446 // is `v3` as an ofs-delta on `v2`. Offsets increase along the chain,
1447 // which is the invariant the ancestor walk's acyclicity rests on.
1448 let pack = build_pack(&[
1449 Item::Whole(GitObjectKind::Blob, v1),
1450 Item::OfsDelta { base: 0, body: v2 },
1451 Item::OfsDelta { base: 1, body: v3 },
1452 Item::Whole(GitObjectKind::Tree, t1),
1453 Item::Whole(GitObjectKind::Tree, t2),
1454 Item::Whole(GitObjectKind::Tree, t3),
1455 Item::Whole(GitObjectKind::Commit, c1),
1456 Item::Whole(GitObjectKind::Commit, c_main),
1457 Item::Whole(GitObjectKind::Commit, c3),
1458 ]);
1459 (
1460 pack,
1461 [
1462 c3_oid, t3_oid, v3_oid, c1_oid, t1_oid, v1_oid, c_main_oid, t2_oid, v2_oid,
1463 ],
1464 )
1465 }
1466
1467 /// 🔴 **A narrowed clone re-deltas its boundary entry against a base the
1468 /// pack DOES carry, instead of shipping it whole — and the pack still holds
1469 /// exactly the same objects, byte for byte.**
1470 ///
1471 /// # The gap this closes, measured before it existed
1472 ///
1473 /// `h2h-linear-sha1-2048c-1024f-16k`, one server process, oden 2026-08-11:
1474 /// a narrowed clone of `base` was **11 613 666** bytes for 31 805 objects
1475 /// where stock git sends 5 820 000 for the identical set — 2.0×. The cause
1476 /// is 961 entries (3.0 %) whose stored delta base falls outside the request:
1477 /// they are 16 KiB blobs stored as ~284-byte deltas, and shipping them whole
1478 /// costs ~5.8 KB each. Thin-pack narrowing (`d2ebb2c`) fixed the *fetch*
1479 /// half and provably cannot touch this one — a clone's receiver holds
1480 /// nothing, so there is no external base to name.
1481 ///
1482 /// # What is asserted, and why each part is needed
1483 ///
1484 /// * **The premise first.** `v3` must really be stored as a delta on `v2`,
1485 /// `v2` must really be outside the request, and `v1` must really be inside
1486 /// it and be `v2`'s base. On a corpus where any of those fails, every
1487 /// implementation produces the same pack and this test is decoration.
1488 /// * **The count, held equal.** A smaller pack that dropped an object would
1489 /// satisfy a byte assertion perfectly. The count is checked in the
1490 /// receipt, in the pack's own header field, and in what git reads back.
1491 /// * **The bytes.** The load-bearing number. Seen red at **33 970** bytes
1492 /// with `ZNIPPY_GIT_BOUNDARY_DELTA=0` — which is the pre-change behaviour
1493 /// in this same binary, against the same store — and green at **16 839**.
1494 /// The 2.02× between them is the same ratio the production fixture shows.
1495 /// * **The object graph, out of stock git.** A wrong copy offset produces a
1496 /// pack `index-pack` accepts and whose objects are *different* — it files
1497 /// each entry under the oid of whatever it decoded. Only reading the bytes
1498 /// back and comparing them against the store catches it, so
1499 /// [`crate::git_oracle::git_reads_back`] does exactly that for all six.
1500 /// * **The receipt.** `copied + recompressed == objects` still holds, and
1501 /// the rebuilt entry is `deltified` rather than whole. Both are counted
1502 /// off what happened; a byte count cannot tell the two rebuilds apart.
1503 #[test]
1504 fn a_narrowed_clone_re_deltas_its_boundary_entry_against_a_base_the_pack_carries() {
1505 let dir = tmpdir("serve-reboundary");
1506 let store = GitStore::open(&dir, "rickard").unwrap();
1507 let (pack, [c3, t3, v3, c1, t1, v1, c_main, _t2, v2]) =
1508 three_revisions_with_the_middle_one_off_branch();
1509 store.put(&pack, &[]).unwrap();
1510 store.wait_indexed();
1511 store.absorb_pending().unwrap();
1512 assert_eq!(store.commit_count(), 3, "all three commits must be folded");
1513
1514 let set = GitServe::select(&store, &[&c3], &[])
1515 .unwrap()
1516 .expect("the projection covers a commit it folded");
1517 let mut want = set.objects.clone();
1518 want.sort();
1519 let mut expected = vec![
1520 c3.clone(),
1521 t3.clone(),
1522 v3.clone(),
1523 c1.clone(),
1524 t1.clone(),
1525 v1.clone(),
1526 ];
1527 expected.sort();
1528 assert_eq!(want, expected, "`base` reaches r1 and r3 and nothing of main");
1529
1530 // ── THE PREMISE ───────────────────────────────────────────────────────
1531 let row_v3 = store.index().lookup(&v3).expect("v3 is indexed");
1532 let row_v2 = store.index().lookup(&v2).expect("v2 is indexed");
1533 let row_v1 = store.index().lookup(&v1).expect("v1 is indexed");
1534 assert_eq!(
1535 row_v3.obj_type,
1536 ObjType::OfsDelta,
1537 "v3 must be stored as a delta or there is no boundary entry"
1538 );
1539 assert_eq!(
1540 row_v3.delta_base, row_v2.offset,
1541 "v3's stored base must be v2 — the off-branch revision"
1542 );
1543 assert!(
1544 !set.objects.contains(&v2),
1545 "v2 must be OUTSIDE the request, or nothing is re-deltified"
1546 );
1547 assert_eq!(
1548 row_v2.delta_base, row_v1.offset,
1549 "v2's own base must be v1, or the chain ancestor this aims at does not exist"
1550 );
1551 assert!(
1552 set.objects.contains(&v1),
1553 "v1 must be INSIDE the request — it is the base the re-delta names"
1554 );
1555 assert!(
1556 row_v1.offset < row_v3.offset,
1557 "the ancestor must sit earlier in the archive; that ordering is what makes the new \
1558 delta edge acyclic by construction"
1559 );
1560 assert!(
1561 !c_main.is_empty(),
1562 "main's commit exists and is what keeps v2 in the repository but out of the request"
1563 );
1564
1565 // ── the emission ──────────────────────────────────────────────────────
1566 let oids: Vec<Oid<'_>> = set.objects.iter().map(Vec::as_slice).collect();
1567 let mut out = Vec::new();
1568 let stats = store.emit_oids(&oids, &[], &Caps::modern(), &mut out).unwrap();
1569
1570 assert_eq!(stats.objects, 6, "the count is held equal");
1571 assert_eq!(
1572 u32::from_be_bytes(out[8..12].try_into().unwrap()),
1573 6,
1574 "the emitted pack header must count what the receipt counts"
1575 );
1576
1577 // ── THE BYTES ─────────────────────────────────────────────────────────
1578 //
1579 // 🔴 **33 970 with `ZNIPPY_GIT_BOUNDARY_DELTA=0`, 16 839 with it on** —
1580 // measured, same binary, same store. The floor is `v1` itself: it is
1581 // 16 KiB of noise and it is *copied* in both arms, so the pack can never
1582 // be small. What halves is the second copy of the same 16 KiB that `v3`
1583 // used to be, and the threshold sits between the two behaviours rather
1584 // than on the green figure, so a zlib version bump moves the number
1585 // without moving the verdict.
1586 assert!(
1587 stats.bytes < 20_000,
1588 "a 16 KiB boundary blob with an in-request chain ancestor must go out as a delta, not \
1589 whole: the pack is {} bytes, and shipping it whole measures 33 970",
1590 stats.bytes
1591 );
1592 assert_eq!(
1593 stats.bytes as usize,
1594 out.len(),
1595 "the receipt's byte count must be what was actually written"
1596 );
1597
1598 // ── THE RECEIPT ───────────────────────────────────────────────────────
1599 assert_eq!(
1600 stats.copied + stats.recompressed,
1601 stats.objects,
1602 "every entry is one or the other, always"
1603 );
1604 assert_eq!(
1605 stats.recompressed, 1,
1606 "exactly the boundary entry is rebuilt; a re-delta is still a rebuild and must not be \
1607 counted as a copy"
1608 );
1609 let entries = store.emit_set(&oids, true, None).unwrap();
1610 let deltified: Vec<&crate::pack_walk::EmitEntry> =
1611 entries.iter().filter(|e| e.deltified).collect();
1612 assert_eq!(deltified.len(), 1, "one entry, and it is the boundary one");
1613 assert_eq!(deltified[0].oid, v3, "and it is v3");
1614 assert_eq!(
1615 deltified[0].delta_base, row_v1.offset,
1616 "the computed delta must name the in-request chain ancestor as its base"
1617 );
1618 // The mechanism, isolated from `v1`'s unavoidable 16 KiB: the entry
1619 // itself. Whole it is ~16.5 KB deflated; against `v1` it is a few
1620 // hundred bytes, and the gap between those two is the entire result.
1621 assert!(
1622 deltified[0].stored.len() < 1_000,
1623 "the re-deltified entry is {} bytes; whole it is ~16 500, and anything in between \
1624 means the delta was computed against the wrong base",
1625 deltified[0].stored.len()
1626 );
1627 assert!(
1628 entries.iter().all(|e| !e.deltified || e.recompressed),
1629 "`deltified` is a strict refinement of `recompressed`"
1630 );
1631
1632 // ── THE OBJECT GRAPH, out of stock git ────────────────────────────────
1633 let scratch = tmpdir("serve-reboundary-idx");
1634 crate::git_oracle::assert_git_accepts(
1635 &scratch,
1636 "dst.git",
1637 &out,
1638 crate::git_oracle::Strictness::Connected,
1639 );
1640 let read_back = crate::git_oracle::git_reads_back(&scratch, "readback.git", &out)
1641 .expect("stock git reads back the pack it just accepted");
1642 assert_eq!(read_back.len(), 6, "git must hold exactly the six objects");
1643 for (oid_hex, body) in &read_back {
1644 let raw = hex::decode(oid_hex).expect("git prints hex oids");
1645 let (_, ours) = store
1646 .content(&raw)
1647 .unwrap()
1648 .unwrap_or_else(|| panic!("git read back {oid_hex}, which this store does not hold"));
1649 assert_eq!(
1650 &ours, body,
1651 "{oid_hex} came back from git with different bytes than the store holds — a \
1652 computed delta with a wrong copy offset produces exactly this and passes \
1653 index-pack"
1654 );
1655 }
1656
1657 // ── and a full clone is untouched ─────────────────────────────────────
1658 let all: Vec<Oid<'_>> = [&c3, &t3, &v3, &c1, &t1, &v1, &c_main, &_t2, &v2]
1659 .iter()
1660 .map(|o| o.as_slice())
1661 .collect();
1662 let mut full = Vec::new();
1663 let stats = store.emit_oids(&all, &[], &Caps::modern(), &mut full).unwrap();
1664 assert_eq!(stats.objects, 9);
1665 assert_eq!(
1666 stats.recompressed, 0,
1667 "a whole-repository request contains every base, so nothing is rebuilt and nothing is \
1668 deltified — the copy claim is unchanged"
1669 );
1670 assert_eq!(stats.copied, 9);
1671 }
1672
1673 /// A fresh bare repository, and the objects `pack` carries unpacked into it.
1674 ///
1675 /// `git unpack-objects` rather than `index-pack`, because the point is a
1676 /// receiver that **holds** these objects, not one that has a pack file
1677 /// sitting next to its repository.
1678 fn bare_repo_holding(scratch: &std::path::Path, name: &str, pack: &[u8]) -> std::path::PathBuf {
1679 use std::io::Write as _;
1680 let repo = scratch.join(name);
1681 let init = std::process::Command::new("git")
1682 .args(["init", "-q", "--bare"])
1683 .arg(&repo)
1684 .output()
1685 .expect("running git init");
1686 assert!(init.status.success(), "git init failed");
1687 if !pack.is_empty() {
1688 let mut child = std::process::Command::new("git")
1689 .args(["unpack-objects", "-q"])
1690 .current_dir(&repo)
1691 .stdin(std::process::Stdio::piped())
1692 .stdout(std::process::Stdio::piped())
1693 .stderr(std::process::Stdio::piped())
1694 .spawn()
1695 .expect("running git unpack-objects");
1696 child.stdin.take().unwrap().write_all(pack).unwrap();
1697 let done = child.wait_with_output().unwrap();
1698 assert!(
1699 done.status.success(),
1700 "seeding the receiver failed:\n{}",
1701 String::from_utf8_lossy(&done.stderr)
1702 );
1703 }
1704 repo
1705 }
1706
1707 /// `git index-pack --stdin --fix-thin --strict` inside `repo`. The exact
1708 /// command a fetching client runs when it advertised `thin-pack`, and the
1709 /// only one that can judge a pack whose bases are somewhere else.
1710 fn fix_thin_into(repo: &std::path::Path, pack: &[u8]) -> std::process::Output {
1711 use std::io::Write as _;
1712 let mut child = std::process::Command::new("git")
1713 .args(["index-pack", "--stdin", "--fix-thin", "--strict"])
1714 .current_dir(repo)
1715 .stdin(std::process::Stdio::piped())
1716 .stdout(std::process::Stdio::piped())
1717 .stderr(std::process::Stdio::piped())
1718 .spawn()
1719 .expect("running git index-pack --fix-thin");
1720 child.stdin.take().unwrap().write_all(pack).unwrap();
1721 child.wait_with_output().unwrap()
1722 }
1723
1724 /// 🔴 **A thin FETCH names a base the receiver already holds instead of
1725 /// rebuilding the boundary entry whole — and stock git resolves it.**
1726 ///
1727 /// This is the cost `8c2679d` left behind, for the half of it that can be
1728 /// paid cheaply. A narrowed request cuts delta chains, and until now every
1729 /// entry on the cut went out **whole**: correct, connected, and measured at
1730 /// 11.6 MB against git's 5.82 MB on `h2h-linear-sha1-2048c-1024f-16k`. When
1731 /// the receiver already holds the base, none of that is necessary — a
1732 /// `REF_DELTA` naming an external base is exactly what `thin-pack` means,
1733 /// and the stored delta stream goes out unchanged.
1734 ///
1735 /// # Four assertions, and each one can fail on its own
1736 ///
1737 /// 1. **The premise.** `t_base` is stored as an `OFS_DELTA` on `t_main`,
1738 /// `t_main` is outside the request, and the client holds it. Without all
1739 /// three there is no thin entry to make and the rest proves nothing.
1740 /// 2. **The receipt.** `recompressed` must be **0** — RED before this
1741 /// change at **1**, because the boundary entry was rebuilt.
1742 /// 3. **Applied output, on the bytes.** The emitted pack must contain a
1743 /// `REF_DELTA` whose base oid is `t_main`, and `t_main` must **not** be
1744 /// an entry in that pack. A receipt cannot tell a thin delta from a
1745 /// copied one; the wire can.
1746 /// 4. **Stock git, both directions.** `index-pack --stdin --fix-thin
1747 /// --strict` accepts it in a repository that holds `main`, and **fails**
1748 /// in an empty one. The second half is what proves the pack is genuinely
1749 /// thin rather than accidentally self-contained, and it is what makes the
1750 /// first half worth anything.
1751 ///
1752 /// And the two arms that must NOT change: the same request without
1753 /// `caps.thin`, and the same request with no `have`, both still rebuild the
1754 /// boundary entry whole. A clone is the second of those.
1755 #[test]
1756 fn a_thin_fetch_names_a_base_the_client_holds_instead_of_rebuilding_it() {
1757 let dir = tmpdir("serve-thin");
1758 let store = GitStore::open(&dir, "rickard").unwrap();
1759 let (pack, [c_main, t_main, b_main, c_base, t_base, b_base]) =
1760 two_branches_with_a_cross_branch_delta(256);
1761 store.put(&pack, &[]).unwrap();
1762 store.wait_indexed();
1763 store.absorb_pending().unwrap();
1764
1765 // The fetch: the client is on `main` and wants `base`.
1766 let set = GitServe::select(&store, &[&c_base], &[&c_main])
1767 .unwrap()
1768 .expect("the projection covers both commits it folded");
1769 let oids: Vec<Oid<'_>> = set.objects.iter().map(Vec::as_slice).collect();
1770
1771 // ── THE PREMISE ───────────────────────────────────────────────────────
1772 assert!(
1773 !set.objects.contains(&t_main),
1774 "main's tree must be OUTSIDE the request or there is no external base"
1775 );
1776 assert!(
1777 set.objects.contains(&t_base),
1778 "base's tree must be IN the request — it is the boundary entry"
1779 );
1780 let row = store.index().lookup(&t_base).expect("indexed");
1781 assert_eq!(
1782 row.obj_type,
1783 ObjType::OfsDelta,
1784 "the fixture must store base's tree as a delta or there is no base to name"
1785 );
1786 assert_eq!(
1787 row.delta_base,
1788 store.index().lookup(&t_main).expect("indexed").offset,
1789 "the fixture's delta must name main's tree as its base"
1790 );
1791 assert!(
1792 set.client_has.contains(&t_main),
1793 "the negotiation must vouch that the client holds main's tree"
1794 );
1795
1796 let thin_caps = Caps {
1797 thin: true,
1798 ofs_delta: true,
1799 };
1800 let have: Vec<Oid<'_>> = vec![&c_main];
1801
1802 // ── the receipt ───────────────────────────────────────────────────────
1803 let mut thin = Vec::new();
1804 let stats = store
1805 .emit_oids(&oids, &have, &thin_caps, &mut thin)
1806 .unwrap();
1807 assert_eq!(stats.objects, 3, "the set is still exactly what was selected");
1808 assert_eq!(
1809 stats.recompressed, 0,
1810 "the boundary entry's base is one the client holds, so nothing may be rebuilt"
1811 );
1812 assert_eq!(stats.copied, 3, "every entry is a byte-for-byte copy");
1813
1814 // ── applied output, on the emitted bytes ──────────────────────────────
1815 let walked = crate::pack_walk::walk(&thin, store.hash_kind().oid_len())
1816 .expect("our own walk reads what we emitted");
1817 assert_eq!(walked.entries.len(), 3);
1818 assert_eq!(
1819 walked.closure().external_refs,
1820 vec![t_main.clone()],
1821 "exactly one entry may name a base by oid, and it must be main's tree — which the \
1822 pack does not carry, because the pack carries only the three objects selected"
1823 );
1824
1825 // ── stock git, both directions ────────────────────────────────────────
1826 let scratch = tmpdir("serve-thin-git");
1827 // What the client already has: main's three objects.
1828 let mut mains = Vec::new();
1829 store
1830 .emit_oids(
1831 &[&c_main, &t_main, &b_main],
1832 &[],
1833 &Caps::modern(),
1834 &mut mains,
1835 )
1836 .unwrap();
1837 let holder = bare_repo_holding(&scratch, "holder.git", &mains);
1838 let got = fix_thin_into(&holder, &thin);
1839 assert!(
1840 got.status.success(),
1841 "git refused a thin pack whose base it holds — status {:?}\n{}",
1842 got.status.code(),
1843 String::from_utf8_lossy(&got.stderr)
1844 );
1845
1846 // …and it must FAIL where the base is absent — for the RIGHT reason.
1847 //
1848 // The receiver here holds `b_main` and nothing else, which is chosen
1849 // rather than convenient: an *empty* repository refuses this pack either
1850 // way, because `t_base` names `b_main` as a child and a strict index-pack
1851 // demands children exist, so a refusal there would prove connectivity and
1852 // say nothing about thinness. Holding exactly `b_main` makes the pack
1853 // connected and leaves only the delta base missing, so the only thing
1854 // git can complain about is the external base — and with the thin arm
1855 // switched off this same call **succeeds**, which is what makes it a
1856 // guard rather than a decoration.
1857 let blob_only = bare_repo_holding(&scratch, "blob-only.git", &{
1858 let mut p = Vec::new();
1859 store
1860 .emit_oids(&[&b_main], &[], &Caps::modern(), &mut p)
1861 .unwrap();
1862 p
1863 });
1864 let refused = fix_thin_into(&blob_only, &thin);
1865 assert!(
1866 !refused.status.success(),
1867 "a receiver without main's TREE accepted the pack, so nothing was actually \
1868 offered as external and this whole test is hollow"
1869 );
1870 let why = String::from_utf8_lossy(&refused.stderr).to_lowercase();
1871 assert!(
1872 why.contains("delta"),
1873 "the refusal must be about the delta base this pack does not carry, and it said: \
1874 {why}"
1875 );
1876
1877 // ── and the two arms that must not have moved ─────────────────────────
1878 let mut not_thin = Vec::new();
1879 let stats = store
1880 .emit_oids(&oids, &have, &Caps::modern(), &mut not_thin)
1881 .unwrap();
1882 assert_eq!(
1883 stats.recompressed, 1,
1884 "a client that did not advertise thin-pack must still get the entry whole"
1885 );
1886 let mut no_have = Vec::new();
1887 let stats = store
1888 .emit_oids(&oids, &[], &thin_caps, &mut no_have)
1889 .unwrap();
1890 assert_eq!(
1891 stats.recompressed, 1,
1892 "consent with nothing negotiated is a clone, and a clone must be unchanged"
1893 );
1894
1895 // The whole point, in bytes: the thin pack is smaller than the one that
1896 // rebuilt the entry.
1897 assert!(
1898 thin.len() < not_thin.len(),
1899 "a thin pack that is not smaller has bought nothing: {} vs {}",
1900 thin.len(),
1901 not_thin.len()
1902 );
1903
1904 // And a FULL request is byte-identical whether or not thin is offered —
1905 // it has no base outside itself for any of this to apply to.
1906 let all: Vec<Oid<'_>> = vec![&c_main, &t_main, &b_main, &c_base, &t_base, &b_base];
1907 let mut full_thin = Vec::new();
1908 store
1909 .emit_oids(&all, &have, &thin_caps, &mut full_thin)
1910 .unwrap();
1911 let mut full_plain = Vec::new();
1912 store
1913 .emit_oids(&all, &[], &Caps::modern(), &mut full_plain)
1914 .unwrap();
1915 assert_eq!(
1916 full_thin, full_plain,
1917 "a full clone must be byte-identical with and without the thin allowance"
1918 );
1919 }
1920
1921 /// **The emitted pack is a byte-range copy, and stock git accepts it.**
1922 ///
1923 /// Proven against `git index-pack --strict` rather than against this
1924 /// crate's own reader: our parser reading our writer would agree with itself
1925 /// even if both were wrong the same way.
1926 ///
1927 /// The receipt is asserted too, and that is not decoration: `copied ==
1928 /// objects` and `recompressed == 0` are the `P-001` applied-output
1929 /// assertion. A pipeline that inflated every object in order to deflate it
1930 /// again would pass `index-pack --strict` **and** `fsck` while sending a
1931 /// measured 18.4x the wire bytes, and only these two counters can see it.
1932 #[test]
1933 fn an_emitted_pack_is_copied_not_recompressed_and_stock_git_accepts_it() {
1934 let dir = tmpdir("serve-emit");
1935 let store = GitStore::open(&dir, "rickard").unwrap();
1936 let (pack, rows) = real_pack();
1937 store.put(&pack, &[]).unwrap();
1938 store.wait_indexed();
1939 store.absorb_pending().unwrap();
1940
1941 let oids: Vec<Oid<'_>> = rows.iter().map(|r| r.oid.as_slice()).collect();
1942 let mut out = Vec::new();
1943 let stats = store.emit_oids(&oids, &[], &Caps::modern(), &mut out).unwrap();
1944
1945 assert_eq!(
1946 stats.objects,
1947 rows.len() as u64,
1948 "every requested object must be emitted"
1949 );
1950 assert_eq!(
1951 stats.copied, stats.objects,
1952 "every payload must be COPIED, not re-deflated"
1953 );
1954 assert_eq!(stats.recompressed, 0, "nothing on this path re-deflates");
1955 assert_eq!(
1956 stats.bytes,
1957 out.len() as u64,
1958 "the receipt's byte count is what was written, not what was intended"
1959 );
1960
1961 // 🔴 Through [`crate::git_oracle`], and that is the 2026-08-11 fix here:
1962 // this ran `index-pack --strict` in `scratch`, which is a plain
1963 // directory and not a repository, and outside a repository that command
1964 // **segfaults** on any pack it would have rejected — exit 139, no
1965 // output. The whole corpus is emitted, so the set is closed and
1966 // `Connected` is the honest question.
1967 let scratch = tmpdir("serve-emit-idx");
1968 crate::git_oracle::assert_git_accepts(
1969 &scratch,
1970 "emitted.git",
1971 &out,
1972 crate::git_oracle::Strictness::Connected,
1973 );
1974 }
1975
1976 /// **A client without `ofs-delta` is SERVED — a ref-delta carrying the same
1977 /// bytes — and never handed an entry it cannot parse.**
1978 ///
1979 /// Until 2026-08-11 this asserted a refusal, because the engine copies
1980 /// stored entries and had no way to re-name a base; that is the
1981 /// `gunnar.clone_no_ofs_delta` arm, red with *"the selection contains at
1982 /// least one stored ofs-delta entry"*. It does have a way now, and it costs
1983 /// nothing: the two delta forms differ only in **how the base is named**, so
1984 /// re-heading an `OFS_DELTA` as a `REF_DELTA` is a header swap over an
1985 /// unchanged compressed payload.
1986 ///
1987 /// Both halves are asserted. Serving is not enough on its own — the
1988 /// silent-corruption direction is to emit the `OFS_DELTA` anyway and exit
1989 /// zero — so the emitted bytes are walked and every entry checked, and the
1990 /// receipt must still say `recompressed = 0` or the swap has quietly become
1991 /// a re-deflate.
1992 #[test]
1993 fn a_client_that_cannot_read_ofs_delta_is_served_ref_deltas_instead() {
1994 let dir = tmpdir("serve-caps");
1995 let store = GitStore::open(&dir, "rickard").unwrap();
1996 let (pack, rows) = real_pack();
1997 store.put(&pack, &[]).unwrap();
1998 store.wait_indexed();
1999 store.absorb_pending().unwrap();
2000
2001 let stored_ofs = rows
2002 .iter()
2003 .filter(|r| r.stored_type == ObjType::OfsDelta)
2004 .count();
2005 assert!(
2006 stored_ofs > 0,
2007 "the corpus must contain an ofs-delta or this asserts nothing"
2008 );
2009
2010 let oids: Vec<Oid<'_>> = rows.iter().map(|r| r.oid.as_slice()).collect();
2011 let caps = Caps {
2012 thin: false,
2013 ofs_delta: false,
2014 };
2015 let mut out = Vec::new();
2016 let stats = store.emit_oids(&oids, &[], &caps, &mut out).unwrap();
2017 assert_eq!(stats.objects, rows.len() as u64);
2018 assert_eq!(
2019 stats.recompressed, 0,
2020 "re-naming a base must not re-deflate a payload"
2021 );
2022
2023 let walked = crate::pack_walk::walk(&out, store.hash_kind().oid_len())
2024 .expect("our own walk reads what we emitted");
2025 assert!(
2026 walked.entries.iter().all(|e| e.obj_type != ObjType::OfsDelta),
2027 "not one ofs-delta may reach a client that cannot parse one"
2028 );
2029 assert_eq!(
2030 walked
2031 .entries
2032 .iter()
2033 .filter(|e| e.obj_type == ObjType::RefDelta)
2034 .count(),
2035 stored_ofs
2036 + rows
2037 .iter()
2038 .filter(|r| r.stored_type == ObjType::RefDelta)
2039 .count(),
2040 "every stored delta must still be a delta — re-headed, not flattened"
2041 );
2042
2043 // And stock git reads it. The whole corpus is emitted, so the set is
2044 // closed and `Connected` is the question a clone would ask.
2045 let scratch = tmpdir("serve-caps-idx");
2046 crate::git_oracle::assert_git_accepts(
2047 &scratch,
2048 "dst.git",
2049 &out,
2050 crate::git_oracle::Strictness::Connected,
2051 );
2052 }
2053
2054 /// **`put_refs_cas` applies every edit or none, and says which one lost.**
2055 ///
2056 /// Two properties in one test because they are one property: the batch that
2057 /// fails must leave the namespace exactly as it was, and the caller must be
2058 /// able to learn *which* ref and *what was there* **without reading the
2059 /// message**.
2060 #[test]
2061 fn an_atomic_batch_applies_every_edit_or_none_and_names_the_loser() {
2062 let dir = tmpdir("serve-cas");
2063 let store = GitStore::open(&dir, "rickard").unwrap();
2064 let (pack, oid) = one_blob_pack(b"cas fixture");
2065 store.put(&pack, &[]).unwrap();
2066 store.wait_indexed();
2067 store
2068 .put_refs(&[RefUpdate::set("refs/heads/taken", hex::encode(&oid))])
2069 .unwrap();
2070
2071 // Edit 1 would succeed on its own; edit 2 must not. Neither may land.
2072 let edits = [
2073 RefCas {
2074 name: "refs/heads/fresh".into(),
2075 old: None,
2076 new: Some(&oid),
2077 },
2078 RefCas {
2079 name: "refs/heads/taken".into(),
2080 old: None, // "must not exist" — and it does
2081 new: Some(&oid),
2082 },
2083 ];
2084 let err = store.put_refs_cas(&edits).unwrap_err();
2085
2086 let rejection = RefRejection::of(&err).expect(
2087 "a lost compare-and-swap must be TYPED — receive-pack cannot tell a lost race from a \
2088 broken disk by reading a message",
2089 );
2090 assert!(rejection.is_cas_failure());
2091 assert!(!rejection.is_lock_contention());
2092 assert_eq!(rejection.name(), "refs/heads/taken");
2093 match rejection {
2094 RefRejection::Cas {
2095 expected, actual, ..
2096 } => {
2097 assert_eq!(*expected, Observed::Nothing, "the caller claimed absent");
2098 assert_eq!(
2099 *actual,
2100 Observed::oid(&oid),
2101 "the rejection must carry what was ACTUALLY there"
2102 );
2103 }
2104 other => panic!("wrong variant: {other:?}"),
2105 }
2106
2107 let names: Vec<String> = store.refs().unwrap().into_iter().map(|r| r.name).collect();
2108 assert!(
2109 !names.iter().any(|n| n == "refs/heads/fresh"),
2110 "the first edit of a failed atomic batch must NOT have landed: {names:?}"
2111 );
2112
2113 // And the same batch, with the expectation corrected, applies whole.
2114 let edits = [
2115 RefCas {
2116 name: "refs/heads/fresh".into(),
2117 old: None,
2118 new: Some(&oid),
2119 },
2120 RefCas {
2121 name: "refs/heads/taken".into(),
2122 old: Some(&oid),
2123 new: None, // delete
2124 },
2125 ];
2126 store.put_refs_cas(&edits).unwrap();
2127 let names: Vec<String> = store.refs().unwrap().into_iter().map(|r| r.name).collect();
2128 assert!(names.iter().any(|n| n == "refs/heads/fresh"));
2129 assert!(!names.iter().any(|n| n == "refs/heads/taken"));
2130 }
2131
2132 /// **`S-023`: a create that must fail still fails when the new value equals
2133 /// the current one.**
2134 ///
2135 /// The defect this guards is a backend short-circuiting an edit whose new
2136 /// value already matches, never evaluating the caller's expectation and
2137 /// turning a `MustNotExist` into a **silent success**. It is not
2138 /// hypothetical — it was found in gix's file ref store, on the only backend
2139 /// that survives a restart — and it breaks *"exactly one creator wins"*,
2140 /// which is how receive-pack arbitrates two racing pushes.
2141 ///
2142 /// Seen RED by moving the expectation check after the write.
2143 #[test]
2144 fn s023_a_must_not_exist_create_fails_even_when_the_value_is_unchanged() {
2145 let dir = tmpdir("serve-s023");
2146 let store = GitStore::open(&dir, "rickard").unwrap();
2147 let (pack, oid) = one_blob_pack(b"s023");
2148 store.put(&pack, &[]).unwrap();
2149 store.wait_indexed();
2150 store
2151 .put_refs(&[RefUpdate::set("refs/heads/racy", hex::encode(&oid))])
2152 .unwrap();
2153
2154 // Same value the ref already holds, with `old: None` — the exact shape a
2155 // short-circuiting backend turns into a no-op success.
2156 let edits = [RefCas {
2157 name: "refs/heads/racy".into(),
2158 old: None,
2159 new: Some(&oid),
2160 }];
2161 let err = store.put_refs_cas(&edits).unwrap_err();
2162 let rejection = RefRejection::of(&err).expect("must be a typed CAS rejection");
2163 assert!(
2164 rejection.is_cas_failure(),
2165 "a create-that-must-not-exist against an existing ref is a lost race, not a fault"
2166 );
2167 }
2168
2169 /// An empty atomic batch is a no-op that succeeded.
2170 #[test]
2171 fn an_empty_atomic_batch_is_a_no_op_rather_than_an_error() {
2172 let dir = tmpdir("serve-cas-empty");
2173 let store = GitStore::open(&dir, "rickard").unwrap();
2174 assert_eq!(store.put_refs_cas(&[]).unwrap(), TxId::default());
2175 }
2176
2177 /// The hash width the emitter trailers with is the store's, not a constant.
2178 #[test]
2179 fn the_emitted_trailer_is_the_stores_hash_width() {
2180 for hash in [GitHashKind::Sha1, GitHashKind::Sha256] {
2181 let dir = tmpdir(&format!("serve-trailer-{}", hash.oid_len()));
2182 let store = GitStore::open_with(&dir, "rickard", hash).unwrap();
2183 // No objects: the pack is header + trailer and nothing else, which
2184 // is exactly the shape that makes the widths comparable.
2185 let mut out = Vec::new();
2186 let stats = store.emit_oids(&[], &[], &Caps::modern(), &mut out).unwrap();
2187 assert_eq!(stats.objects, 0);
2188 assert_eq!(out.len(), 12 + hash.oid_len());
2189 assert_eq!(stats.bytes, out.len() as u64);
2190 }
2191 }
2192}