Skip to main content

gunnar_wire/
receive_pack.rs

1//! `git-receive-pack`, i.e. serving push. Protocol v0, which is the only
2//! protocol push has ever had.
3//!
4//! # The order that matters
5//!
6//! ```text
7//! advertise refs+caps → command list → push-options → PACK → index →
8//! connectivity → policy gate → ref transaction → report
9//! ```
10//!
11//! The two steps in the middle are the ones that separate a forge from a
12//! directory of files:
13//!
14//! * **Connectivity is verified before any reference moves.** A thin pack's
15//!   deltas resolve against objects the receiver already had, so "the pack
16//!   indexed cleanly" does not mean "the new tip is complete". A reference
17//!   pointing at an incomplete commit is a repository that every subsequent
18//!   clone fails on, and nothing in the push itself reports it.
19//! * **`atomic` is all-or-nothing at the storage layer**, not a loop that stops
20//!   early. It is one `GitOps::put_refs_cas`, which is where the batch and the
21//!   compare-and-swap are the *same* call; a per-reference loop with a break
22//!   leaves the references it already applied applied.
23//!
24//!   That method is why `--atomic` needed a contract change rather than a
25//!   rearrangement here (`locked_on_target.md`'s `GitServe` reading contract): `put_refs` is a batch
26//!   **without** CAS — `RefUpdate` has no expected-old field — and
27//!   `update_ref` is CAS **without** a batch, and git defines `--atomic` as
28//!   both at once. Neither composes into the other, so the non-atomic path
29//!   below uses `update_ref` (one reference, still compare-and-swap) and the
30//!   atomic path uses `put_refs_cas`.
31//!
32//! # Where the packfile goes
33//!
34//! Not decided here. `gunnar-wire` must work with no filesystem, and
35//! "keep the pushed pack" is inherently a storage decision: an appliance folds
36//! it into a znippy archive, a normal-OS server drops it in `objects/pack`. So
37//! the caller supplies a [`PackIngest`], and this module's only requirement is
38//! the one it can check: **after `ingest` returns, the objects must be readable
39//! from the store.** The connectivity walk proves it — and it needs them
40//! *decoded*, which is why the handle is a `GitServe` and not a `GitOps`.
41
42use std::io::{BufRead, Write};
43
44use crate::pack::IndexedPack;
45use gix_hash::ObjectId;
46use gunnar_pktline::{Packet, BAND_PACK};
47use gunnar_store::git_storage_trait::{GitServe, Observed, RefCas, RefRejection, RefTarget};
48
49use crate::caps::{self, PeerCapabilities};
50use crate::graph::{verify_connectivity, Frontier, Graph};
51use crate::refs::RefSnapshot;
52use crate::wire_io::{self, read_packet, read_section};
53use crate::{Error, Result, Session};
54
55/// The tracing target every line in this module carries, so a push is
56/// filterable with the same `RUST_LOG=gunnar::git=info` that selects the
57/// server's own transfer log.
58///
59/// # Why the string and not `gunnar_core::observe::TARGET_GIT`
60///
61/// That is where it is *named*, and this is a second spelling of it, which one
62/// implementation is normally against. The edge that would remove the second
63/// spelling runs the wrong way: `gunnar-core` carries the identity table, the
64/// ACL and the path sandbox, and this crate is bound by the no-OS rule and the
65/// crate layout to plumbing —
66/// pulling it in to reach one `&str` would put all of it in the appliance's
67/// crate budget.
68///
69/// So the two spellings are held together by a guard where both crates are
70/// already linked (`gunnar-cli`'s `observability` test), which costs nothing
71/// and cannot be forgotten by a build that does not link them.
72pub const GIT_TARGET: &str = "gunnar::git";
73
74/// One `<old> <new> <ref>` line from the client.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct RefCommand {
77    /// What the client believes the reference currently holds; null for a
78    /// create.
79    pub old: ObjectId,
80    /// What it should hold afterwards; null for a delete.
81    pub new: ObjectId,
82    /// The reference.
83    ///
84    /// `String`, not the `FullName` newtype that died with `RefStore`, because
85    /// `RefUpdate::name` and `RefRow::name` on the contract are `String` and a
86    /// second validated name type in gunnar is the duplication the 2026-08-10
87    /// demolition removes.
88    ///
89    /// **The validation did not go with the type.** It moved into
90    /// `ReceivePack::parse_command`, which routes every client-supplied name
91    /// through `gunnar_store::refname::validate` before one of these can
92    /// exist — and that is a path-traversal boundary
93    /// (`refs/heads/../../etc/passwd`), not a tidiness rule. What is gone is
94    /// only the *compile-time* guarantee that a value of this type was
95    /// checked; the runtime check is in one place and it is the same rule set.
96    pub name: String,
97}
98
99impl RefCommand {
100    /// Is this a deletion?
101    pub fn is_delete(&self) -> bool {
102        self.new.is_null()
103    }
104
105    /// Is this a creation?
106    pub fn is_create(&self) -> bool {
107        self.old.is_null()
108    }
109}
110
111/// What the client sends first: its ref commands (and the ones it could not
112/// name) plus the capabilities it chose. Read by [`ReceivePack::read_preamble`]
113/// — with the hash check already applied — and consumed by
114/// [`ReceivePack::serve_after_preamble`]. The split exists so the
115/// first-repository door can read this **before** a store exists.
116#[derive(Debug)]
117pub struct Preamble {
118    /// The nameable commands, in wire order.
119    pub commands: Vec<RefCommand>,
120    /// The commands whose names gunnar refused.
121    pub unnameable: Vec<UnnameableRef>,
122    /// What the client asked for.
123    pub peer: PeerCapabilities,
124}
125
126/// A command line this server will not accept a **name** from.
127///
128/// # Why an invalid name is not an error
129///
130/// It used to be. `read_commands` called `FullName::new` with `?`, so one bad
131/// name aborted the whole session before the pack had been read: the client was
132/// still writing and got a broken pipe, no `report-status` was ever produced,
133/// and every *other* reference in the same push was lost with no line saying
134/// which one was at fault. That is the `the remote end hung up unexpectedly`
135/// shape, and it is the same defect the shallow-clone fix in `read_commands`
136/// closed for one particular cause without closing the class.
137///
138/// **Stock git defers it to the report, and this is measured, not assumed.**
139/// `git receive-pack` 2.53.0, 2026-08-06, fed a raw two-command push of
140/// `refs/heads/goodone` and `refs/heads/bad~name` over one connection, answered:
141///
142/// ```text
143/// unpack ok
144/// ok refs/heads/goodone
145/// ng refs/heads/bad~name funny refname
146/// ```
147///
148/// exit 0, `refs/heads/goodone` created. So the good reference lands, the bad
149/// one is named in the report, and the session ends cleanly. gunnar now does
150/// the same.
151///
152/// The name is kept as **bytes**: it is precisely a name the validator refused,
153/// so there is nothing to hold it in, and it is echoed back verbatim because a
154/// client matches the `ng` line against the name it sent. Since the contract's
155/// `RefRow::name` is a `String`, a name that is *valid* but not UTF-8 lands
156/// here too — see `ReceivePack::parse_command`, which records that narrowing
157/// rather than hiding it.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct UnnameableRef {
160    /// The reference name exactly as the client spelled it.
161    pub name: Vec<u8>,
162    /// The one-line reason, rendered into the `ng` line.
163    pub reason: String,
164    /// Where the line sat in the client's command list, so the report can be
165    /// emitted in the order the client sent — which is what git does and what a
166    /// client zipping its own list against the report needs.
167    pub position: usize,
168    /// Whether the line asked for a deletion. A push carrying only unnameable
169    /// *deletes* sends no pack, and a server that waited for one would hang.
170    pub deletes: bool,
171}
172
173/// What happened to one reference.
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub enum RefStatus {
176    /// Applied.
177    Ok,
178    /// Refused, with the reason the client is told.
179    Rejected(String),
180}
181
182impl RefStatus {
183    /// Was it applied?
184    pub fn is_ok(&self) -> bool {
185        matches!(self, RefStatus::Ok)
186    }
187}
188
189/// Everything `receive-pack` learned about the push, handed to every gate.
190///
191/// # Why this is a struct and not three arguments
192///
193/// The gate seam used to be `(&RefCommand, &dyn ObjectStore)` and nothing else,
194/// so a gate could not see which objects the push introduced or how many bytes
195/// the client sent. `gunnar-policy`'s size and quota gates
196/// (`gunnar_policy::gates::limits`) are the things standing between a forge and
197/// a client pushing until the disk fills, and neither could be enforced: the
198/// object set was recovered downstream by reading the written `.idx` back,
199/// which counts a thin pack's resolved bases as if the client had sent them,
200/// and the byte count was never produced at all.
201///
202/// A struct rather than a wider tuple so the *next* fact does not re-break
203/// every implementation of [`PushGate`].
204pub struct PushFacts<'a> {
205    /// The store, after the pack has been taken in.
206    ///
207    /// `GitServe`: a gate reads objects, and reading means *inflated and
208    /// delta-resolved*. `gunnar-policy`'s ancestry walk and its signature gate
209    /// were two of the three errors that crate was blocked on, and they are the
210    /// same missing capability the connectivity walk here needs.
211    pub objects: &'a dyn GitServe,
212    /// The objects this push genuinely introduced, or the reason they could not
213    /// be enumerated.
214    ///
215    /// **`Err` is not "none"**: a gate that cannot see what was pushed cannot
216    /// decide, and an undecidable gate refuses. Handing an empty slice for
217    /// "I do not know" is the shape that lets a configured limit be silently
218    /// satisfied, which is what this type exists to prevent.
219    pub new_objects: std::result::Result<&'a [ObjectId], &'a str>,
220    /// Packfile bytes the client sent, counted off the wire by
221    /// [`ReceivePack::serve`] rather than measured from what landed on disk.
222    ///
223    /// `Some(0)` for a deletions-only push, which carries no pack: that is a
224    /// measured zero, not an absent measurement, and a byte limit must not
225    /// refuse it. `None` only if the count is genuinely unavailable.
226    pub pack_bytes: Option<u64>,
227    /// The `push-option` lines the client sent, in wire order.
228    ///
229    /// # Why this field exists
230    ///
231    /// `push-options` was **advertised** by
232    /// [`caps::receive_pack_capabilities`](crate::caps::receive_pack_capabilities),
233    /// **parsed** by [`ReceivePack::serve`] and then thrown away: the only
234    /// consumer in the whole tree was `.len()` in one log line. A client that
235    /// sent `-o ci.skip` got the capability acknowledged on the wire and the
236    /// option silently ignored, which is a wrong answer on the wire rather than
237    /// a missing feature — the gate could not see them, so no policy could act
238    /// on one even in principle.
239    ///
240    /// Stock git hands push options to the `pre-receive` hook. gunnar has no
241    /// hooks and no shell; [`PushGate`] is what replaces them, so
242    /// this is where they have to arrive.
243    ///
244    /// **Empty means the client sent none**, which is also what a client that
245    /// never negotiated the capability produces. The two are not told apart
246    /// here on purpose: a gate that requires an option must refuse both, and
247    /// a gate that forbids unknown ones must accept both.
248    pub push_options: &'a [String],
249}
250
251/// What taking the pack into storage produced.
252///
253/// [`PackIngest`] returns this rather than a bare [`IndexedPack`] because the
254/// only place that can answer *"which objects did this push introduce"*
255/// honestly is the code that resolved the pack: it is the one that knows which
256/// entries came off the wire and which bases it pulled out of the store to
257/// complete a thin pack. Recovering it afterwards from the written index cannot
258/// tell those apart.
259///
260/// `Debug` and `Clone` because both of its fields already are, and because
261/// [`crate::fetch_pack::FetchReport`] carries one: a report a caller cannot
262/// print is a report a caller logs by hand, field by field, and gets wrong.
263#[derive(Debug, Clone)]
264pub struct PackReceipt {
265    /// The pack as indexed.
266    pub indexed: IndexedPack,
267    /// The genuinely-new objects, or why they are unknown. See
268    /// [`PushFacts::new_objects`] for why this is not an `Option`.
269    pub new_objects: std::result::Result<Vec<ObjectId>, String>,
270}
271
272impl PackReceipt {
273    /// A receipt from an ingest that does not enumerate the pushed objects.
274    ///
275    /// The reason, rather than an empty set, so gates refuse instead of
276    /// deciding on nothing.
277    pub fn without_object_set(indexed: IndexedPack) -> Self {
278        PackReceipt {
279            indexed,
280            new_objects: Err(
281                "this PackIngest does not report which objects the push introduced".to_owned(),
282            ),
283        }
284    }
285}
286
287/// A pre-receive gate.
288///
289/// `gunnar-policy` supplies the real ones; this crate only defines
290/// the seam so that receive-pack has somewhere to ask. **Measuring rather than
291/// asserting applies hardest here**: a gate that always returns `Ok` is
292/// indistinguishable from a working gate in any test that only pushes valid
293/// commits, so every implementation must be seen to reject before it is
294/// trusted to allow.
295pub trait PushGate {
296    /// `Err(reason)` rejects the reference; the reason is sent verbatim.
297    fn check(&self, command: &RefCommand, facts: &PushFacts<'_>)
298        -> std::result::Result<(), String>;
299}
300
301/// The gate that lets everything through. This is the default, and the thing
302/// `gunnar-policy` replaces.
303pub struct AllowAll;
304
305impl PushGate for AllowAll {
306    fn check(
307        &self,
308        _command: &RefCommand,
309        _facts: &PushFacts<'_>,
310    ) -> std::result::Result<(), String> {
311        Ok(())
312    }
313}
314
315/// How a pushed packfile is taken into storage.
316///
317/// The stream handed to [`PackIngest::ingest`] is positioned on the packfile's
318/// first byte (the `P` of `PACK`) because [`crate::receive_pack`] reads
319/// pkt-lines with exact-length reads and never buffers past the flush.
320pub trait PackIngest {
321    /// Consume the pack and make its objects readable from the store.
322    fn ingest(&self, pack: &mut dyn BufRead) -> Result<PackReceipt>;
323}
324
325/// A bare closure is still an ingest, and it reports **no** object set.
326///
327/// Kept because most of this crate's own tests are closures that only need the
328/// pack indexed. The receipt says so out loud
329/// ([`PackReceipt::without_object_set`]) rather than answering "no new
330/// objects", so a real gate behind such an ingest refuses rather than passing a
331/// limit it never evaluated.
332impl<F> PackIngest for F
333where
334    F: Fn(&mut dyn BufRead) -> Result<IndexedPack>,
335{
336    fn ingest(&self, pack: &mut dyn BufRead) -> Result<PackReceipt> {
337        self(pack).map(PackReceipt::without_object_set)
338    }
339}
340
341/// Counts the bytes an ingest consumes, so [`PushFacts::pack_bytes`] is the
342/// number that came off the wire.
343///
344/// Wrapping the reader is what makes the count true by construction: it is not
345/// `IndexedPack`'s business, it is not the storage backend's, and the only code
346/// that sees every byte of the packfile go past is the code that hands the
347/// stream over.
348pub(crate) struct Counting<'a> {
349    inner: &'a mut dyn BufRead,
350    pub(crate) bytes: u64,
351    /// The ceiling this push is being held to, if any. See [`PackCeiling`].
352    ceiling: Option<PackCeiling>,
353    /// **Bytes charged to the shared ledger that this push has not yet taken.**
354    ///
355    /// `fill_buf` charges what it is about to OFFER and `consume` discharges
356    /// what was actually taken, and the two are not the same call: a consumer
357    /// may peek at a bufferful three times and consume half of it. Without this
358    /// counter each peek would charge the same bytes again and a push would
359    /// spend its namespace's room several times over on bytes it never took.
360    ///
361    /// Charging at `consume` alone — after the bytes are already out — was the
362    /// alternative, and it does not close the race: every one of N pushes would
363    /// still hand its ingest a whole buffer measured against a room nobody had
364    /// spent yet. The charge has to happen BEFORE the bytes are offered, which
365    /// means it has to be remembered until they are taken.
366    ///
367    /// Left-over credit at the end of a transfer (bytes peeked and never
368    /// consumed) stays charged, which is the same over-conservative,
369    /// self-healing direction `gunnar_lfs::ledger` documents for an aborted
370    /// upload: it is bounded by one buffer, it costs only the holders alive
371    /// beside it, and it errs towards refusing rather than towards a byte the
372    /// operator pays for.
373    charged_ahead: u64,
374    /// Set the moment the count passed the ceiling, and never cleared. The
375    /// caller reads it rather than trying to recognise the abort in whatever
376    /// the ingest turned the cut stream into — an indexer is entitled to
377    /// report a truncated pack as a malformed one, and "your plan is full"
378    /// and "your pack is corrupt" are not the same sentence to a tenant.
379    pub(crate) cut: bool,
380}
381
382impl<'a> Counting<'a> {
383    /// A reader over `inner` held to `ceiling` — the receive loop's own guard,
384    /// lent to a FETCH so a mirror is cut at its namespace's room by the same
385    /// code that cuts a push (`fetch_pack::FetchPack::with_ceiling`).
386    pub(crate) fn new(inner: &'a mut dyn BufRead, ceiling: Option<PackCeiling>) -> Self {
387        Counting {
388            inner,
389            bytes: 0,
390            ceiling,
391            charged_ahead: 0,
392            cut: false,
393        }
394    }
395}
396
397impl Counting<'_> {
398    /// Has the count passed the ceiling?
399    ///
400    /// **Strictly greater**, so a push of exactly `ceiling` bytes lands. The
401    /// ceiling is the last byte that is allowed, not the first that is not —
402    /// see [`PackCeiling`] on why the number already carries slack.
403    fn over(&self) -> bool {
404        matches!(&self.ceiling, Some(ceiling) if self.bytes > ceiling.bytes)
405    }
406
407    /// How many more bytes may be handed out before the ceiling is passed —
408    /// the room under it **plus the one byte that trips it**, so the trip is
409    /// reachable. `None` when there is no ceiling.
410    ///
411    /// # Why the slice is truncated to this, rather than trusted
412    ///
413    /// `fill_buf` hands out whatever the source has buffered, and the source
414    /// decides how much that is: a `BufReader` gives a bufferful, a `Cursor`
415    /// over a `Vec` gives **the whole rest of the push in one call**. Without
416    /// this clamp the ceiling's precision would be the source's buffer size —
417    /// the guard would still fire, but a pack read out of memory would cross
418    /// it by its entire length before anything looked, and the guard's own
419    /// test would be measuring the fixture rather than the code (measure,
420    /// never assert). With it, a push that overshoots costs the server exactly
421    /// `bytes + 1` and the number is the same on every reader.
422    fn room(&self) -> Option<usize> {
423        let ceiling = self.ceiling.as_ref()?.bytes;
424        let room = ceiling.saturating_sub(self.bytes).saturating_add(1);
425        // A ceiling past `usize` on a 32-bit box is not a ceiling this read can
426        // reach in one call, so clamping to `usize::MAX` is the honest answer
427        // rather than a truncating cast.
428        Some(usize::try_from(room).unwrap_or(usize::MAX))
429    }
430
431    /// **How many of `want` bytes this push may hand on, asking the shared
432    /// ledger for whatever it has not already been credited.**
433    ///
434    /// `want` is what the caller would otherwise offer, already clamped to
435    /// [`Self::room`]. With no ceiling, or a ceiling with no ledger behind it,
436    /// the answer is `want` and nothing is charged — the community build and
437    /// every gunnar before 2026-09-08 take this path, and it is the same code
438    /// path, not a second one.
439    ///
440    /// The room the ledger is asked about is `ceiling + 1`: the ceiling plus
441    /// the one byte that trips it. A budget of exactly `ceiling` would starve
442    /// the trip byte, and a push of exactly its ceiling would then end as a
443    /// stream that simply stopped — which the indexer reports as a malformed
444    /// pack, and telling a tenant their pack is corrupt when their plan filled
445    /// up is the refusal this file's `refuse` exists to prevent.
446    fn admit(&mut self, want: usize) -> usize {
447        let Some(ledger) = self.ceiling.as_ref().and_then(|c| c.ledger.as_ref()) else {
448            return want;
449        };
450        // Unwrap-free: the `let else` above already proved there is a ceiling.
451        let room = self
452            .ceiling
453            .as_ref()
454            .map(|c| c.bytes.saturating_add(1))
455            .unwrap_or(u64::MAX);
456        let want = want as u64;
457        // Only the part that is not already paid for. On a re-peek of a buffer
458        // nothing was consumed from, this is 0 and the ledger is not touched.
459        let owing = want.saturating_sub(self.charged_ahead);
460        if owing > 0 {
461            self.charged_ahead = self
462                .charged_ahead
463                .saturating_add(ledger.admit_upto(owing, room));
464        }
465        // `min`: credit carried over from an earlier, larger offer does not
466        // make this offer bigger than it is.
467        usize::try_from(self.charged_ahead.min(want)).unwrap_or(usize::MAX)
468    }
469
470    /// Cut the stream: mark the push and return the error the ingest sees.
471    ///
472    /// The text here is for the **server log**, not the client: what the client
473    /// is told is [`PackCeiling::refusal`], installed over the ingest's own
474    /// message by [`ReceivePack::serve_after_preamble`]. Two sentences because
475    /// they answer two people.
476    fn refuse(&mut self) -> std::io::Error {
477        self.cut = true;
478        let ceiling = self.ceiling.as_ref().map(|c| c.bytes).unwrap_or_default();
479        std::io::Error::other(format!(
480            "the push passed its {ceiling}-byte ceiling after {} byte(s); the pack read was \
481             aborted mid-transfer and nothing was stored",
482            self.bytes
483        ))
484    }
485}
486
487impl std::io::Read for Counting<'_> {
488    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
489        if self.cut || self.over() {
490            let err = self.refuse();
491            return Err(err);
492        }
493        // **What the source actually has, before anything is charged for it —
494        // and asked for ONLY when something is going to be charged.**
495        //
496        // The ledger has to be asked about bytes that EXIST: charging
497        // `buf.len()` and reading fewer would leave the difference charged to
498        // the namespace with nothing to discharge it, and
499        // `simultaneous_pushes_that_all_fit_are_all_admitted` measured that as
500        // seven of eight legitimate pushes admitted where all eight fit. A
501        // quota that refuses a push nobody's bytes displaced is the same defect
502        // as one that admits too many, pointing the other way.
503        //
504        // `fill_buf` blocks exactly where `read` would — until one byte is
505        // available or the stream ends — so it costs no latency. It does cap
506        // this call at what the source has buffered, which is why it is skipped
507        // entirely with no ledger to ask: an unmetered `read` still goes
508        // straight through to the source and may still hand back more than one
509        // bufferful in a call, exactly as it did before this field existed.
510        let available = match self.ceiling.as_ref().and_then(|c| c.ledger.as_ref()) {
511            Some(_) => {
512                let available = self.inner.fill_buf()?.len();
513                if available == 0 {
514                    // The client is done. Not a refusal: nothing to refuse.
515                    return Ok(0);
516                }
517                available
518            }
519            None => usize::MAX,
520        };
521        let want = match self.room() {
522            Some(room) => room.min(buf.len()).min(available),
523            None => buf.len().min(available),
524        };
525        // The namespace's answer, not just this push's: `admit` returns `want`
526        // unchanged when there is no ledger, so the unmetered path is
527        // arithmetically untouched.
528        let allowed = self.admit(want);
529        if allowed == 0 && !buf.is_empty() {
530            // Nothing left in the namespace, and the client is still sending.
531            // Cut it here rather than returning `Ok(0)`: a zero-length read is
532            // EOF to every consumer in std, and "your plan is full" must not
533            // reach the indexer as "the pack ended early".
534            let err = self.refuse();
535            return Err(err);
536        }
537        let read = self.inner.read(&mut buf[..allowed])?;
538        self.bytes += read as u64;
539        self.charged_ahead = self.charged_ahead.saturating_sub(read as u64);
540        Ok(read)
541    }
542}
543
544impl BufRead for Counting<'_> {
545    /// **Where the ceiling actually bites**, because this is how the pack is
546    /// read: `read_pushed_pack_spooling` peeks with `fill_buf` and takes with
547    /// `consume`, so this is the path the shipping ingest takes and `read`
548    /// above is the path one that does not peek takes. Both are guarded and
549    /// both clamp to [`Counting::room`], so the overshoot is **one byte**, not
550    /// one buffer — a push that means to send 400 GiB past its ceiling costs
551    /// the server `ceiling + 1` and nothing more.
552    fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
553        if self.cut || self.over() {
554            let err = self.refuse();
555            return Err(err);
556        }
557        // `room()` is never `Some(0)` — `over()` above has already returned for
558        // that — so this never turns a live stream into a spurious EOF.
559        let room = self.room();
560        // **The namespace's half, and it fills the buffer TWICE.** The ledger
561        // has to be asked about the bytes actually in hand — asking it about
562        // `room` instead would reserve a tenant's whole remaining plan for the
563        // first connection to say hello and refuse a sibling push that would
564        // have fitted twice over — and it cannot be asked while a slice
565        // borrowed out of `self.inner` is alive. A second `fill_buf` with no
566        // `consume` between is a peek: it returns the same buffer it just
567        // returned, reads nothing more and cannot block. The `min` below makes
568        // even a source that ignores that safe.
569        //
570        // Skipped entirely with no ledger to ask, so the unmetered path fills
571        // once and clamps to `room`, exactly as it did before this existed.
572        let allowed = match self.ceiling.as_ref().and_then(|c| c.ledger.as_ref()) {
573            Some(_) => {
574                let available = self.inner.fill_buf()?.len();
575                let want = match room {
576                    Some(room) if available > room => room,
577                    _ => available,
578                };
579                let allowed = self.admit(want);
580                if allowed == 0 && available > 0 {
581                    // The namespace's room is gone and the client is still
582                    // sending. An empty slice would be EOF to the indexer, i.e.
583                    // "the pack is truncated"; the cut says whose bytes filled
584                    // the plan.
585                    let err = self.refuse();
586                    return Err(err);
587                }
588                Some(allowed)
589            }
590            None => None,
591        };
592        let filled = self.inner.fill_buf()?;
593        Ok(match (allowed, room) {
594            (Some(allowed), _) => &filled[..allowed.min(filled.len())],
595            (None, Some(room)) if filled.len() > room => &filled[..room],
596            _ => filled,
597        })
598    }
599
600    /// The only place `fill_buf`'s bytes are charged. `read` charges its own,
601    /// and the two never see the same byte: `BufRead::read`'s default goes
602    /// through `fill_buf`/`consume`, but this type overrides `read` to delegate
603    /// straight to the inner reader instead, so a byte is counted once whichever
604    /// way the consumer asks for it.
605    fn consume(&mut self, amount: usize) {
606        self.inner.consume(amount);
607        self.bytes += amount as u64;
608        // Taken, so no longer owed: the credit `fill_buf` bought for these
609        // bytes is spent. What is left is credit for bytes still in the buffer,
610        // which the next `fill_buf` will offer again and must not pay for
611        // twice.
612        self.charged_ahead = self.charged_ahead.saturating_sub(amount as u64);
613    }
614}
615
616/// Everything one push did.
617#[derive(Debug)]
618pub struct PushOutcome {
619    /// The commands as sent.
620    pub commands: Vec<RefCommand>,
621    /// One status per command, in the same order.
622    pub statuses: Vec<RefStatus>,
623    /// Command lines refused for their reference **name**, which therefore have
624    /// no [`RefCommand`] to carry a status. See [`UnnameableRef`].
625    pub unnameable: Vec<UnnameableRef>,
626    /// `push-option` lines, if the capability was negotiated.
627    pub push_options: Vec<String>,
628    /// The indexed pack, or `None` when the push was deletions only.
629    pub pack: Option<IndexedPack>,
630    /// Packfile bytes the client sent, counted off the wire. See
631    /// [`PushFacts::pack_bytes`].
632    pub pack_bytes: Option<u64>,
633    /// The objects the push introduced, or why they are unknown. Same value the
634    /// gates saw; see [`PushFacts::new_objects`].
635    pub new_objects: std::result::Result<Vec<ObjectId>, String>,
636    /// Whether the client asked for `--atomic`.
637    pub atomic: bool,
638    /// What the client was told about unpacking: `Ok` or a one-line reason.
639    pub unpack: std::result::Result<(), String>,
640}
641
642impl PushOutcome {
643    /// Did every reference land?
644    ///
645    /// An unnameable reference counts as a refusal: it is reported as `ng`, so
646    /// a caller reading this to mean *the push succeeded* must see false.
647    pub fn all_ok(&self) -> bool {
648        self.unpack.is_ok()
649            && self.unnameable.is_empty()
650            && self.statuses.iter().all(RefStatus::is_ok)
651    }
652}
653
654/// **How many packfile bytes this push may send, and what the client is told
655/// when it sends more.**
656///
657/// # Why a ceiling exists at all
658///
659/// `git-receive-pack`'s pack header carries an **object count, not a byte
660/// count**, so the size of a push is not knowable at the moment it is
661/// authorised. A quota asked before the pack (`gunnar_ssh::PlanGate::pack_quota`)
662/// refuses a tenant who is *already* at the ceiling, and a limit asked after it
663/// (`gunnar_policy::gates::limits`, whose own header says so plainly) refuses a
664/// pack that has *already been received* — so between those two there was
665/// nothing at all in front of one enormous push. A tenant one byte under a
666/// 10 GiB cap could send 400 GiB and the box would take every byte of it before
667/// anything looked.
668///
669/// This is that missing thing. It is enforced in the receive loop itself
670/// ([`Counting`]), so an unbounded push costs the server the ceiling and not
671/// the push.
672///
673/// # It is a backstop, not a meter
674///
675/// The polite refusal is `pack_quota`'s, before a byte moves. This fires only
676/// for a push that was under the cap when it started and crossed it in flight,
677/// which is why whoever sets it is expected to have added **slack** to it: the
678/// bytes on the wire are not the bytes that land (a thin pack is completed with
679/// bases the client never sent; the engine stores compressed), and a ceiling
680/// with no room refuses a push that would have fitted. `gunnar-wire` does not
681/// choose that number — it does not know what a plan is — it enforces the one
682/// it is handed.
683/// # …and it is one number shared by everything pushing at once
684///
685/// [`PackCeiling::bytes`] is measured for ONE push, and two pushes that arrive
686/// together get the same measurement — so before [`PackCeiling::ledger`] each
687/// of N simultaneous pushes was held to the whole of the namespace's remaining
688/// room, and the room was spent N times. `tests/concurrent_pack_ceiling.rs`
689/// measured it at eight packs of 41016 bytes into 65536 bytes of room. The
690/// ledger is what makes the number a shared budget rather than a per-connection
691/// allowance.
692#[derive(Debug, Clone)]
693pub struct PackCeiling {
694    /// Packfile bytes past which the read is aborted. A push of exactly this
695    /// many bytes is **accepted**; the next byte cuts it.
696    pub bytes: u64,
697    /// The line the client is told, verbatim, in place of whatever the ingest
698    /// made of the truncated stream. Composed by whoever set the ceiling — the
699    /// metered appliance names the plan, the cap and the billing URL, the same
700    /// shape every other entitlement refusal uses — because this crate knows
701    /// nothing about plans, caps or money.
702    pub refusal: String,
703    /// **Who else is spending [`Self::bytes`] right now.** `None` — the
704    /// default, and what a build with no byte cap installs — means the number
705    /// is this push's alone, which is what every gunnar before 2026-09-08 did
706    /// and is correct exactly as long as one push is the only push.
707    ///
708    /// `Some` makes the ceiling a budget the namespace shares: the receive loop
709    /// asks before every chunk it hands to the ingest, and takes what it is
710    /// granted. See [`PackLedger`].
711    ///
712    /// `PartialEq`/`Eq` came off this struct when the field arrived. Two
713    /// ceilings are not comparable any more, because a shared standing in a
714    /// counter is not a value — and nothing compared them: the equality the
715    /// tests use is `gunnar_ssh::PackCeiling`'s, which is the plain twin the
716    /// gates return and still derives both.
717    pub ledger: Option<std::sync::Arc<dyn PackLedger>>,
718}
719
720/// **The namespace's shared standing, asked one chunk at a time.**
721///
722/// A packfile's header carries an object count and no byte count, so a push
723/// cannot declare its size and the door cannot admit it up front the way the
724/// LFS door admits an object. The receive loop therefore asks about the chunk
725/// in its hand, before it hands it on, and takes what it is granted.
726///
727/// # Why a trait here instead of the ledger itself
728///
729/// The implementation is `gunnar_lfs::Mark` (a monotone `u64` per namespace and
730/// a per-holder mark — see that module for the whole design, including why
731/// there is deliberately nothing to give back), wired up by
732/// `gunnar_server::transport`, which is the one place that already holds both
733/// halves of the [`PackCeiling`] twin. This crate must not link `gunnar-lfs`
734/// and must not link `gunnar-core`: it is bound by the no-OS rule and the crate
735/// layout to plumbing, and
736/// its own [`GIT_TARGET`] refuses `gunnar-core` for a single `&str` on exactly
737/// that argument. So the edge is inverted rather than added, and `gunnar-wire`
738/// resolves the same 51 packages it resolved before the ledger existed
739/// (MEASURED 2026-09-08, `cargo tree -e normal`).
740pub trait PackLedger: Send + Sync + std::fmt::Debug {
741    /// **Charge up to `want` bytes against everything admitted to this
742    /// namespace since this push measured it, inside a total of `room`, and
743    /// return how many were charged.**
744    ///
745    /// Partial credit, not a verdict: a grant short of `want` is a chunk the
746    /// caller may take that much of, and a grant of `0` means the room is gone
747    /// and the push must be cut. An implementation that answered all-or-nothing
748    /// would refuse a push that fits, for asking about a buffer bigger than the
749    /// remainder.
750    ///
751    /// `room` is passed in on every call rather than held by the
752    /// implementation because it is the RECEIVE LOOP's number: it is
753    /// [`PackCeiling::bytes`] plus the one byte that makes the ceiling
754    /// reachable ([`Counting::room`] explains why that byte exists), and
755    /// spelling that `+ 1` in two crates is how two guards start disagreeing.
756    ///
757    /// **Must not block on I/O.** It is called once per chunk of a transfer at
758    /// wire speed; the shipping implementation takes a `Mutex` across a hash
759    /// lookup and an integer add, and nothing else.
760    fn admit_upto(&self, want: u64, room: u64) -> u64;
761}
762
763/// Knobs for one push.
764#[derive(Debug, Clone)]
765pub struct Config {
766    /// The `agent=` string.
767    pub agent: String,
768    /// Traversal budget for the connectivity check.
769    pub budget: Option<usize>,
770    /// **A line the client is told beside the report**, on the progress band
771    /// (`remote: …`), for a push that is ALLOWED but carries a warning — the
772    /// metered appliance's `plan expired <date>; renew at <url>` for a tenant
773    /// in Grace. `None` prints nothing. Sent only when the client negotiated a
774    /// sideband, which every git since 1.7 does; a client without one gets the
775    /// bare report, never a line where the report should be.
776    pub notice: Option<String>,
777    /// **The mid-upload ceiling.** `None` — the default, and what a community
778    /// build with no byte cap installs — reads the pack to its end whatever
779    /// its size, which is what every gunnar before 2026-09-05 did. See
780    /// [`PackCeiling`].
781    pub pack_ceiling: Option<PackCeiling>,
782}
783
784impl Default for Config {
785    fn default() -> Self {
786        Config {
787            agent: caps::default_agent(),
788            budget: Some(crate::graph::CONNECTIVITY_BUDGET),
789            notice: None,
790            pack_ceiling: None,
791        }
792    }
793}
794
795/// The server side of `git-receive-pack`.
796pub struct ReceivePack<'a> {
797    /// **One store, not two.** `ObjectStore` and `RefStore` were separate
798    /// gunnar traits and are deleted; `GitOps` says both, and `GitServe`
799    /// extends it with the decoded read the connectivity walk needs.
800    git: &'a dyn GitServe,
801    /// The repository's hash kind.
802    ///
803    /// Carried in rather than asked for — `GitServe` has no `hash_kind()` and
804    /// the frozen signature leaves whether `GitOps` should grow one open. It is not a
805    /// convenience here: `parse_command` refuses a command line whose oids are
806    /// in the wrong hash, and answering that from a default would accept a
807    /// sha1 push into a sha256 repository.
808    hash_kind: gix_hash::Kind,
809    config: Config,
810}
811
812/// An [`Observed`] in git's own vocabulary for a ref-status line.
813///
814/// git says `is at <oid> but expected <oid>`, and for a ref that did not exist
815/// it says the all-zero oid. `Unreadable` has no git spelling at all — the
816/// backend could not decode what the ref held — so it is named rather than
817/// flattened into "absent", which would tell a client the ref was empty when it
818/// was in fact corrupt.
819fn render(o: &Observed) -> String {
820    match o {
821        Observed::Nothing => "0000000000000000000000000000000000000000".to_owned(),
822        // A symbolic ref is spelled `ref: <name>`, as git does, so a client is
823        // never handed a branch name where it expects an oid.
824        Observed::Value(RefTarget::Object(oid)) => {
825            // Written out rather than pulling in a `hex` dependency for one
826            // call: this crate has none and a ref-status line is not worth one.
827            oid.iter()
828                .fold(String::with_capacity(oid.len() * 2), |mut acc, b| {
829                    use std::fmt::Write as _;
830                    let _ = write!(acc, "{b:02x}");
831                    acc
832                })
833        }
834        Observed::Value(RefTarget::Symbolic(name)) => format!("ref: {name}"),
835        Observed::Unreadable(what) => format!("unreadable ({what})"),
836    }
837}
838
839impl<'a> ReceivePack<'a> {
840    /// Receive into `git` with default configuration.
841    pub fn new(git: &'a dyn GitServe, hash_kind: gix_hash::Kind) -> Self {
842        ReceivePack {
843            git,
844            hash_kind,
845            config: Config::default(),
846        }
847    }
848
849    /// Replace the configuration.
850    pub fn with_config(mut self, config: Config) -> Self {
851        self.config = config;
852        self
853    }
854
855    /// The repository's hash kind.
856    pub fn hash_kind(&self) -> gix_hash::Kind {
857        self.hash_kind
858    }
859
860    /// Write the reference advertisement and its capabilities.
861    pub fn write_ref_advertisement(&self, out: &mut dyn Write) -> Result<()> {
862        let capabilities = caps::receive_pack_capabilities(self.hash_kind(), &self.config.agent);
863        // ONE snapshot: `RefStore::iter` followed by a `resolve` per symbolic
864        // row was N+1 reads of a namespace that can move between them, so an
865        // advertisement could name an oid for a reference that had already
866        // been deleted. A client then pushes against what it was told.
867        let snapshot = RefSnapshot::read(self.git)?;
868        let mut refs = Vec::new();
869        for row in snapshot.rows() {
870            let Some(id) = snapshot.resolve_row(row)? else {
871                continue;
872            };
873            refs.push(gunnar_sendpack::RemoteRef::new(row.name.as_bytes(), id));
874        }
875        // Rendered by the shared grammar, including the `capabilities^{}`
876        // pseudo-ref an empty repository has to emit so a client can still
877        // learn the hash algorithm. `gunnar-client` parses these lines with the
878        // same module.
879        for line in gunnar_sendpack::advertisement::lines(&refs, &capabilities, self.hash_kind()) {
880            wire_io::text(out, &line)?;
881        }
882        wire_io::flush(out)?;
883        Ok(())
884    }
885
886    /// Serve one push, from the advertisement through the status report.
887    pub fn serve(
888        &self,
889        input: &mut dyn BufRead,
890        out: &mut dyn Write,
891        ingest: &dyn PackIngest,
892        gate: &dyn PushGate,
893    ) -> Result<Option<PushOutcome>> {
894        self.serve_session(Session::Connected, input, out, ingest, gate)
895    }
896
897    /// Serve one push for either [`Session`].
898    ///
899    /// **Push is the easy half of stateless-rpc, and it is worth saying why.**
900    /// `receive-pack` has no negotiation: the client is told the refs once, and
901    /// then sends its command list, its options and its packfile in one go. So
902    /// a smart-HTTP push is exactly one `POST` and the only difference from SSH
903    /// is that the advertisement was a separate `GET`. There is no round to cut
904    /// short here, and protocol v2 has no push side to complicate it.
905    pub fn serve_session(
906        &self,
907        session: Session,
908        input: &mut dyn BufRead,
909        out: &mut dyn Write,
910        ingest: &dyn PackIngest,
911        gate: &dyn PushGate,
912    ) -> Result<Option<PushOutcome>> {
913        if session.advertises_inline() {
914            let _s = crate::stage::enter("advertise");
915            self.write_ref_advertisement(out)?;
916            out.flush()?;
917        }
918        let Some(preamble) = Self::read_preamble(self.hash_kind(), input)? else {
919            return Ok(None);
920        };
921        self.serve_after_preamble(preamble, input, out, ingest, gate)
922            .map(Some)
923    }
924
925    /// The advertisement of a repository that **does not exist yet**: no
926    /// references, the capabilities and `object-format` of `hash_kind`. The
927    /// first-repository door writes this instead of [`Self::write_ref_advertisement`]
928    /// because there is no store to snapshot — and must not be one, until the
929    /// client has sent a command that passes the hash check.
930    pub fn write_empty_advertisement(
931        hash_kind: gix_hash::Kind,
932        config: &Config,
933        out: &mut dyn Write,
934    ) -> Result<()> {
935        let capabilities = caps::receive_pack_capabilities(hash_kind, &config.agent);
936        for line in gunnar_sendpack::advertisement::lines(&[], &capabilities, hash_kind) {
937            wire_io::text(out, &line)?;
938        }
939        wire_io::flush(out)?;
940        out.flush()?;
941        Ok(())
942    }
943
944    /// The client's half of the opening: its command list and capabilities,
945    /// with the hash check applied. `None` when the client took the
946    /// advertisement and sent nothing, which is `git push` with nothing to do
947    /// — and a sha1 working tree that read `object-format=sha256` and gave up
948    ///. Neither is an error and neither is a push.
949    ///
950    /// An associated function, not a method: the first-repository door calls
951    /// it **before** any store exists, so it must not need one.
952    pub fn read_preamble(
953        hash_kind: gix_hash::Kind,
954        input: &mut dyn BufRead,
955    ) -> Result<Option<Preamble>> {
956        let (commands, unnameable, peer) = {
957            let _s = crate::stage::enter("command_read");
958            Self::read_commands(hash_kind, input)?
959        };
960        if commands.is_empty() && unnameable.is_empty() {
961            tracing::info!(
962                target: GIT_TARGET,
963                event = "git.receive_pack.no_commands",
964                "receive-pack: the client sent no ref commands"
965            );
966            // The client took the advertisement and hung up; `git push` with
967            // nothing to do does exactly this. Not an error, and not a push.
968            return Ok(None);
969        }
970        caps::check_object_format(hash_kind, peer.value("object-format"))?;
971        Ok(Some(Preamble {
972            commands,
973            unnameable,
974            peer,
975        }))
976    }
977
978    /// Serve a push whose [`Preamble`] has been read: options, the pack, the
979    /// gates, the ref updates and the status report.
980    pub fn serve_after_preamble(
981        &self,
982        preamble: Preamble,
983        input: &mut dyn BufRead,
984        out: &mut dyn Write,
985        ingest: &dyn PackIngest,
986        gate: &dyn PushGate,
987    ) -> Result<PushOutcome> {
988        let Preamble {
989            commands,
990            unnameable,
991            peer,
992        } = preamble;
993        let _s_opts = crate::stage::enter("push_options");
994        let push_options = if peer.has("push-options") {
995            // The push side has its own ceiling for the same reason the fetch
996            // side does: the section ends when the *peer* sends a flush-pkt.
997            // The number is `gunnar-wire`'s one request ceiling, not a second
998            // constant that could drift from it (one implementation).
999            let mut budget =
1000                crate::limits::Budget::new(Some(crate::limits::DEFAULT_REQUEST_LIMIT_BYTES));
1001            read_section(input, "the push-options section", &mut budget)?
1002                .into_iter()
1003                .map(|line| String::from_utf8_lossy(&line).into_owned())
1004                .collect()
1005        } else {
1006            Vec::new()
1007        };
1008
1009        crate::stage::end(_s_opts);
1010        let sideband = peer.has("side-band-64k");
1011        let report_requested = peer.has("report-status") || peer.has("report-status-v2");
1012        let atomic = peer.has("atomic");
1013
1014        // Deletions carry no objects, so the client sends no pack at all. A
1015        // server that reads one anyway blocks until the connection times out.
1016        //
1017        // An unnameable reference counts here too. The client does not know
1018        // gunnar refused its name and streams the pack regardless, so a server
1019        // that skipped the read because every *nameable* command was a delete
1020        // would leave a pack in the pipe, write its report into the middle of
1021        // it, and desync the session.
1022        let expects_pack = commands.iter().any(|command| !command.is_delete())
1023            || unnameable.iter().any(|refused| !refused.deletes);
1024
1025        // ── the phase log ───────────────────────────────────────────────────
1026        //
1027        // Everything below this point can take minutes on a repository the
1028        // size of a kernel, and until now it said **nothing at all** until it
1029        // was over. The Fet Kaos Monkey rig's `linux.git` import (13 816 503
1030        // objects, 8 170 MB pack, 2 872 refs; 2026-08-05, load 25–33)
1031        // wrote its pack and its index in ~30 min and had still logged no line
1032        // at 40 min. That is an operability defect on its own terms and
1033        // separate from the walk being slow: an operator watching a working
1034        // import and one watching a hang were reading the identical empty log.
1035        //
1036        // So each phase announces itself on **entry** and reports on **exit**.
1037        // The entry line is the load-bearing one — an exit-only log still
1038        // leaves the ten minutes in between unattributed, which is precisely
1039        // the state that was unreadable.
1040        //
1041        // `info`, not `debug`: a phase that can run for ten minutes is not
1042        // diagnostic detail, and a default-level log an operator has to know to
1043        // turn on is one that is off during the incident.
1044        tracing::info!(
1045            target: GIT_TARGET,
1046            event = "git.receive_pack.commands",
1047            commands = commands.len(),
1048            deletes = commands.iter().filter(|c| c.is_delete()).count(),
1049            push_options = push_options.len(),
1050            atomic,
1051            expects_pack,
1052            "receive-pack: read the ref command list"
1053        );
1054        let mut pack = None;
1055        // `Some(0)`, not `None`: a deletions-only push carries no pack, and that
1056        // is a MEASURED zero. A byte limit must refuse an oversized pack, not a
1057        // `git push --delete`.
1058        let mut pack_bytes = Some(0u64);
1059        let mut new_objects: std::result::Result<Vec<ObjectId>, String> = Ok(Vec::new());
1060        let mut unpack: std::result::Result<(), String> = Ok(());
1061        if expects_pack {
1062            // Indexing and verifying what the client sent. This is the stage
1063            // the receipt's `num_objects` is about, and on a push it is where
1064            // the money is.
1065            let _s = crate::stage::enter("pack_ingest");
1066            tracing::info!(
1067                target: GIT_TARGET,
1068                event = "git.receive_pack.receiving",
1069                "receiving and indexing the pushed pack"
1070            );
1071            let started = std::time::Instant::now();
1072            // Counted HERE rather than by measuring the pack that landed on
1073            // disk. What a limit is defending against is what the client sent;
1074            // the file that lands is a different number, because completing a
1075            // thin pack adds bases the client did not send, and on a store with
1076            // no filesystem there is no file to measure at all.
1077            let mut counting = Counting {
1078                inner: input,
1079                bytes: 0,
1080                ceiling: self.config.pack_ceiling.clone(),
1081                charged_ahead: 0,
1082                cut: false,
1083            };
1084            let received = ingest.ingest(&mut counting);
1085            pack_bytes = Some(counting.bytes);
1086            let cut = counting.cut;
1087            match received {
1088                Ok(receipt) => {
1089                    tracing::info!(
1090                        target: GIT_TARGET,
1091                        event = "git.receive_pack.indexed",
1092                        objects = receipt.indexed.num_objects,
1093                        pack = %receipt.indexed.pack_id,
1094                        pack_bytes = counting.bytes,
1095                        elapsed_ms = started.elapsed().as_millis() as u64,
1096                        "indexed a pushed pack"
1097                    );
1098                    new_objects = receipt.new_objects;
1099                    pack = Some(receipt.indexed);
1100                }
1101                Err(err) => {
1102                    let message = err.client_message();
1103                    tracing::warn!(
1104                        target: GIT_TARGET,
1105                        event = "git.receive_pack.index_failed",
1106                        pack_bytes = counting.bytes,
1107                        elapsed_ms = started.elapsed().as_millis() as u64,
1108                        // **The unredacted error, in the server's own log.**
1109                        //
1110                        // This logged `client_message()` — the redacted
1111                        // sentence the *client* is told — so a push that failed
1112                        // to index left `the repository is unavailable; try
1113                        // again` in the log and nothing anywhere said what the
1114                        // store had actually refused. `Error::client_message`'s
1115                        // own doc promises the opposite: "the unredacted text is
1116                        // still what the server logs". The fetch side kept that
1117                        // promise (`git.upload_pack.failed` carries `%err`);
1118                        // this side did not, and an operator debugging a refused
1119                        // push had the client's sentence twice and the cause
1120                        // never.
1121                        error = %err,
1122                        // Kept beside it, so a log reader can match what the
1123                        // operator was shown to what actually happened.
1124                        client_message = %message,
1125                        "the pushed pack did not index"
1126                    );
1127                    unpack = Err(message);
1128                }
1129            }
1130            // ── the ceiling, installed over whatever the ingest concluded ──
1131            //
1132            // Checked AFTER the match and unconditionally, not inside the `Err`
1133            // arm, because an ingest that swallowed the cut and answered `Ok`
1134            // would otherwise hand a partially-read push to `apply` — and a
1135            // guard that only fires when the code under it already failed is
1136            // the guard a measured assertion forbids.
1137            //
1138            // What the client is told is the ceiling's own line
1139            // (`PackCeiling::refusal`) and not `Error::client_message`'s: the
1140            // indexer's honest answer to a truncated stream is "the pack is
1141            // malformed", and telling a paying tenant their pack was corrupt
1142            // when what happened is that their plan filled up is the refusal an
1143            // operator gets a ticket about.
1144            if cut {
1145                let line = self
1146                    .config
1147                    .pack_ceiling
1148                    .as_ref()
1149                    .map(|ceiling| ceiling.refusal.clone())
1150                    // Unreachable — `Counting::cut` is set only where a ceiling
1151                    // exists — and still spelled out rather than
1152                    // `unwrap_or_default()`, because an empty `unpack` line is
1153                    // a report that says a push failed and will not say why.
1154                    .unwrap_or_else(|| "the push was cut at its byte ceiling".to_owned());
1155                tracing::warn!(
1156                    target: GIT_TARGET,
1157                    event = "git.receive_pack.ceiling",
1158                    pack_bytes = counting.bytes,
1159                    ceiling = self.config.pack_ceiling.as_ref().map(|c| c.bytes),
1160                    refusal = %line,
1161                    "the pushed pack passed its byte ceiling and the transfer was aborted \
1162                     mid-stream; nothing was indexed and no reference was moved"
1163                );
1164                // The pack is not `Some`: the ingest never completed one, and a
1165                // receipt here would make `PushOutcome` claim a pack that does
1166                // not exist.
1167                pack = None;
1168                new_objects = Err(
1169                    "the push was cut at its byte ceiling before the pack was indexed".to_owned(),
1170                );
1171                unpack = Err(line);
1172            }
1173        }
1174
1175        let statuses = if unpack.is_err() {
1176            vec![RefStatus::Rejected("unpacker error".to_owned()); commands.len()]
1177        } else {
1178            let facts = PushFacts {
1179                objects: self.git,
1180                new_objects: new_objects.as_deref().map_err(String::as_str),
1181                pack_bytes,
1182                push_options: &push_options,
1183            };
1184            // `--atomic` is all-or-nothing across the batch, and an unnameable
1185            // reference is a refusal *in* the batch. `apply` cannot see one —
1186            // it is only ever handed the commands that have a name — so the
1187            // fact is passed in, and it must be passed **in** rather than
1188            // collapsed afterwards: an after-the-fact collapse would rewrite
1189            // statuses for references the transaction had already applied, and
1190            // report as refused what is on disk.
1191            self.apply(&commands, atomic, !unnameable.is_empty(), gate, &facts)?
1192        };
1193
1194        let outcome = PushOutcome {
1195            commands,
1196            statuses,
1197            unnameable,
1198            push_options,
1199            pack,
1200            pack_bytes,
1201            new_objects,
1202            atomic,
1203            unpack,
1204        };
1205
1206        if report_requested {
1207            let _s = crate::stage::enter("report");
1208            self.write_report(out, &outcome, sideband)?;
1209            out.flush()?;
1210        }
1211        Ok(outcome)
1212    }
1213
1214    fn read_commands(
1215        hash_kind: gix_hash::Kind,
1216        input: &mut dyn BufRead,
1217    ) -> Result<(Vec<RefCommand>, Vec<UnnameableRef>, PeerCapabilities)> {
1218        let mut commands = Vec::new();
1219        let mut unnameable = Vec::new();
1220        let mut peer = PeerCapabilities::default();
1221        let mut first = true;
1222        let mut position = 0usize;
1223        loop {
1224            let packet = match read_packet(input)? {
1225                None => {
1226                    if first {
1227                        return Ok((commands, unnameable, peer));
1228                    }
1229                    return Err(Error::UnexpectedEof("the push command list"));
1230                }
1231                Some(packet) => packet,
1232            };
1233            match packet {
1234                Packet::Flush => break,
1235                Packet::Data(data) => {
1236                    let mut text: &[u8] = data.strip_suffix(b"\n").unwrap_or(&data);
1237                    // ── a shallow announcement is not the first COMMAND ──────
1238                    //
1239                    // A shallow client sends `shallow <oid>` for each graft
1240                    // before its commands. They are not commands, which this
1241                    // already knew — and they are not the line the capabilities
1242                    // ride on, which it did not.
1243                    //
1244                    // `first` used to mean "the first packet", so a shallow
1245                    // pusher spent it on a `shallow` line. That line carries no
1246                    // NUL, so no capabilities were parsed; `first` went false;
1247                    // and the real first command was then handed to
1248                    // `parse_command` with its whole capability section still
1249                    // attached:
1250                    //
1251                    // ```text
1252                    // invalid reference name "refs/heads/x\0 report-status-v2 side-band-64k
1253                    //   quiet object-format=sha1 agent=git/2.53.0-Linux": control character or space
1254                    // ```
1255                    //
1256                    // So EVERY push from a shallow clone was refused, and the
1257                    // client saw only `the remote end hung up unexpectedly`.
1258                    // That is not an exotic client: `actions/checkout` clones at
1259                    // `fetch-depth: 1` by default, so a CI job that pushes back
1260                    // is the shape that hits it.
1261                    //
1262                    // ISOLATED 2026-08-05 against stock git 2.53.0 over
1263                    // real SSH, one client, one commit, one server:
1264                    //
1265                    // | client                         | push        |
1266                    // |--------------------------------|-------------|
1267                    // | `clone --depth 3`              | **refused** |
1268                    // | the same clone, `fetch --unshallow` first | accepted |
1269                    //
1270                    // The second failure hid behind the first: `peer` stayed
1271                    // empty, so `report-status`, `side-band-64k`, `atomic`,
1272                    // `push-options` and the `object-format` check were all
1273                    // silently off for a shallow pusher even where the ref name
1274                    // happened to validate.
1275                    //
1276                    // git does it this way round too — `receive-pack.c`'s
1277                    // `read_head_info` takes the capabilities off the first
1278                    // **ref command**, having consumed any `shallow` lines
1279                    // ahead of it.
1280                    if text.starts_with(b"shallow ") {
1281                        continue;
1282                    }
1283                    if first {
1284                        if let Some(nul) = text.iter().position(|&b| b == 0) {
1285                            peer =
1286                                PeerCapabilities::parse(&String::from_utf8_lossy(&text[nul + 1..]));
1287                            text = &text[..nul];
1288                        }
1289                        first = false;
1290                    }
1291                    match Self::parse_command(hash_kind, text, position)? {
1292                        Ok(command) => commands.push(command),
1293                        Err(refused) => unnameable.push(refused),
1294                    }
1295                    position += 1;
1296                }
1297                other => {
1298                    return Err(Error::protocol(format!(
1299                        "unexpected {other:?} in the push command list"
1300                    )))
1301                }
1302            }
1303        }
1304        Ok((commands, unnameable, peer))
1305    }
1306
1307    /// One `<old> <new> <ref>` line, decoded by the shared grammar and then
1308    /// validated as a reference name by this server.
1309    ///
1310    /// **The split is on bytes, not on a lossy `String`,** and that is
1311    /// [`gunnar_sendpack::command::parse_line`]'s job now rather than this
1312    /// function's. Reference names are bytes in git: `refs/heads/\xff` is a
1313    /// name git will create and serve. Routing the line through
1314    /// `String::from_utf8_lossy` first replaced every invalid byte with U+FFFD
1315    /// *before* the name was validated, so a non-UTF-8 name was accepted and
1316    /// stored under a name the client never asked for — and any two names
1317    /// differing only in their invalid bytes collapsed onto the same reference,
1318    /// making a push to one silently overwrite the other.
1319    ///
1320    /// What stays here is the part that is this **server's** policy rather than
1321    /// the format's: **validating the name**, and
1322    /// reporting a hash-kind mismatch as [`Error::ObjectFormatMismatch`] so the
1323    /// client is told which two formats disagreed. The grammar refuses the same
1324    /// oid; it just cannot know what gunnar wants to call the refusal.
1325    /// The grammar's own failures stay hard errors — a line that is not
1326    /// `<old> <new> <ref>`, or an oid in the wrong hash, means the two ends
1327    /// disagree about the *format* and there is nothing left to report over.
1328    /// A name that parses and this server will not have is a different thing:
1329    /// the conversation is intact and the client is owed a line about that one
1330    /// reference, so it comes back as [`UnnameableRef`] rather than as `Err`.
1331    fn parse_command(
1332        hash_kind: gix_hash::Kind,
1333        line: &[u8],
1334        position: usize,
1335    ) -> Result<std::result::Result<RefCommand, UnnameableRef>> {
1336        let command =
1337            gunnar_sendpack::command::parse_line(line, hash_kind).map_err(|err| match err {
1338                gunnar_sendpack::Error::ObjectFormat { ours, theirs } => {
1339                    Error::ObjectFormatMismatch {
1340                        ours,
1341                        theirs: theirs.to_owned(),
1342                    }
1343                }
1344                other => Error::protocol(other.to_string()),
1345            })?;
1346        // ── the validator moved; the rule set did not ───────────────────────
1347        //
1348        // This was `FullName::new`, whose constructor ran the rules. The type
1349        // is gone with `RefStore` and the rules live in
1350        // `gunnar_store::refname::validate` — the same rules, in the place
1351        // `locked_on_target.md`'s checklist names, reached the same way
1352        // `gunnar-core::check_branch` reaches them. **This call is
1353        // load-bearing**: `gunnar-policy`'s gate layer no longer catches
1354        // `refs/heads/../../etc/passwd`, because its `RefUpdate::name` is a
1355        // plain `String` now, so this is the path-traversal boundary.
1356        //
1357        // ⚠ **A narrowing, recorded rather than hidden.** Reference names are
1358        // bytes in git and `FullName` held bytes; `RefRow::name` and
1359        // `RefUpdate::name` on the contract are `String`, so a name that
1360        // passes validation but is not UTF-8 — `refs/heads/\xff`, which git
1361        // will create and serve — can no longer be represented and comes back
1362        // as an [`UnnameableRef`] instead. That refuses **one** reference with
1363        // a line the client can read, and leaves the rest of the push
1364        // untouched, which is the failure direction this whole type exists to
1365        // produce. It is a real interop narrowing and it belongs to the
1366        // contract, not to this function: widening it means widening
1367        // `RefRow::name`, a two-implementation decision.
1368        let name = match validate_ref_name(command.name.as_slice()) {
1369            Ok(name) => name,
1370            Err(err) => {
1371                tracing::warn!(
1372                    target: GIT_TARGET,
1373                    event = "git.receive_pack.unnameable_ref",
1374                    ref_name = %String::from_utf8_lossy(&command.name),
1375                    error = %err,
1376                    "refusing one reference for its name; the rest of the push continues"
1377                );
1378                return Ok(Err(UnnameableRef {
1379                    name: command.name.to_vec(),
1380                    // git says `funny refname` and nothing else; gunnar's
1381                    // validator already knows *which* rule was broken and the
1382                    // client can only act on being told.
1383                    //
1384                    // **The rule and not the error.** `Error::InvalidRefName`
1385                    // renders the offending name inside its own message, so
1386                    // `format!("{err}")` here put the name in the `ng` line
1387                    // twice — and the `ng` line is one pkt-line. A 60 000-byte
1388                    // name then produced a 120 105-byte packet, `write_text`
1389                    // refused it with `TooLong`, and the whole report failed to
1390                    // write: the refusal path had its own way of dropping the
1391                    // session, which is the exact defect this type exists to
1392                    // remove. MEASURED 2026-08-06.
1393                    reason: format!("funny refname: {err}"),
1394                    position,
1395                    deletes: command.new.is_null(),
1396                }));
1397            }
1398        };
1399        Ok(Ok(RefCommand {
1400            old: command.old,
1401            new: command.new,
1402            name,
1403        }))
1404    }
1405
1406    /// Connectivity, policy, then the reference transaction.
1407    fn apply(
1408        &self,
1409        commands: &[RefCommand],
1410        atomic: bool,
1411        already_refused: bool,
1412        gate: &dyn PushGate,
1413        facts: &PushFacts<'_>,
1414    ) -> Result<Vec<RefStatus>> {
1415        let mut statuses = vec![RefStatus::Ok; commands.len()];
1416        let _conn = crate::stage::enter("connectivity");
1417        tracing::info!(
1418            target: GIT_TARGET,
1419            event = "git.receive_pack.checking",
1420            commands = commands.len(),
1421            "checking that the push closes, before any reference moves"
1422        );
1423        let started = std::time::Instant::now();
1424
1425        // ── THE INDEX, BEFORE THE WALK AND NOT DURING IT (`S-0xx`) ─────────
1426        //
1427        // The znippy arm acks a push's *bytes* before its *oids* exist: a pack
1428        // entry does not carry an oid, so one is knowable only after its delta
1429        // chain has been applied, and doing 13.9 M of those is what the indexer
1430        // puts behind the ack. Everything below reads objects by oid.
1431        //
1432        // Without this call the walk's first `peel` fell into that backend's
1433        // read-time fallback, which resolves the whole pending pack **on this
1434        // thread and throws the rows away** — a second resolver beside the
1435        // indexer's, over the same objects, each holding its own O(pack) tables.
1436        // MEASURED on the rig 2026-08-30, gunnar v1.1.3-180-g01ce6c58,
1437        // torvalds/linux --mirror at 16 GB and at 8 GB:
1438        //
1439        // ```text
1440        // 07:43:49  git.receive_pack.checking   commands=2874
1441        // 09:20:21  index.scanned  "a reader scanned a pending pack for itself:
1442        //                           13948244 objects in 5792442 ms"
1443        // ```
1444        //
1445        // 1h36m32s and 1h44m14s for the scan alone, and `git.receive_pack.checked`
1446        // never fired on either box: the clients sat unacked 2h14m30s and
1447        // 2h12m24s and the run ended only because the repository was deleted.
1448        // Stock git acks the same mirror in ~11 min, and it does it by running
1449        // `index-pack` first and `check_connected` second — which is exactly
1450        // this line and then the rest of this function.
1451        //
1452        // One call, before any read, so the resolve happens ONCE and everything
1453        // after it is a point lookup. A backend that indexes inside `put_pack`
1454        // (the gix arm) answers `Ok(0)` and pays nothing.
1455        match self.git.index_now() {
1456            Ok(0) => {}
1457            Ok(packs) => tracing::info!(
1458                target: GIT_TARGET,
1459                event = "git.receive_pack.indexed_for_check",
1460                packs = packs,
1461                elapsed_ms = started.elapsed().as_millis() as u64,
1462                "finished the deferred index before checking that the push closes"
1463            ),
1464            // **The push is REFUSED, and this used to be a warning it walked
1465            // past.** MEASURED on the rig 2026-08-30 at 8 GB, which is
1466            // what changed my mind: the backend's watchdog gave up by name
1467            // after 300 s (`index.gave_up ... 1.5G available, 1.6G needed`) —
1468            // exactly as designed, five minutes instead of the 1h45m park it
1469            // replaced — and then this arm logged a warning and went on to the
1470            // walk anyway. The walk's first read then fell into the very
1471            // read-time fallback `index_now` exists to keep it out of: a
1472            // whole-pack resolve on the connection thread, on a box that had
1473            // just said in writing that it cannot resolve this pack. The client
1474            // was back to hanging, one layer down.
1475            //
1476            // "Failing the push on this line would turn a slow index into a
1477            // refused mirror" — the old comment — mistook the error class. This
1478            // is not the backend being slow; it is the backend saying *I could
1479            // not make these objects addressable*. A check that cannot look
1480            // objects up cannot say whether the push closes, and the only
1481            // honest answers are "refused, here is why" or an unbounded wait.
1482            //
1483            // The backend's own sentence is carried through verbatim, because
1484            // it is the one that names what was available, what the floor is,
1485            // how far it got and what the operator can do. The bytes stay
1486            // durable and nothing partial is indexed, so the same push retried
1487            // on a bigger box is a no-op transfer and an ack.
1488            //
1489            // **A DIFFERENT event name from the ingest's**, deliberately.
1490            // `git.receive_pack.index_failed` already means *the pushed pack
1491            // could not be stored*; this one means *the pack is stored and
1492            // could not be indexed*. One name over two outcomes is a log
1493            // nobody can read.
1494            Err(err) => {
1495                tracing::warn!(
1496                    target: GIT_TARGET,
1497                    event = "git.receive_pack.index_for_check_failed",
1498                    commands = commands.len(),
1499                    elapsed_ms = started.elapsed().as_millis() as u64,
1500                    error = %format_args!("{err:#}"),
1501                    "the deferred index did not finish, so the push cannot be checked and is \
1502                     refused; the pack's bytes are durable and nothing partial was indexed"
1503                );
1504                // **The ROOT cause on the `ng` line, the whole chain in the
1505                // log.** `{err:#}` prints outermost-first, and on the rig at
1506                // 8 GB the outermost frame is the absorber's own context —
1507                // "absorbing pack 0 at (0, 8723679817) — its bytes are durable
1508                // and it stays un-indexed; reads will keep falling back rather
1509                // than answer absent" — which is true for a READER and is not
1510                // what happened to this push, and which buried the sentence
1511                // that matters ("this box cannot index this pack: the memory
1512                // watchdog gave up (deadline) with 1.2G available against a
1513                // 1.6G floor") 150 characters into a one-line pkt.
1514                //
1515                // A `ng` line is read by a person at a terminal, once. It gets
1516                // the cause; the log keeps the chain.
1517                let reason = format!("the push cannot be checked: {}", err.root_cause());
1518                return Ok(commands
1519                    .iter()
1520                    .map(|command| {
1521                        // A delete needs no object of this push to be
1522                        // addressable, and refusing one because an unrelated
1523                        // pack would not index is a refusal with no cause.
1524                        if command.is_delete() {
1525                            RefStatus::Ok
1526                        } else {
1527                            RefStatus::Rejected(reason.clone())
1528                        }
1529                    })
1530                    .collect());
1531            }
1532        }
1533
1534        // The frontier is what the repository already pointed at. It is built
1535        // ONCE, not once per command: a push of a thousand references used to
1536        // peel every reference in the repository a thousand times.
1537        let snapshot = RefSnapshot::read(self.git)?;
1538        let mut frontier_values: Vec<ObjectId> = Vec::with_capacity(snapshot.rows().len());
1539        for row in snapshot.rows() {
1540            // Direct rows only, exactly as before: a symbolic reference points
1541            // at one of the others, so following it would put the same oid in
1542            // the frontier twice and add nothing.
1543            if let Some(bytes) = &row.oid {
1544                frontier_values.push(crate::graph::oid(bytes)?);
1545            }
1546        }
1547        let graph = Graph::new(self.git, self.hash_kind).with_budget(self.config.budget);
1548        let frontier = Frontier::new(&graph, &frontier_values)?;
1549
1550        // ── ONE walk for the whole push, not one per reference ──────────────
1551        //
1552        // `verify_connectivity` already takes a *slice* of tips and shares one
1553        // `seen` set across all of them, and `Frontier` is already built once
1554        // for the whole push. What was still per-command was the
1555        // call, so N references over one shared history walked that history N
1556        // times — the second axis the earlier fix never touched.
1557        //
1558        // A mirror push is exactly that shape and it is the ordinary case for
1559        // an import: `linux.git` is 13 816 503 objects under **2 872**
1560        // references on one mainline, so the union of their closures is the
1561        // closure of the newest and the per-command loop was doing it 2 872
1562        // times. MEASURED 2026-08-05 on the in-memory fixture
1563        // (`tests/connectivity_cost.rs`): 64 references over one 200-commit
1564        // history cost **54.2x** what pushing the newest alone costs, 33 376
1565        // object touches against 616. The ratio is the reference count, so at
1566        // 2 872 it is the reason a kernel-sized push writes its pack and its
1567        // index and then never reports.
1568        //
1569        // And in wall clock, on a real repository over the `fs` backend, with
1570        // every object already present and an empty pack so that the
1571        // connectivity walk is the only thing in the bracket. `small.git`,
1572        // 12 328 objects, sha1; N references taken down one first-parent line,
1573        // which is `linux.git`'s shape; 2026-08-05, `taskset -c 0-7`, best
1574        // of three, **box not idle** (load average 33–48):
1575        //
1576        // ```text
1577        //   refs      per-command        batched      ratio
1578        //      1          0.086 s        0.084 s       1.0x
1579        //     16          0.561 s        0.104 s       5.4x
1580        //     64          1.978 s        0.141 s      14.0x
1581        //    256          6.903 s        0.122 s      56.6x
1582        //    512         10.424 s        0.153 s      68.1x
1583        // ```
1584        //
1585        // The left column is linear in the reference count and the right one is
1586        // flat, which is the whole claim. `njord.git` (10 468 objects, one
1587        // 966 MB blob) is the same shape at 17.0x for 256 references.
1588        //
1589        // # Why the per-command walk is still here
1590        //
1591        // Because the event stream reports **per reference**, and one batched walk can only
1592        // say that *something* in the push does not close. So the batch is the
1593        // fast path and the per-command loop is the attribution path, entered
1594        // only when the batch has already failed. That keeps the common case
1595        // O(union) and pays O(sum) only on a push that is being refused
1596        // anyway — which is also the one case where an operator needs the
1597        // precision. git makes the cheaper choice here and refuses the whole
1598        // batch with one message (`check_connected` over the entire command
1599        // list, `receive-pack.c`); this keeps gunnar's finer report at a cost
1600        // that is only ever paid on failure.
1601        let new_tips: Vec<ObjectId> = commands
1602            .iter()
1603            .filter(|command| !command.is_delete())
1604            .map(|command| command.new)
1605            .collect();
1606        let batch = if new_tips.is_empty() {
1607            Ok(0)
1608        } else {
1609            verify_connectivity(&graph, &new_tips, &frontier)
1610        };
1611        match &batch {
1612            Ok(objects) => tracing::info!(
1613                target: GIT_TARGET,
1614                event = "git.receive_pack.checked",
1615                tips = new_tips.len(),
1616                objects = *objects,
1617                elapsed_ms = started.elapsed().as_millis() as u64,
1618                "connectivity verified"
1619            ),
1620            Err(err) => tracing::warn!(
1621                target: GIT_TARGET,
1622                event = "git.receive_pack.check_failed",
1623                tips = new_tips.len(),
1624                elapsed_ms = started.elapsed().as_millis() as u64,
1625                error = %err,
1626                "the push does not close; re-walking one reference at a time to say which"
1627            ),
1628        }
1629
1630        for (index, command) in commands.iter().enumerate() {
1631            // **A deletion skips the WALK, not the GATE.**
1632            //
1633            // `if command.is_delete() { continue; }` stood here, and the half
1634            // of it that is right is the walk: a deletion introduces no
1635            // objects, `command.new` is the zero oid, and there is nothing for
1636            // `verify_connectivity` to reach. The `continue` carried
1637            // `gate.check` out with it — so **every deletion bypassed the
1638            // pre-receive gate entirely**.
1639            //
1640            // What that made unenforceable is not a corner: `allow_delete` is
1641            // the field whose whole purpose is refusing this push, and
1642            // `allowed_principal_ids` and the ref-name denylist were bypassable
1643            // the same way. A branch could be protected against a force-push,
1644            // visibly and correctly, and deleted outright by anyone with WRITE
1645            // — which is a strictly worse outcome than the force-push beside
1646            // it, and was invisible behind it because the force-push guard
1647            // worked.
1648            //
1649            // `gunnar-policy`'s protection gate has always had a
1650            // `UpdateKind::Delete` arm with its own refusal message, and
1651            // `tests/protection.rs` exercises `allow_delete` directly. The gate
1652            // was written for this and simply never handed one.
1653            //
1654            // Found by `gunnar-ui/uitest`'s `a-protected-branch-refuses-a-deletion`
1655            // going red while the force-push row beside it passed; pinned in
1656            // `gunnar-cli/tests/policy_mirror.rs::a_protection_rule_refuses_a_real_branch_deletion`,
1657            // which drives stock git over real ssh.
1658            if !command.is_delete() && batch.is_err() {
1659                if let Err(err) = verify_connectivity(&graph, &[command.new], &frontier) {
1660                    tracing::warn!(target: GIT_TARGET, ref_name = %command.name, error = %err, "push failed connectivity");
1661                    statuses[index] =
1662                        RefStatus::Rejected(format!("missing necessary objects: {err}"));
1663                    continue;
1664                }
1665            }
1666            if let Err(reason) = gate.check(command, facts) {
1667                statuses[index] = RefStatus::Rejected(reason);
1668            }
1669        }
1670
1671        crate::stage::end(_conn);
1672        if atomic && (already_refused || statuses.iter().any(|s| !s.is_ok())) {
1673            // One refusal fails the batch. Every other reference is reported
1674            // as refused too, or the client is told a lie about what landed.
1675            // `already_refused` carries in the refusals that never became a
1676            // command at all — a reference whose *name* this server will not
1677            // have (see [`UnnameableRef`]).
1678            for status in statuses.iter_mut() {
1679                if status.is_ok() {
1680                    *status = RefStatus::Rejected("atomic transaction failed".to_owned());
1681                }
1682            }
1683            return Ok(statuses);
1684        }
1685
1686        // `edit_for` peels a new annotated-tag tip **once, here on the write
1687        // path**, while the objects the push just delivered are warm and the
1688        // connectivity walk above has already proven them present, and hands the
1689        // terminal commit to the store as the ref row's peeled column. That is
1690        // the write half of the stored peel: with it stored, `advertise_from` reads the
1691        // peeled oid straight out of the row and never re-peels, so every later
1692        // clone, fetch and `ls-remote` pays zero object reads per tag instead of
1693        // one. The graph is the same one `verify_connectivity` just used.
1694        let edits: Vec<(usize, RefCas<'_>)> = commands
1695            .iter()
1696            .enumerate()
1697            .filter(|(index, _)| statuses[*index].is_ok())
1698            .map(|(index, command)| Ok((index, edit_for(command, &graph)?)))
1699            .collect::<Result<_>>()?;
1700
1701        let _txn = crate::stage::enter("ref_txn");
1702        tracing::info!(
1703            target: GIT_TARGET,
1704            event = "git.receive_pack.updating",
1705            edits = edits.len(),
1706            atomic,
1707            "applying the reference updates"
1708        );
1709        let started = std::time::Instant::now();
1710        if atomic {
1711            // ONE call. `put_refs_cas` applies every edit or none and names the
1712            // first whose expected value did not match; a loop with a break
1713            // leaves what it already applied applied, which is the thing
1714            // `--atomic` promises does not happen.
1715            let batch: Vec<RefCas<'_>> = edits
1716                .iter()
1717                .map(|(_, edit)| RefCas {
1718                    name: edit.name.clone(),
1719                    old: edit.old,
1720                    new: edit.new,
1721                    peeled: edit.peeled.clone(),
1722                })
1723                .collect();
1724            if let Err(err) = self.git.put_refs_cas(&batch) {
1725                let reason = format!("atomic transaction failed: {err:#}");
1726                for (index, _) in &edits {
1727                    statuses[*index] = RefStatus::Rejected(reason.clone());
1728                }
1729            }
1730        } else {
1731            for (index, edit) in edits {
1732                // A **one-element `put_refs_cas`**, not `update_ref`: both are
1733                // the contract's compare-and-swap-one-reference call and a
1734                // single-edit atomic batch has identical single-ref semantics —
1735                // it applies that one ref or rejects it, raising the same typed
1736                // `RefRejection::Cas` a lost race needs. `update_ref` cannot
1737                // carry a peel (its signature is `name/old/new`), and `put_refs`
1738                // would lose the CAS; `put_refs_cas` keeps the CAS **and** takes
1739                // the `RefCas::peeled` this edit computed, so the non-atomic path
1740                // stores a tag's peel exactly as the atomic path above does
1741                // (one implementation: one writer for both, not a peel-losing
1742                // second one).
1743                // Per-ref independence is preserved because each edit is its own
1744                // call — a rejection on one does not touch the others.
1745                if let Err(err) = self.git.put_refs_cas(std::slice::from_ref(&edit)) {
1746                    // ── THE TYPED REJECTION, RECOVERED ──────────────────────
1747                    //
1748                    // This used to render `{err:#}` and the comment here said
1749                    // so: "A LOST COMPARE-AND-SWAP AND A BROKEN DISK ARE THE
1750                    // SAME STRING HERE … Nothing may recover the distinction by
1751                    // matching on this text; the fix is a typed rejection on
1752                    // the contract."
1753                    //
1754                    // The contract had `RefRejection` all along
1755                    // (`git-storage-trait`); `put_refs_cas` raised it and
1756                    // `update_ref` raised a bare `bail!`. That is fixed, so the
1757                    // distinction is a `downcast_ref` rather than a substring
1758                    // match — and this is deliberately NOT parsing the message,
1759                    // which the old comment forbade in as many words.
1760                    //
1761                    // A lost race is rendered in git's own words so a client
1762                    // can act on it; anything else keeps the full chain,
1763                    // because a broken disk deserves every line it has.
1764                    statuses[index] = match err.downcast_ref::<RefRejection>() {
1765                        Some(RefRejection::Cas {
1766                            name,
1767                            expected,
1768                            actual,
1769                        }) => RefStatus::Rejected(format!(
1770                            "cannot lock ref '{name}': is at {} but expected {}",
1771                            render(actual),
1772                            render(expected)
1773                        )),
1774                        Some(RefRejection::Locked { name }) => RefStatus::Rejected(format!(
1775                            "cannot lock ref '{name}': held by another writer, try again"
1776                        )),
1777                        _ => RefStatus::Rejected(format!("{err:#}")),
1778                    };
1779                }
1780            }
1781        }
1782        tracing::info!(
1783            target: GIT_TARGET,
1784            event = "git.receive_pack.updated",
1785            // Counted off `statuses`, which is what the client is about to be
1786            // told, rather than off `edits`, which is only what was attempted.
1787            // A transaction that lost its compare-and-swap turns an edit into a
1788            // refusal, and a log reporting the attempt would report a reference
1789            // as moved that did not (measure, never assert).
1790            refs_ok = statuses.iter().filter(|s| s.is_ok()).count(),
1791            refs_rejected = statuses.iter().filter(|s| !s.is_ok()).count(),
1792            elapsed_ms = started.elapsed().as_millis() as u64,
1793            "the reference updates are applied"
1794        );
1795
1796        Ok(statuses)
1797    }
1798
1799    fn write_report(
1800        &self,
1801        out: &mut dyn Write,
1802        outcome: &PushOutcome,
1803        sideband: bool,
1804    ) -> Result<()> {
1805        // The lines come from the shared grammar (`gunnar-sendpack`), which is
1806        // the same code `gunnar-client` parses them with. What stays here is
1807        // the framing and the sideband, which belong to this transport.
1808        //
1809        // The two lists are merged back into the order the client sent, using
1810        // the position [`UnnameableRef`] carries. A client zips the report
1811        // against its own command list — `gunnar-client` does, and so does
1812        // `git push`'s per-ref output — so a report that silently reordered
1813        // would attribute a refusal to the wrong reference.
1814        let mut statuses: Vec<(usize, gunnar_sendpack::RefStatus)> = Vec::new();
1815        // The nameable commands sit at the positions the unnameable ones left
1816        // free, in order, which is enough to recover the original interleaving
1817        // without threading a position through `RefCommand` as well.
1818        let taken: std::collections::BTreeSet<usize> =
1819            outcome.unnameable.iter().map(|r| r.position).collect();
1820        let mut free = (0..).filter(|position| !taken.contains(position));
1821        for (command, status) in outcome.commands.iter().zip(&outcome.statuses) {
1822            let position = free.next().expect("the position sequence is infinite");
1823            statuses.push((
1824                position,
1825                match status {
1826                    RefStatus::Ok => gunnar_sendpack::RefStatus::ok(command.name.as_bytes()),
1827                    RefStatus::Rejected(reason) => {
1828                        gunnar_sendpack::RefStatus::rejected(command.name.as_bytes(), reason)
1829                    }
1830                },
1831            ));
1832        }
1833        for refused in &outcome.unnameable {
1834            statuses.push((
1835                refused.position,
1836                gunnar_sendpack::RefStatus::rejected(
1837                    echoable_name(&refused.name, refused.reason.len()),
1838                    &refused.reason,
1839                ),
1840            ));
1841        }
1842        statuses.sort_by_key(|(position, _)| *position);
1843        let statuses: Vec<gunnar_sendpack::RefStatus> =
1844            statuses.into_iter().map(|(_, status)| status).collect();
1845        let mut report = Vec::new();
1846        for line in gunnar_sendpack::report::lines(
1847            outcome.unpack.as_ref().copied().map_err(String::as_str),
1848            &statuses,
1849        ) {
1850            gunnar_pktline::write_text(&mut report, &line)?;
1851        }
1852        gunnar_pktline::write_flush(&mut report)?;
1853
1854        if sideband {
1855            // The notice first, on band 2, so `git push` prints it as
1856            // `remote: …` ABOVE the per-ref lines — where a person reads before
1857            // the summary — and separately framed so a client that ignores band
1858            // 2 still parses the report untouched.
1859            if let Some(notice) = &self.config.notice {
1860                let mut line = notice.clone();
1861                if !line.ends_with('\n') {
1862                    line.push('\n');
1863                }
1864                wire_io::progress(out, true, &line)?;
1865            }
1866            // The whole report, its terminating flush-pkt included, rides band
1867            // 1; the connection is then ended by an *unframed* flush. That is
1868            // what `git-receive-pack` 2.53 puts on the wire (captured
1869            // 2026-08-03), and a client reading a demultiplexed stream cannot
1870            // see the difference between the two flushes any other way.
1871            wire_io::sideband(out, BAND_PACK, &report)?;
1872            wire_io::flush(out)?;
1873        } else {
1874            out.write_all(&report)?;
1875        }
1876        Ok(())
1877    }
1878}
1879
1880/// The most of a refused reference name that can go into an `ng` line without
1881/// the line outgrowing a pkt-line.
1882///
1883/// A refused name is the one name on the wire that is **not** bounded by
1884/// anything gunnar chose: it arrived because a client sent it, and the only
1885/// ceiling it ever passed is the command packet's own 65 516 bytes. Echoing it
1886/// whole into `ng <name> <reason>\n` therefore does not fit whenever the name is
1887/// within a few dozen bytes of that ceiling, and `write_text` refuses the packet
1888/// rather than emitting a header that lies. The report then does not get
1889/// written at all — the session dies on the refusal path, which is precisely the
1890/// failure [`UnnameableRef`] exists to remove.
1891///
1892/// So a name too big to echo is truncated and **says so**, rather than costing
1893/// the client the whole report. A name this long cannot match anything the
1894/// client will keep anyway: every legal name is at most
1895/// [`gunnar_store::MAX_REF_COMPONENT_BYTES`] per component.
1896fn echoable_name(name: &[u8], reason_len: usize) -> Vec<u8> {
1897    const MARKER: &[u8] = b"\xe2\x80\xa6[truncated]";
1898    // `ng ` + name + ` ` + reason + `\n`.
1899    let overhead = 3 + 1 + reason_len + 1;
1900    let budget = gunnar_pktline::MAX_PAYLOAD.saturating_sub(overhead);
1901    if name.len() <= budget {
1902        return name.to_vec();
1903    }
1904    let keep = budget.saturating_sub(MARKER.len());
1905    let mut out = name[..keep].to_vec();
1906    out.extend_from_slice(MARKER);
1907    out
1908}
1909
1910/// The rules `FullName::new` used to run, plus the one the contract adds.
1911///
1912/// Two failures with one shape, because both produce the same `ng` line and a
1913/// caller that had to tell them apart would be re-deriving the difference from
1914/// a message: the name is not a legal reference name, or it is legal and is not
1915/// UTF-8 and therefore cannot be a `RefRow::name`. See
1916/// [`ReceivePack::parse_command`] for why the second is a recorded narrowing
1917/// rather than a bug.
1918fn validate_ref_name(name: &[u8]) -> std::result::Result<String, String> {
1919    gunnar_store::refname::validate(name).map_err(|err| match err {
1920        // The validator renders the offending name inside its own message and
1921        // the `ng` line already carries the name, so only the *rule* is taken.
1922        // A 60 000-byte name once produced a 120 105-byte packet, `write_text`
1923        // refused it, and the whole report failed to write. MEASURED
1924        // 2026-08-06.
1925        gunnar_store::Error::InvalidRefName { reason, .. } => reason.to_owned(),
1926        other => other.to_string(),
1927    })?;
1928    String::from_utf8(name.to_vec())
1929        .map_err(|_| "a reference name gunnar can store must be UTF-8".to_owned())
1930}
1931
1932/// One command as a compare-and-swap edit, with a tag's peeled target attached.
1933///
1934/// The three shapes `PreviousValue`/`Change` used to spell out collapse into
1935/// two `Option`s, because that is what the contract says and it says the same
1936/// thing: `old: None` is *"must not exist"* (a create) and `new: None` is a
1937/// delete. A non-create carries `command.old`, which is the client's claim
1938/// about what the reference holds and the only thing that makes this a race
1939/// the server can lose safely.
1940///
1941/// The fourth field, `peeled`, is the one this fn computes rather than copies:
1942/// an annotated tag's terminal commit, so the store persists it and the
1943/// advertisement never re-peels. It needs the object graph, which is why
1944/// this takes `graph` and returns a `Result` — a peel is a store read.
1945fn edit_for<'a>(command: &'a RefCommand, graph: &Graph<'_>) -> Result<RefCas<'a>> {
1946    // The peeled column, filled only for a create/update whose new tip is an
1947    // annotated tag. `peel` follows the tag chain to the first non-tag object
1948    // (a commit, in the common case of one tag over a commit) and reports the
1949    // tags it walked through: `tags` is non-empty **iff** the tip really was a
1950    // tag object, which is the only case git's `packed-refs` writes a `^peeled`
1951    // line for. A delete has no tip; a branch head or a lightweight tag peels to
1952    // itself with no tag walked, so both carry `None` and the store keeps no
1953    // peel — matching packed-refs exactly. This is the one peel per new tag the
1954    // whole stored-peel fix costs, paid once at ingest instead of on every
1955    // advertisement.
1956    //
1957    // The oid is **owned** here (`RefCas::peeled` is `Option<Vec<u8>>`): unlike
1958    // `old`/`new`, which are slices of the command line the caller still holds,
1959    // the peel is derived from the store and borrows nothing.
1960    let peeled = if command.is_delete() {
1961        None
1962    } else {
1963        let peeled = graph.peel(&command.new)?;
1964        (!peeled.tags.is_empty()).then(|| peeled.id.as_bytes().to_vec())
1965    };
1966    Ok(RefCas {
1967        name: command.name.clone(),
1968        old: (!command.is_create()).then(|| command.old.as_bytes()),
1969        new: (!command.is_delete()).then(|| command.new.as_bytes()),
1970        peeled,
1971    })
1972}
1973
1974#[cfg(test)]
1975mod tests {
1976    use super::*;
1977
1978    /// **[`echoable_name`] is a fix, not a guard, and it is not reachable from
1979    /// the wire today. Both facts, with the arithmetic that decides them.**
1980    ///
1981    /// A command line is one pkt-line, so the longest reference name a client
1982    /// can physically send is `MAX_PAYLOAD - (40 + 1 + 40 + 1 + 1)` =
1983    /// **65 433** bytes. The `ng` line it comes back in is
1984    /// `"ng " + name + " " + reason + "\n"`, so the name fits whenever the
1985    /// reason is at most `65 511 - 65 433` = **78** bytes. Every reason
1986    /// [`ReceivePack::parse_command`] can produce is `"funny refname: "` plus
1987    /// one of the validator's fixed strings, and the longest of those makes
1988    /// **51**. 51 < 78, so no push can currently reach the truncation.
1989    ///
1990    /// That margin is 27 bytes of coincidence between two constants nobody
1991    /// holds together. One longer refusal string and the report becomes
1992    /// unwritable — `write_text` refuses the packet, `write_report` returns
1993    /// `Err`, and the session dies **on the refusal path**, which is the exact
1994    /// failure [`UnnameableRef`] exists to remove. So the truncation stays, and
1995    /// this is where it is proven, since the wire cannot prove it.
1996    #[test]
1997    fn a_refused_name_too_long_to_echo_is_truncated_rather_than_costing_the_report() {
1998        let name = vec![b'x'; gunnar_pktline::MAX_PAYLOAD - 40];
1999        let reason = "funny refname: ".to_owned() + &"y".repeat(200);
2000
2001        let echoed = echoable_name(&name, reason.len());
2002
2003        // The property that matters, asserted FIRST so a red run names the
2004        // consequence and not a proxy for it: the line frames.
2005        let mut line = b"ng ".to_vec();
2006        line.extend_from_slice(&echoed);
2007        line.push(b' ');
2008        line.extend_from_slice(reason.as_bytes());
2009        let mut out = Vec::new();
2010        gunnar_pktline::write_text(&mut out, &line).unwrap_or_else(|err| {
2011            panic!(
2012                "the `ng` line for a refused {}-byte name does not fit in a pkt-line ({err}), \
2013                 so the whole report fails to write and the session dies on the refusal path",
2014                name.len()
2015            )
2016        });
2017        assert!(
2018            echoed.ends_with("…[truncated]".as_bytes()),
2019            "the name was cut to fit and the client is not told; a truncated name that \
2020             does not say so is a name the client cannot match against its own list"
2021        );
2022
2023        // And the acceptance half: a name that fits is not touched.
2024        let short = b"refs/heads/main".to_vec();
2025        assert_eq!(
2026            echoable_name(&short, reason.len()),
2027            short,
2028            "a name that fits was truncated anyway"
2029        );
2030    }
2031}