Skip to main content

gunnar_wire/
upload_pack.rs

1//! `git-upload-pack`: serving fetch and clone.
2//!
3//! Protocol **v2** is the real implementation; **v0** is a fallback that has to
4//! exist because real clients still open it. That is not hypothetical: `git`
5//! 2.53 falls back to v0 whenever `GIT_PROTOCOL` does not reach the server
6//! process, which is exactly what happens over the `ext::` transport. Measured
7//! 2026-08-03, and the reason `tests/real_git_interop.rs` gets a genuine,
8//! unforced v0 exchange for free.
9//!
10//! # Shape
11//!
12//! Nothing here owns a socket. [`UploadPack::serve`] takes `&mut dyn BufRead`
13//! and `&mut dyn Write`; `gunnar-ssh` supplies a russh channel, a test supplies
14//! a pipe or a `Vec<u8>`, and the state machine cannot tell.
15//!
16//! # `object-format`
17//!
18//! Echoed from the repository's own hash kind — carried in at construction,
19//! from `RepoStore::hash_kind()` — and never from a default. Answering `sha1` to a sha256 repository does not error; it returns
20//! an *empty* response, which is why it cost a session in `nornir-git` and why
21//! [`crate::caps::check_object_format`] turns a mismatch into a hard error here.
22
23use std::collections::HashSet;
24use std::io::{BufRead, Write};
25
26use gix_hash::{Kind, ObjectId};
27// `write_pack` and `WriteOptions` are gone from this module: the engine owns
28// the format now, and the three knobs that used to be `WriteOptions` —
29// `ofs_delta`, `thin`, the base-closure ceiling — cross the seam as
30// `git_storage_trait::Caps`. `WriteOutcome` stays, because it is what this end
31// reports and the server logs.
32use gunnar_pktline::Packet;
33use gunnar_store::git_storage_trait::GitServe;
34use gunnar_store::object;
35
36use crate::caps::{self, Offered, PeerCapabilities};
37use crate::filter::Filter;
38use crate::graph::{
39    has_for_negotiation, reachable_closure, select_objects, Graph, Missing, SelectOptions,
40};
41use crate::limits::{Budget, Limits};
42use crate::negotiate::{negotiate, HaveMemo};
43use crate::refs::RefSnapshot;
44use crate::shallow::{self, ShallowOutcome, ShallowRequest};
45use crate::wire_io::{self, err_packet, progress, read_packet, read_section, PackOutput};
46use crate::{Error, ProtocolVersion, Result, Session};
47
48/// Knobs the SSH layer sets per connection.
49#[derive(Debug, Clone)]
50pub struct Config {
51    /// The `agent=` string. Defaults to `gunnar/<version>`.
52    pub agent: String,
53    /// Which optional v2 features are on offer.
54    pub offered: Offered,
55    /// Which object ids a client may `want`.
56    pub want_policy: WantPolicy,
57    /// An operator's explicit cap on the objects one traversal may hold.
58    ///
59    /// **`None` by default, and that is not a guard left off.**
60    /// [`Limits::walk_bytes`] is the ceiling that ships, in bytes, and this is
61    /// `--fetch-budget` for an operator who wants a second one in objects.
62    ///
63    /// It used to default to [`crate::DEFAULT_BUDGET`], and leaving it there
64    /// would have made the byte ceiling decorative: MEASURED 2026-08-05,
65    /// ten million objects refuses a clone of a 14 042 000-object repository
66    /// that the same machine serves in 5.59 GB, so the object cap and not the
67    /// memory would still have been what decided.
68    ///
69    /// See [`crate::DEFAULT_BUDGET`] for why the unit was wrong and
70    /// [`limits`](crate::limits) for the ceilings that are about bytes.
71    pub budget: Option<usize>,
72    /// The memory ceilings this connection serves under.
73    ///
74    /// Defaults to [`Limits::default`], which is **every ceiling on**. See
75    /// [`limits`](crate::limits).
76    pub limits: Limits,
77    /// The server-wide purse this connection's traversals spend from.
78    ///
79    /// **`None` by default, and this is the one field where that is not a guard
80    /// left off.** A pool is shared state between connections, so it can only be
81    /// created by whatever owns them all: `gunnar serve` builds one sized to the
82    /// machine and hands the same `Arc` to every session. A `Config` built by
83    /// hand — a test, `gunnar mirror`, an embedding — has no siblings to share
84    /// with, and giving it a private pool of its own would be a ceiling that
85    /// bounds nothing while reading as if it did.
86    ///
87    /// [`Limits::walk_bytes`] is the per-request half and is on by default. See
88    /// [`WalkPool`](crate::WalkPool) for why one without the other is not a
89    /// bound on the server.
90    pub walk_pool: Option<std::sync::Arc<crate::WalkPool>>,
91}
92
93impl Default for Config {
94    fn default() -> Self {
95        Config {
96            agent: caps::default_agent(),
97            offered: Offered::default(),
98            want_policy: WantPolicy::default(),
99            budget: None,
100            limits: Limits::default(),
101            walk_pool: None,
102        }
103    }
104}
105
106/// Which object ids a client is allowed to ask for.
107///
108/// git spells this as three separate `uploadpack.allow*SHA1InWant` settings,
109/// and the middle one is not optional for a server that advertises `filter`: a
110/// partial clone's **lazy fetch** asks for bare blob ids that were never
111/// advertised, so [`WantPolicy::AdvertisedTips`] turns every `git checkout` in
112/// a `blob:none` clone into "the remote end hung up unexpectedly".
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
114pub enum WantPolicy {
115    /// Only ids that appeared in the advertisement.
116    ///
117    /// The strictest setting, and the one that breaks partial clone.
118    AdvertisedTips,
119    /// Anything reachable from a reference. This is git's
120    /// `uploadpack.allowReachableSHA1InWant`, and the default here.
121    ///
122    /// **The cost is honest and real:** proving reachability means walking the
123    /// repository's whole object closure once per request that names a
124    /// non-tip id. The `git` package's `__gunnar_reach__` bitmaps are what make this
125    /// cheap; until they exist, this walk is the price of partial clone.
126    #[default]
127    ReachableFromRefs,
128    /// Anything present in the store, reachable or not.
129    ///
130    /// Makes every object that was ever pushed fetchable by whoever can name
131    /// it, including on branches that have since been deleted.
132    Any,
133}
134
135impl WantPolicy {
136    /// Does this policy make good on a capability row that
137    /// [`requires`](crate::caps::Capability::requires) `promise`?
138    ///
139    /// # Why the answer is a `match` and not a `>=`
140    ///
141    /// The three variants happen to be orderable, and encoding that as an
142    /// ordering would make adding a fourth silently inherit whichever
143    /// neighbours it landed between. Written out, a new variant does not
144    /// compile until someone decides what it promises — which is the property
145    /// that was missing, not the ordering.
146    ///
147    /// The mapping is git's, bit for bit: `ALLOW_TIP_SHA1` prints
148    /// `allow-tip-sha1-in-want`, `ALLOW_REACHABLE_SHA1` prints
149    /// `allow-reachable-sha1-in-want`, and `ALLOW_ANY_SHA1` is both bits set,
150    /// so it prints both (`upload-pack.c`, `send_ref`).
151    ///
152    /// [`WantPolicy::AdvertisedTips`] promises nothing, on purpose: gunnar
153    /// hides no reference, so "only ids in the advertisement" is exactly what a
154    /// v0 client assumes when neither capability is present.
155    pub fn advertises(self, promise: WantPolicy) -> bool {
156        match (self, promise) {
157            // Reachability is promised by the policy that serves it and by the
158            // one that serves more.
159            (WantPolicy::ReachableFromRefs, WantPolicy::ReachableFromRefs) => true,
160            (WantPolicy::Any, WantPolicy::ReachableFromRefs) => true,
161            (WantPolicy::Any, WantPolicy::Any) => true,
162            (WantPolicy::AdvertisedTips, _) => false,
163            (WantPolicy::ReachableFromRefs, WantPolicy::Any) => false,
164            (_, WantPolicy::AdvertisedTips) => false,
165        }
166    }
167}
168
169/// The server side of `git-upload-pack`.
170///
171/// One of these serves **one connection**. That matters for [`Self::fetch`],
172/// which a v2 client calls once per negotiation round: the `have`s a client has
173/// already been answered about are memoised here rather than re-resolved
174/// against the object store every round. The memo holds no decisions, only
175/// facts about the store (see `negotiate::HaveMemo`), so a shared or reused
176/// instance answers the same, only slower.
177pub struct UploadPack<'a> {
178    /// **One store, not two.** `ObjectStore` and `RefStore` were separate
179    /// gunnar traits and are deleted; `GitOps` says both, and `GitServe`
180    /// extends it with the decoded read this crate cannot work without. So the
181    /// objects and the references are the same handle, which is also the only
182    /// way an advertisement and the pack it promises can be one snapshot.
183    git: &'a dyn GitServe,
184    /// The repository's hash kind.
185    ///
186    /// Carried rather than asked for: `GitServe` has no `hash_kind()` and the
187    /// frozen signature leaves whether `GitOps` should grow one open. `RepoStore::hash_kind()`
188    /// already exists and the server already holds one per repository, so
189    /// nothing is being invented here — only carried the last hop, and the open
190    /// question keeps one fewer consumer.
191    hash_kind: Kind,
192    config: Config,
193    haves: HaveMemo,
194    /// Whether a sideband is open on this connection, and therefore which of
195    /// the two channels [`UploadPack::report_error`] must use.
196    ///
197    /// Set by [`UploadPack::send_pack`], which is the one place either protocol
198    /// version opens one. Before that an `ERR` pkt-line is the only thing a
199    /// client will read; after it, a bare pkt-line in the middle of a
200    /// demultiplexed stream is a framing error rather than a message, so the
201    /// reason has to ride band 3.
202    ///
203    /// An `AtomicBool` rather than a `Cell` to match [`HaveMemo`]'s `Mutex`:
204    /// this type is built inside `spawn_blocking` and nothing shares it across
205    /// threads today, but a `Cell` here would silently make it `!Sync` and the
206    /// day someone does share it the compiler should be the one saying so.
207    sideband_open: std::sync::atomic::AtomicBool,
208}
209
210/// One reference as it goes out in an advertisement.
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct AdvertisedRef {
213    /// Full reference name.
214    ///
215    /// `String`, not the `FullName` newtype that died with `RefStore`: the
216    /// contract speaks `RefRow::name: String` and minting a second validated
217    /// name type in gunnar is the duplication the demolition removes. What the
218    /// newtype carried *by construction* is now a rule about the producer —
219    /// see `receive_pack::parse_command`, which is where a client-supplied name
220    /// is validated before it can reach a store.
221    pub name: String,
222    /// What it resolves to, or `None` for an unborn `HEAD`.
223    pub target: Option<ObjectId>,
224    /// The commit an annotated tag points at, when peeling was asked for.
225    pub peeled: Option<ObjectId>,
226    /// The reference a symbolic reference points at.
227    pub symref_target: Option<String>,
228}
229
230/// The arguments of `command=ls-refs`.
231#[derive(Debug, Clone, Default, PartialEq, Eq)]
232pub struct LsRefsRequest {
233    /// Report `symref-target:` for symbolic references.
234    pub symrefs: bool,
235    /// Report `peeled:` for annotated tags.
236    pub peel: bool,
237    /// Report an unborn `HEAD` instead of omitting it.
238    pub unborn: bool,
239    /// Only references starting with one of these; empty means all.
240    pub ref_prefixes: Vec<String>,
241}
242
243impl LsRefsRequest {
244    /// Parse the argument section of `command=ls-refs`.
245    pub fn parse(lines: &[Vec<u8>]) -> Result<Self> {
246        let mut out = LsRefsRequest::default();
247        for line in lines {
248            let text = String::from_utf8_lossy(line).into_owned();
249            let (name, value) = split_argument(&text);
250            let Some(argument) = caps::argument(caps::LS_REFS_ARGUMENTS, name) else {
251                return Err(Error::UnknownArgument {
252                    command: "ls-refs",
253                    argument: text,
254                });
255            };
256            match (argument.name, argument.value, value) {
257                ("symrefs", _, _) => out.symrefs = true,
258                ("peel", _, _) => out.peel = true,
259                ("unborn", _, _) => out.unborn = true,
260                ("ref-prefix", caps::Value::Required, Some(prefix)) => {
261                    out.ref_prefixes.push(prefix.to_owned())
262                }
263                _ => {
264                    return Err(Error::UnknownArgument {
265                        command: "ls-refs",
266                        argument: text,
267                    })
268                }
269            }
270        }
271        Ok(out)
272    }
273}
274
275/// The arguments of `command=fetch`, and of a v0 `want` section.
276#[derive(Debug, Clone, Default, PartialEq, Eq)]
277pub struct FetchRequest {
278    /// `want <oid>`.
279    pub wants: Vec<ObjectId>,
280    /// `want-ref <name>`, the `ref-in-want` feature.
281    pub want_refs: Vec<String>,
282    /// `have <oid>`.
283    pub haves: Vec<ObjectId>,
284    /// `done`: stop negotiating and send the pack.
285    pub done: bool,
286    /// `thin-pack`, and since the thin-pack ruling this one is **honoured**, not ignored.
287    ///
288    /// Pack-copy preserves the deltas the pushing client computed, but until
289    /// 2026-08-04 only those whose base was also in the transfer: the rest
290    /// were inflated and deflated whole, because `allow_thin_pack: false` will
291    /// not point an entry at a base the receiver may not have. On a **clone**
292    /// that costs nothing, since the transfer is the whole history and every
293    /// base is in it. On a **negotiated fetch** it is the dominant cost —
294    /// MEASURED 2026-08-04, 24 of 40 objects expanded and 3.98x stock
295    /// git's thin pack of the same object set.
296    ///
297    /// What changed is not the caution, only what backs it: the walk that
298    /// decides the transfer already knows what the client holds
299    /// (`graph::Selection::client_has`), so a base can now be *checked*
300    /// instead of assumed unsafe. A base that does not answer still costs its
301    /// object the delta. See `send_pack` for the three gating conditions.
302    pub thin_pack: bool,
303    /// `no-progress`.
304    pub no_progress: bool,
305    /// `include-tag`.
306    pub include_tag: bool,
307    /// `ofs-delta`, and since pack-copy this one is **honoured**, not ignored.
308    ///
309    /// The pack-copy path emits an entry exactly as it found it in a pack, and
310    /// most of those entries are `ofs-delta`s. A client that did not ask for
311    /// `ofs-delta` cannot read them, so it gets whole objects instead. That is
312    /// what `git-upload-pack` does too: it passes `--delta-base-offset` to
313    /// `pack-objects` only when the client advertised the capability.
314    ///
315    /// Every git since 2005 sends it and stock `git 2.53.0` always does, so
316    /// this is the fallback, not the path. It is a real fallback though, and
317    /// `tests/pack_copy.rs` exercises it rather than assuming nobody lands
318    /// there.
319    pub ofs_delta: bool,
320    /// `filter <spec>`.
321    pub filter: Option<Filter>,
322    /// `wait-for-done`.
323    pub wait_for_done: bool,
324    /// Everything shallow-related.
325    pub shallow: ShallowRequest,
326}
327
328impl FetchRequest {
329    /// Parse the argument section of `command=fetch`.
330    ///
331    /// Unknown arguments are an error rather than a shrug. The argument table
332    /// in [`crate::caps`] is the same one the advertisement is generated from,
333    /// so "accepted here" and "advertised there" cannot drift.
334    pub fn parse(lines: &[Vec<u8>], hash_kind: Kind, offered: Offered) -> Result<Self> {
335        let mut out = FetchRequest::default();
336        for line in lines {
337            let text = String::from_utf8_lossy(line).into_owned();
338            let (name, value) = split_argument(&text);
339            let argument = caps::argument(caps::FETCH_ARGUMENTS, name)
340                .filter(|argument| offered.accepts(argument))
341                .ok_or_else(|| Error::UnknownArgument {
342                    command: "fetch",
343                    argument: text.clone(),
344                })?;
345            let required = || {
346                value.ok_or_else(|| Error::UnknownArgument {
347                    command: "fetch",
348                    argument: text.clone(),
349                })
350            };
351            match argument.name {
352                "want" => out.wants.push(parse_oid(required()?, hash_kind)?),
353                "have" => out.haves.push(parse_oid(required()?, hash_kind)?),
354                "want-ref" => out.want_refs.push(required()?.to_owned()),
355                "done" => out.done = true,
356                "thin-pack" => out.thin_pack = true,
357                "no-progress" => out.no_progress = true,
358                "include-tag" => out.include_tag = true,
359                "ofs-delta" => out.ofs_delta = true,
360                "wait-for-done" => out.wait_for_done = true,
361                "filter" => out.filter = Some(Filter::parse(required()?)?),
362                "shallow" => out
363                    .shallow
364                    .client_shallow
365                    .push(parse_oid(required()?, hash_kind)?),
366                "deepen" => {
367                    out.shallow.depth = Some(required()?.parse::<usize>().map_err(|_| {
368                        Error::protocol(format!("deepen wants a number, got {text:?}"))
369                    })?)
370                }
371                "deepen-relative" => out.shallow.relative = true,
372                "deepen-since" => {
373                    out.shallow.since = Some(required()?.parse::<i64>().map_err(|_| {
374                        Error::protocol(format!("deepen-since wants unix seconds, got {text:?}"))
375                    })?)
376                }
377                "deepen-not" => out.shallow.not.push(required()?.to_owned()),
378                other => {
379                    return Err(Error::UnknownArgument {
380                        command: "fetch",
381                        argument: other.to_owned(),
382                    })
383                }
384            }
385        }
386        Ok(out)
387    }
388}
389
390/// What a whole `upload-pack` session did.
391///
392/// Returned rather than logged because the SSH layer wants it for metrics and
393/// the interop tests want it as evidence: "the fetch succeeded" is true of a
394/// server that negotiated properly and of one that shipped the entire
395/// repository every time, and only `fetch_rounds` and `objects_sent` tell them
396/// apart.
397#[derive(Debug, Clone, Default, PartialEq, Eq)]
398pub struct UploadSummary {
399    /// Which protocol was spoken.
400    pub protocol: ProtocolVersion,
401    /// How many `ls-refs` commands were served.
402    pub ls_refs: usize,
403    /// How many negotiation rounds the client took, the one that carried the
404    /// packfile included.
405    pub fetch_rounds: usize,
406    /// Objects in the packfile, if one was sent.
407    ///
408    /// **This is what the ENGINE emitted, not what this end chose.** See
409    /// [`objects_selected`](Self::objects_selected) for the other half; a
410    /// reader that has only this one cannot tell the two apart.
411    pub objects_sent: u32,
412    /// Objects [`GitServe::select`] chose — the **exact set** `emit_pack` was
413    /// handed.
414    ///
415    /// # Why both counts, and why this one is asserted rather than logged
416    ///
417    /// `emit_pack`'s first parameter is an exact set and the engine must not
418    /// close it over reachability (DECIDED 2026-08-10). It may still emit
419    /// *more* than it was given, legitimately: an `OFS_DELTA` names its base by
420    /// a backwards distance **inside the pack**, so an engine copying stored
421    /// entries has to bring along any delta base that fell outside the
422    /// selection or emit something unparseable. `Caps::thin` is the allowance
423    /// to skip that, and an engine is free not to take it.
424    ///
425    /// So the gap is expected and **unbounded**, which is precisely why it has
426    /// to be reported rather than assumed small. MEASURED on the laptop 2026-08-10,
427    /// the `h2h-linear-sha1-2048c-1024f-16k` corpus over the znippy engine, a
428    /// negotiated fetch from a receiver `N` commits behind the tip:
429    ///
430    /// ```text
431    ///   N commits behind    selected    sent    amplification
432    ///                  1          18     157            8.72x
433    ///                256        4382    7801            1.78x
434    /// ```
435    ///
436    /// Until this field existed the left-hand column reached exactly one place
437    /// — a human-readable sideband progress line that `git fetch -q` suppresses
438    /// and no meter parses — so `bench/`'s `gunnar.vs_git_push` could report a
439    /// 33.6% divergence with nothing anywhere able to attribute it. That is the
440    /// failure a published number can hide: one describing a path other than the
441    /// one its name claims.
442    ///
443    /// Zero for a session that sent no packfile. Summed over rounds in v2, for
444    /// the same reason [`objects_sent`](Self::objects_sent) is.
445    pub objects_selected: usize,
446    /// How many `have`s turned out to be common on the final round.
447    pub common: usize,
448    /// Of [`objects_sent`](Self::objects_sent), how many left as a **copy** of
449    /// the pack entry they were already stored as.
450    ///
451    /// Reported for the same reason `fetch_rounds` and `objects_sent` are: "the
452    /// clone succeeded" is true of a server that shipped the client's own
453    /// deltas back and of one that inflated and re-deflated 528 MiB to send
454    /// 18× the bytes, and nothing else on this struct tells them apart. A byte
455    /// count cannot either, since it moves with the corpus.
456    pub objects_copied_from_pack: usize,
457    /// Of [`objects_sent`](Self::objects_sent), how many had to be deflated
458    /// from their decompressed content: everything, on a store with no packs,
459    /// and on a packed one only the loose objects and the deltas whose base
460    /// did not come along.
461    pub objects_recompressed: usize,
462    /// Whether the packfile round's object selection came from a `.bitmap`
463    /// rather than from the walk.
464    ///
465    /// # Why this is on the summary and not only in a log line
466    ///
467    /// Because the two paths do **not** produce identical bytes and every
468    /// measurement of this server is therefore a measurement of one of them.
469    /// The bitmap path vouches for the client's whole closure
470    /// (`ClientHas::Bits`) where `Exclusion::Edges`' walk vouches only for the
471    /// frontier, so a delta base the walk expands the bitmap names — same
472    /// request, same repository, fewer bytes. Nothing else a server reports
473    /// says which one answered, and the failure is silent in the direction that
474    /// matters: a repository with no `.bitmap` on disk falls through to the
475    /// walk and every number taken on it is a walk number wearing the
476    /// default's name.
477    ///
478    /// MEASURED 2026-08-04: the head-to-head fetch arm's server fixture is
479    /// a `git clone --bare` carve, `git clone` writes no `.bitmap`, and so
480    /// `gunnar.vs_git_fetch_divergent` — the one arm gunnar loses on bytes —
481    /// had never once exercised the shipping path. `false` here is what says
482    /// so.
483    ///
484    /// `false` for a session that sent no packfile, and for a request shape
485    /// [`crate::bitmap::select`] declines (a filter, a shallow boundary, a
486    /// deepening).
487    pub answered_from_bitmap: bool,
488    /// Thin-delta bases `gix` offered over the session, and how many
489    /// `client_has` refused. See [`crate::pack::WriteOutcome::thin_bases_offered`].
490    pub thin_bases_offered: usize,
491    /// See [`crate::pack::WriteOutcome::thin_bases_vetoed`].
492    pub thin_bases_vetoed: usize,
493    /// See [`crate::pack::WriteOutcome::closure_bases_declined`]. Non-zero only
494    /// on a clone out of a **multi-pack** store whose selection reaches one
495    /// object by two routes, which is the shape no fixture in this tree had
496    /// until 2026-08-05.
497    pub closure_bases_declined: usize,
498}
499
500/// What one `fetch` produced.
501#[derive(Debug, Clone, Default)]
502pub struct FetchOutcome {
503    /// The `have`s this repository holds.
504    pub common: Vec<ObjectId>,
505    /// Whether the server declared a cut point.
506    pub ready: bool,
507    /// Whether a packfile was sent, as opposed to another negotiation round.
508    pub sent_pack: bool,
509    /// How many objects went into it — what the **engine** emitted.
510    pub objects_sent: u32,
511    /// How many objects `select` chose. See
512    /// [`UploadSummary::objects_selected`], which is where the two-column
513    /// argument is written down.
514    pub objects_selected: usize,
515    /// How many of those were copied out of a pack rather than deflated
516    /// out of an inflated object. See [`UploadSummary::objects_copied_from_pack`].
517    pub objects_copied_from_pack: usize,
518    /// How many had to be deflated. See
519    /// [`UploadSummary::objects_recompressed`].
520    pub objects_recompressed: usize,
521    /// Whether a `.bitmap` answered this round's selection. See
522    /// [`UploadSummary::answered_from_bitmap`].
523    pub answered_from_bitmap: bool,
524    /// See [`UploadSummary::thin_bases_offered`].
525    pub thin_bases_offered: usize,
526    /// See [`UploadSummary::thin_bases_vetoed`].
527    pub thin_bases_vetoed: usize,
528    /// See [`UploadSummary::closure_bases_declined`].
529    pub closure_bases_declined: usize,
530}
531
532/// What [`UploadPack::send_pack`] wrote, and which selection path chose it.
533///
534/// A pair rather than two out-parameters, and its own type rather than a
535/// tuple, because the second half is the thing a reader of the first half most
536/// needs to know: a byte count from the walk and a byte count from the bitmap
537/// are answers to different questions (see
538/// [`UploadSummary::answered_from_bitmap`]).
539struct SentPack {
540    written: crate::pack::WriteOutcome,
541    answered_from_bitmap: bool,
542    /// The exact set handed to `emit_pack`. See
543    /// [`UploadSummary::objects_selected`].
544    objects_selected: usize,
545}
546
547impl<'a> UploadPack<'a> {
548    /// Serve `objects`/`refs` with default configuration.
549    pub fn new(git: &'a dyn GitServe, hash_kind: Kind) -> Self {
550        let config = Config::default();
551        UploadPack {
552            haves: HaveMemo::with_capacity(memo_capacity(&config.limits)),
553            git,
554            hash_kind,
555            config,
556            sideband_open: std::sync::atomic::AtomicBool::new(false),
557        }
558    }
559
560    /// Replace the configuration.
561    ///
562    /// The `have` memo is rebuilt, because its ceiling is derived from
563    /// [`Config::limits`] — a configuration that lifted the request ceiling and
564    /// left a memo capped at the old one would be honouring a limit nobody set.
565    pub fn with_config(mut self, config: Config) -> Self {
566        self.haves = HaveMemo::with_capacity(memo_capacity(&config.limits));
567        self.config = config;
568        self
569    }
570
571    /// The repository's hash kind, and the only source of `object-format`.
572    pub fn hash_kind(&self) -> Kind {
573        self.hash_kind
574    }
575
576    /// **One traversal scope for this call.** Its lifetime is what releases the
577    /// [`WalkPool`](crate::WalkPool) reservation and the thread's id charge, so
578    /// it is built per call and dropped where the walk ends — never held on
579    /// `self`, which lives for the whole connection.
580    fn graph(&self) -> Graph<'_> {
581        Graph::new(self.git, self.hash_kind)
582            .with_budget(self.config.budget)
583            .with_walk_limit(self.config.limits.walk_bytes, self.config.walk_pool.clone())
584    }
585
586    /// Serve one connection at the given protocol version.
587    pub fn serve(
588        &self,
589        version: ProtocolVersion,
590        input: &mut dyn BufRead,
591        out: &mut dyn Write,
592    ) -> Result<UploadSummary> {
593        self.serve_session(version, Session::Connected, input, out)
594    }
595
596    /// Serve one *call* at the given protocol version and [`Session`].
597    ///
598    /// [`Session::Connected`] is what [`serve`](Self::serve) does and what SSH
599    /// uses. [`Session::StatelessRpc`] is one smart-HTTP `POST`: no
600    /// advertisement is written (it was a separate `GET`), and the call answers
601    /// one round and returns.
602    pub fn serve_session(
603        &self,
604        version: ProtocolVersion,
605        session: Session,
606        input: &mut dyn BufRead,
607        out: &mut dyn Write,
608    ) -> Result<UploadSummary> {
609        let served = match version {
610            ProtocolVersion::V2 => self.serve_v2_session(session, input, out),
611            ProtocolVersion::V0 => self.serve_v0_session(session, input, out),
612        };
613        // ── the reason goes to the CLIENT, not only to the log ───────────────
614        //
615        // Every refusal this crate makes — an unsupported `filter`, an unknown
616        // v2 command, an `object-format` mismatch, a `want` that is not
617        // reachable — used to reach the transport as an `Err` and stop there.
618        // The transport logs it and closes the channel, so `git` printed
619        //
620        // ```text
621        // fatal: the remote end hung up unexpectedly
622        // ```
623        //
624        // and the operator had to be on the server to learn why. MEASURED on
625        // 2026-08-05, `git -c protocol.version=2 fetch <gunnar> <unknown
626        // oid>`: the client got that line and nothing else, while the server
627        // log carried `err=object 0000…0001 is not in this repository (a want)`
628        // in full. `report_error` was written for exactly this and had **zero**
629        // call sites in the tree, tests included, which is why nothing noticed.
630        //
631        // Here rather than at either transport: `serve_session` is the one
632        // entry point SSH and smart HTTP both come through, so one call covers
633        // both and they cannot drift (one implementation).
634        //
635        // A failure to write the reason is discarded on purpose. The commonest
636        // way to get here at all is a client that has already hung up, and
637        // replacing the real error with "could not tell you about the real
638        // error" would lose the only useful fact in the exchange.
639        if let Err(err) = &served {
640            let sideband = self
641                .sideband_open
642                .load(std::sync::atomic::Ordering::Relaxed);
643            let _ = self.report_error(out, sideband, &err.client_message());
644            let _ = out.flush();
645        }
646        served
647    }
648
649    /// The advertisement a smart-HTTP `GET /info/refs?service=git-upload-pack`
650    /// answers with, for either protocol version.
651    ///
652    /// The `# service=git-upload-pack` preamble is **not** written here: it is
653    /// an HTTP framing detail (and one protocol v2 does not use at all), so it
654    /// belongs to `gunnar-http` and not to a state machine that also runs over
655    /// SSH, where no such line exists.
656    pub fn advertise(&self, version: ProtocolVersion, out: &mut dyn Write) -> Result<()> {
657        match version {
658            ProtocolVersion::V2 => self.write_capability_advertisement(out),
659            ProtocolVersion::V0 => self.write_ref_advertisement(out),
660        }
661    }
662
663    // ── protocol v2 ──────────────────────────────────────────────────────
664
665    /// The v2 capability advertisement, as bytes.
666    ///
667    /// Sent before the client says anything, exactly as `git-upload-pack` does.
668    pub fn capability_advertisement(&self) -> Result<Vec<u8>> {
669        let mut out = Vec::new();
670        self.write_capability_advertisement(&mut out)?;
671        Ok(out)
672    }
673
674    /// Write the v2 capability advertisement.
675    pub fn write_capability_advertisement(&self, out: &mut dyn Write) -> Result<()> {
676        wire_io::text(out, b"version 2")?;
677        // The list is `caps::UPLOAD_PACK_V2`, not five literals here. It used
678        // to be five literals here — a fourth hand-written advertisement, in a
679        // different file from the other three — which is how `server-option`
680        // came to be advertised with nothing in the tree that had to agree
681        // with it. `caps::ADVERTISED` now names every surface and
682        // `tests/advertisement.rs` walks all of them.
683        for line in caps::advertise(
684            caps::UPLOAD_PACK_V2,
685            self.hash_kind(),
686            &self.config.agent,
687            self.config.offered,
688            // No v2 row carries a `requires`: protocol v2 has no
689            // `allow-*-sha1-in-want` capability, because `upload_pack_v2` in
690            // git sets `ALLOW_ANY_SHA1` unconditionally and the question never
691            // reaches the wire. Passed anyway so the surface cannot acquire a
692            // policy-dependent row without this call site being the one that
693            // decides what it renders under.
694            self.config.want_policy,
695        ) {
696            wire_io::text(out, line.as_bytes())?;
697        }
698        wire_io::flush(out)?;
699        Ok(())
700    }
701
702    /// The whole v2 session: advertisement, then commands until the client
703    /// stops.
704    pub fn serve_v2(&self, input: &mut dyn BufRead, out: &mut dyn Write) -> Result<UploadSummary> {
705        self.serve_v2_session(Session::Connected, input, out)
706    }
707
708    /// The v2 session, for either [`Session`].
709    ///
710    /// Under [`Session::StatelessRpc`] the advertisement is skipped and the
711    /// call returns after **one** command, because that is what one HTTP `POST`
712    /// carries. Returning after one is not merely equivalent to hitting EOF: a
713    /// body carrying a second command would otherwise be served on a
714    /// negotiation state the next `POST` cannot see, which is the shape of a
715    /// clone that loops.
716    pub fn serve_v2_session(
717        &self,
718        session: Session,
719        input: &mut dyn BufRead,
720        out: &mut dyn Write,
721    ) -> Result<UploadSummary> {
722        let mut summary = UploadSummary {
723            protocol: ProtocolVersion::V2,
724            ..Default::default()
725        };
726        if session.advertises_inline() {
727            let _s = crate::stage::enter("advertise");
728            self.write_capability_advertisement(out)?;
729            out.flush()?;
730        }
731        loop {
732            let command = {
733                let _s = crate::stage::enter("request_read");
734                self.read_command(input)?
735            };
736            let Some(command) = command else {
737                return Ok(summary);
738            };
739            caps::check_object_format(
740                self.hash_kind(),
741                command.capabilities.value("object-format"),
742            )?;
743            match command.name.as_str() {
744                "ls-refs" => {
745                    let _s = crate::stage::enter("ls_refs");
746                    let request = LsRefsRequest::parse(&command.arguments)?;
747                    self.ls_refs(&request, out)?;
748                    summary.ls_refs += 1;
749                }
750                "fetch" => {
751                    let request = {
752                        let _s = crate::stage::enter("request_parse");
753                        FetchRequest::parse(
754                            &command.arguments,
755                            self.hash_kind(),
756                            self.config.offered,
757                        )?
758                    };
759                    let outcome = self.fetch(&request, out)?;
760                    summary.fetch_rounds += 1;
761                    summary.common = outcome.common.len();
762                    summary.objects_sent += outcome.objects_sent;
763                    summary.objects_selected += outcome.objects_selected;
764                    summary.objects_copied_from_pack += outcome.objects_copied_from_pack;
765                    summary.objects_recompressed += outcome.objects_recompressed;
766                    // `|=` and not `=`: only the round that carries the
767                    // packfile has a selection at all, and the rounds before it
768                    // return `FetchOutcome::default()`, whose `false` would
769                    // otherwise overwrite the one answer this field exists to
770                    // report. Seen red on 2026-08-04 by omitting this line
771                    // entirely: every arm of a five-arm A/B reported
772                    // `answered_from_bitmap: false` including the ones served
773                    // from a bitmap, which is the exact shape of the four
774                    // instruments that spent that day measuring the wrong path.
775                    summary.answered_from_bitmap |= outcome.answered_from_bitmap;
776                    summary.thin_bases_offered += outcome.thin_bases_offered;
777                    summary.thin_bases_vetoed += outcome.thin_bases_vetoed;
778                    summary.closure_bases_declined += outcome.closure_bases_declined;
779                }
780                other => {
781                    return Err(Error::protocol(format!("unknown v2 command {other:?}")));
782                }
783            }
784            {
785                let _s = crate::stage::enter("flush");
786                out.flush()?;
787            }
788            if session.is_stateless() {
789                return Ok(summary);
790            }
791        }
792    }
793
794    /// Read one v2 command — `command=…`, its capability lines, and, after the
795    /// delim-pkt, its argument section.
796    ///
797    /// **One [`Budget`] spans the whole command**, capability lines and
798    /// arguments together, because they land in the same two `Vec`s and a
799    /// ceiling that only covered one half would be walked around by putting the
800    /// bytes in the other. The budget is per *round*: a v2 client re-sends its
801    /// whole `have` list on every `command=fetch`, so what is being bounded is
802    /// what the server holds at once, not what the client says over a session.
803    fn read_command(&self, input: &mut dyn BufRead) -> Result<Option<V2Command>> {
804        let mut budget = Budget::new(self.config.limits.request_bytes);
805        let mut name: Option<String> = None;
806        let mut capability_lines: Vec<String> = Vec::new();
807        let mut arguments: Vec<Vec<u8>> = Vec::new();
808        let saw_delim;
809
810        loop {
811            match read_packet(input)? {
812                // A clean hang-up, or the client's flush-pkt: both mean the
813                // session is over. Over SSH a client that is done simply
814                // closes, so EOF here is normal and must not be an error.
815                None | Some(Packet::Flush) => {
816                    return Ok(None);
817                }
818                Some(Packet::Delim) => {
819                    saw_delim = true;
820                    break;
821                }
822                Some(Packet::ResponseEnd) => {
823                    return Err(Error::protocol("a response-end-pkt in a v2 request"))
824                }
825                Some(Packet::Data(data)) => {
826                    budget.spend(data.len(), "v2 request")?;
827                    let text = String::from_utf8_lossy(data.strip_suffix(b"\n").unwrap_or(&data))
828                        .into_owned();
829                    match text.strip_prefix("command=") {
830                        Some(command) => name = Some(command.to_owned()),
831                        None => capability_lines.push(text),
832                    }
833                }
834            }
835        }
836
837        if saw_delim {
838            arguments = read_section(input, "a v2 argument section", &mut budget)?;
839        }
840
841        let name = name.ok_or_else(|| Error::protocol("a v2 request with no command="))?;
842        Ok(Some(V2Command {
843            name,
844            capabilities: PeerCapabilities::parse(&capability_lines.join(" ")),
845            arguments,
846        }))
847    }
848
849    /// `command=ls-refs`.
850    pub fn ls_refs(&self, request: &LsRefsRequest, out: &mut dyn Write) -> Result<()> {
851        let refs = self.collect_refs(&request.ref_prefixes, request.peel, request.symrefs)?;
852        for entry in refs {
853            let mut line = match entry.target {
854                Some(id) => format!("{id} {}", entry.name),
855                None if request.unborn => format!("unborn {}", entry.name),
856                None => continue,
857            };
858            if let Some(symref) = &entry.symref_target {
859                line.push_str(&format!(" symref-target:{symref}"));
860            }
861            if let Some(peeled) = entry.peeled {
862                line.push_str(&format!(" peeled:{peeled}"));
863            }
864            wire_io::text(out, line.as_bytes())?;
865        }
866        wire_io::flush(out)?;
867        Ok(())
868    }
869
870    /// `command=fetch`, one round.
871    ///
872    /// Returns without a packfile when negotiation is not finished; the client
873    /// then sends another `command=fetch` with more `have`s.
874    pub fn fetch(&self, request: &FetchRequest, out: &mut dyn Write) -> Result<FetchOutcome> {
875        let graph = self.graph();
876        // ONE snapshot of the reference namespace for the whole round: the
877        // `want-ref`s, the shallow computation and `include-tag` all resolve
878        // against the same one, so a reference that moves mid-request cannot
879        // make this fetch promise an oid under a name it no longer has.
880        let refs = RefSnapshot::read(self.git)?;
881        let mut wants = request.wants.clone();
882        let mut wanted_refs: Vec<(ObjectId, String)> = Vec::new();
883        {
884            let _s = crate::stage::enter("want_check");
885            for name in &request.want_refs {
886                gunnar_store::refname::validate(name.as_bytes())?;
887                // `HEAD` is NOT a row, so `resolve` — a row lookup — cannot find
888                // it. It has an accessor pair instead, and `ls-refs` already
889                // advertises it through `head()`. Without this arm every
890                // protocol-v2 `git clone` fails: the advertisement offers
891                // `HEAD symref-target:refs/heads/…`, the client answers
892                // `want-ref HEAD`, and the server says the ref does not exist.
893                //
894                // `refname::validate` admits `HEAD`, so it passes validation and
895                // dies one line later — which is why nothing caught it: the wire
896                // suites drive the state machine directly, and `ext::` negotiates
897                // v0, where `want-ref` never appears. Found by running a real
898                // `git clone` (2026-08-10).
899                let resolved = if name == "HEAD" {
900                    refs.head(self.git)?
901                } else {
902                    refs.resolve(name)?
903                };
904                let Some(id) = resolved else {
905                    return Err(Error::protocol(format!(
906                        "want-ref names {name:?}, which does not exist"
907                    )));
908                };
909                wanted_refs.push((id, name.clone()));
910                wants.push(id);
911            }
912            dedupe_oids(&mut wants);
913            self.check_wants(&wants)?;
914        }
915
916        // `done` means the client has stopped negotiating, so the expensive
917        // reachability question is not asked at all, only which `have`s are
918        // present, which is what the pack needs anyway.
919        let negotiation = {
920            let _s = crate::stage::enter("negotiate");
921            negotiate(
922                &graph,
923                &wants,
924                &request.haves,
925                request.wait_for_done || request.done,
926                &self.haves,
927            )?
928        };
929
930        if !request.done {
931            wire_io::text(out, b"acknowledgments")?;
932            // `ack`, not `common`, in **both** branches, which is git's
933            // `send_acks` line for line: `if (!acks->nr) NAK` and then one
934            // `ACK` per entry. The pack is still computed from the whole of
935            // `common`; what goes on the wire is the subset the client had not
936            // already told the server about, and that is 25 400 bytes on the
937            // divergent fixture. See `Negotiation::ack`.
938            //
939            // A round that finds commons but no *new* ones therefore answers
940            // `NAK`, exactly as git's does — and the section is never empty,
941            // which an `ack.is_empty()` fall-through would have made it.
942            if negotiation.ack.is_empty() {
943                wire_io::text(out, b"NAK")?;
944            } else {
945                for id in &negotiation.ack {
946                    wire_io::text(out, format!("ACK {id}").as_bytes())?;
947                }
948            }
949            if negotiation.ready {
950                wire_io::text(out, b"ready")?;
951                wire_io::delim(out)?;
952            } else {
953                wire_io::flush(out)?;
954                return Ok(FetchOutcome {
955                    common: negotiation.common,
956                    ready: false,
957                    sent_pack: false,
958                    ..Default::default()
959                });
960            }
961        }
962
963        let want_commits = {
964            let _s = crate::stage::enter("peel_wants");
965            self.commit_wants(&graph, &wants)?
966        };
967        let shallow_outcome = {
968            let _s = crate::stage::enter("shallow");
969            shallow::compute(&graph, &refs, &want_commits, &request.shallow)?
970        };
971        if shallow_outcome.has_info() {
972            wire_io::text(out, b"shallow-info")?;
973            for id in &shallow_outcome.newly_shallow {
974                // No trailing newline, matching what `git-upload-pack` 2.53
975                // puts on the wire (captured 2026-08-03).
976                wire_io::data(out, format!("shallow {id}").as_bytes())?;
977            }
978            for id in &shallow_outcome.unshallow {
979                wire_io::data(out, format!("unshallow {id}").as_bytes())?;
980            }
981            wire_io::delim(out)?;
982        }
983
984        if !wanted_refs.is_empty() {
985            wire_io::text(out, b"wanted-refs")?;
986            for (id, name) in &wanted_refs {
987                wire_io::text(out, format!("{id} {name}").as_bytes())?;
988            }
989            wire_io::delim(out)?;
990        }
991
992        wire_io::text(out, b"packfile")?;
993        let written = self.send_pack(
994            &graph,
995            &refs,
996            &wants,
997            &negotiation.common,
998            request,
999            &shallow_outcome,
1000            out,
1001            true,
1002        )?;
1003        {
1004            let _s = crate::stage::enter("flush");
1005            wire_io::flush(out)?;
1006        }
1007
1008        Ok(FetchOutcome {
1009            common: negotiation.common,
1010            ready: negotiation.ready,
1011            sent_pack: true,
1012            objects_sent: written.written.num_objects,
1013            objects_selected: written.objects_selected,
1014            objects_copied_from_pack: written.written.objects_copied_from_pack,
1015            objects_recompressed: written.written.objects_recompressed,
1016            answered_from_bitmap: written.answered_from_bitmap,
1017            thin_bases_offered: written.written.thin_bases_offered,
1018            thin_bases_vetoed: written.written.thin_bases_vetoed,
1019            closure_bases_declined: written.written.closure_bases_declined,
1020        })
1021    }
1022
1023    // ── protocol v0 ──────────────────────────────────────────────────────
1024
1025    /// The v0 reference advertisement.
1026    pub fn write_ref_advertisement(&self, out: &mut dyn Write) -> Result<()> {
1027        let capabilities = caps::upload_pack_v0_capabilities(
1028            self.hash_kind(),
1029            &self.config.agent,
1030            self.config.offered,
1031            self.config.want_policy,
1032        );
1033        let refs_snapshot = RefSnapshot::read(self.git)?;
1034        let head = self.head_ref(&refs_snapshot)?;
1035        let mut extra = capabilities.clone();
1036        if let Some(symref) = head.as_ref().and_then(|h| h.symref_target.as_ref()) {
1037            extra.push(format!("symref=HEAD:{symref}"));
1038        }
1039
1040        let refs = self.advertise_from(&refs_snapshot, &[], true, true)?;
1041        let mut first = true;
1042        for entry in refs {
1043            let Some(target) = entry.target else {
1044                continue;
1045            };
1046            let mut line = format!("{target} {}", entry.name).into_bytes();
1047            if first {
1048                line.push(0);
1049                line.extend_from_slice(extra.join(" ").as_bytes());
1050                first = false;
1051            }
1052            wire_io::text(out, &line)?;
1053            if let Some(peeled) = entry.peeled {
1054                wire_io::text(out, format!("{peeled} {}^{{}}", entry.name).as_bytes())?;
1055            }
1056        }
1057        if first {
1058            // An empty repository still has to say who it is, or the client
1059            // cannot learn the hash algorithm and reports nothing useful.
1060            let mut line = format!("{} capabilities^{{}}", self.hash_kind().null()).into_bytes();
1061            line.push(0);
1062            line.extend_from_slice(extra.join(" ").as_bytes());
1063            wire_io::text(out, &line)?;
1064        }
1065        wire_io::flush(out)?;
1066        Ok(())
1067    }
1068
1069    /// The whole v0 session.
1070    pub fn serve_v0(&self, input: &mut dyn BufRead, out: &mut dyn Write) -> Result<UploadSummary> {
1071        self.serve_v0_session(Session::Connected, input, out)
1072    }
1073
1074    /// The v0 session, for either [`Session`].
1075    ///
1076    /// # What stateless-rpc changes here, and it is one branch
1077    ///
1078    /// v0 has no `command=` boundary, so a stateless round is delimited by the
1079    /// client's flush-pkt after its `have` list. Under
1080    /// [`Session::StatelessRpc`] that flush ends the **call**: the server
1081    /// answers `ACK`/`NAK` and returns, and the client opens a new `POST`
1082    /// carrying its whole `want` list again plus the `have`s it has since
1083    /// decided to send. Under [`Session::Connected`] the same flush ends only a
1084    /// *round* and the loop keeps reading, which is what SSH needs.
1085    ///
1086    /// Getting this wrong is not a subtle bug: a server that kept reading would
1087    /// block on a body that has already ended, and the client would sit waiting
1088    /// for a response that is waiting for it.
1089    pub fn serve_v0_session(
1090        &self,
1091        session: Session,
1092        input: &mut dyn BufRead,
1093        out: &mut dyn Write,
1094    ) -> Result<UploadSummary> {
1095        let mut summary = UploadSummary {
1096            protocol: ProtocolVersion::V0,
1097            // The v0 advertisement IS the `ls-refs`, so it is counted where it
1098            // is written and not from a constant: a stateless `POST` writes no
1099            // advertisement, and a summary claiming one would make the metric
1100            // say a ref list went out when none did.
1101            ls_refs: usize::from(session.advertises_inline()),
1102            ..Default::default()
1103        };
1104        if session.advertises_inline() {
1105            self.write_ref_advertisement(out)?;
1106            out.flush()?;
1107        }
1108
1109        // **One budget for the whole v0 session**, not one per section. v0 has
1110        // no `command=` boundary: the `want` list and every subsequent round of
1111        // `have`s accumulate into the *same* two vectors, held until the pack is
1112        // written, so a per-section ceiling would be spent afresh on every round
1113        // and bound nothing at all. This is the shape a hostile client reaches
1114        // for — open a connection, send `have <oid>` forever, never send `done`
1115        // — and it costs the server memory before any repository is touched.
1116        let mut budget = Budget::new(self.config.limits.request_bytes);
1117        let mut request = FetchRequest::default();
1118        let mut peer = PeerCapabilities::default();
1119        let mut first = true;
1120        loop {
1121            let packet = match read_packet(input)? {
1122                // `git ls-remote` over v0 takes the advertisement and hangs
1123                // up. That is a completed request, not a truncated one.
1124                None => return Ok(summary),
1125                Some(packet) => packet,
1126            };
1127            match packet {
1128                Packet::Flush => break,
1129                Packet::Data(data) => {
1130                    budget.spend(data.len(), "v0 want section")?;
1131                    let mut text: &[u8] = data.strip_suffix(b"\n").unwrap_or(&data);
1132                    if first {
1133                        // v0 upload-pack puts the client's capabilities on the
1134                        // first `want` line, **space-separated after the oid**,
1135                        // not after a NUL, which is receive-pack's form.
1136                        // Reading it as a NUL split leaves the capability
1137                        // string glued to the object id, and the request dies
1138                        // with "a hash sized 133 hexadecimal characters".
1139                        if let Some((head, caps)) = split_v0_capabilities(text) {
1140                            peer = PeerCapabilities::parse(&String::from_utf8_lossy(caps));
1141                            text = head;
1142                        }
1143                        first = false;
1144                    }
1145                    self.parse_v0_want_line(text, &mut request)?;
1146                }
1147                other => {
1148                    return Err(Error::protocol(format!(
1149                        "unexpected {other:?} in a v0 want section"
1150                    )))
1151                }
1152            }
1153        }
1154
1155        caps::check_object_format(self.hash_kind(), peer.value("object-format"))?;
1156        if request.wants.is_empty() {
1157            return Ok(summary);
1158        }
1159        dedupe_oids(&mut request.wants);
1160        self.check_wants(&request.wants)?;
1161
1162        // `side-band-64k` and NOT git's older plain `side-band`, which gunnar
1163        // does not advertise. Reading a capability that is not on offer is the
1164        // inverse of the drift `caps::Honoured` catches, and here it was not
1165        // merely dead: the two forms differ in the maximum payload a band may
1166        // carry — 65515 against 1000 — and `wire_io::sideband` frames at the
1167        // 64k size unconditionally. So a peer that guessed `side-band` and got
1168        // it honoured would have been handed packets it must reject. Advertise
1169        // what you keep, keep what you advertise, read nothing else;
1170        // `tests/advertisement.rs::every_capability_the_server_reads_is_one_it_advertises`
1171        // is what says so.
1172        let sideband = peer.has("side-band-64k");
1173        request.no_progress = request.no_progress || peer.has("no-progress");
1174        request.include_tag = request.include_tag || peer.has("include-tag");
1175        // v0 carries this as a capability rather than as a request line, and it
1176        // decides whether the pack may contain `ofs-delta` entries at all, so
1177        // whether pack-copy is usable on this connection. Reading it
1178        // here rather than in `parse_v0_want_line` is where the other two
1179        // capability-shaped flags are read.
1180        request.ofs_delta = request.ofs_delta || peer.has("ofs-delta");
1181        // The third capability-shaped flag, and it was missing.
1182        //
1183        // v0 advertises `thin-pack` (`caps::upload_pack_v0_capabilities`) and
1184        // `FetchRequest::thin_pack` was only ever set from a **v2** `thin-pack`
1185        // argument, so the `thin` gate in `send_pack` could not be true on a v0
1186        // connection: every v0 fetch was served as a whole pack against an
1187        // advertisement that promised otherwise.
1188        //
1189        // MEASURED 2026-08-05, `fx/knut` from the production-shaped corpus,
1190        // a client cut 40 commits back, the same 431 objects served on both
1191        // arms, stock git 2.53.0 as the client:
1192        //
1193        // | protocol | bytes_out |
1194        // |---|---|
1195        // | v0, before | 942 987 |
1196        // | v2 | 702 138 |
1197        //
1198        // 1.343x, 240 849 bytes, for no reason but this line. v2 has been git's
1199        // default since 2.26 so this is the compatibility path rather than the
1200        // common one, which is why it went four months unnoticed — and is also
1201        // why nothing else was going to notice it.
1202        request.thin_pack = request.thin_pack || peer.has("thin-pack");
1203        // The fourth (GHUNT-1, 2026-10-28): v0 carries `deepen-relative` as a CAPABILITY on
1204        // the first `want` line, never as a request line (stock git dies on one: "expected
1205        // to get object ID, not 'deepen-relative'"). Read only from the line parser, it was
1206        // never set on a v0 connection, so `git fetch --deepen=N` over v0 counted N from the
1207        // tip and handed the client its own boundary back: `--deepen 5` from a depth-1 clone
1208        // left 5 commits where stock git leaves 6.
1209        request.shallow.relative = request.shallow.relative || peer.has("deepen-relative");
1210
1211        let graph = self.graph();
1212        let want_commits = self.commit_wants(&graph, &request.wants)?;
1213        let refs = RefSnapshot::read(self.git)?;
1214        let shallow_outcome = shallow::compute(&graph, &refs, &want_commits, &request.shallow)?;
1215        if request.shallow.is_deepening() {
1216            for id in &shallow_outcome.newly_shallow {
1217                wire_io::text(out, format!("shallow {id}").as_bytes())?;
1218            }
1219            for id in &shallow_outcome.unshallow {
1220                wire_io::text(out, format!("unshallow {id}").as_bytes())?;
1221            }
1222            wire_io::flush(out)?;
1223            out.flush()?;
1224        }
1225
1226        // ── negotiation: two ACK modes, chosen by the client ────────────────
1227        //
1228        // Which one is in play is decided by what
1229        // [`caps::UPLOAD_PACK_V0`] advertised, and that table's comment carries
1230        // the measurement that settled it. In short:
1231        //
1232        // **`multi_ack_detailed`** is what any git since 1.6.6 picks, and the
1233        // only mode in which a multi-round negotiation over **stateless-rpc**
1234        // has a correct answer at all. A round answers `ACK <oid> common` per
1235        // newly-common `have`, `ACK <oid> ready` once the wants are reachable,
1236        // then `NAK` — git writes that `NAK` at every multi-ack round boundary
1237        // whether or not it is ready. With **`no-done`** the server then writes
1238        // the terminal `ACK` and the packfile into the same response instead of
1239        // waiting for the client to say `done`, which over HTTP saves a whole
1240        // request.
1241        //
1242        // **Single-ACK** is the fallback, for a client that offered neither.
1243        // It is `b9fee55`'s shape and not a reversion of it: one `ACK <oid>` at
1244        // the first recognised `have`, nothing at any later flush, and nothing
1245        // after `done`. That is git's `have_obj.nr` rule, and it is correct on
1246        // the connected sessions such a client can actually reach.
1247        let mut haves: Vec<ObjectId> = Vec::new();
1248        let multi_ack_detailed = peer.has("multi_ack_detailed");
1249        let no_done = multi_ack_detailed && peer.has("no-done");
1250        // In single-ACK this is git's `have_obj.nr > 0` — "an ACK has gone out,
1251        // say nothing more". In multi-ack it is git's `last_hex`, the LAST
1252        // `have` found common, which is what the terminal `ACK` names.
1253        let mut acked: Option<ObjectId> = None;
1254        // git's `sent_ready`: `ACK <oid> ready` is announced once per session.
1255        // Over stateless-rpc the session is one `POST`, so the latch resets
1256        // with it and every request that reaches readiness says so — which is
1257        // also what stock does, its upload-pack being a fresh process per
1258        // request.
1259        let mut ready_sent = false;
1260        // Set when `no-done` let this session write its terminal `ACK` inside
1261        // the round, so the block after the loop does not write a second one.
1262        let mut terminal_written = false;
1263        let mut done = false;
1264        // How many of `haves` a flush has already judged; the rest arrived in the block `done`
1265        // closes (GHUNT-2).
1266        let mut judged = 0usize;
1267        while !done {
1268            match read_packet(input)? {
1269                None => return Ok(summary),
1270                Some(Packet::Flush) => {
1271                    summary.fetch_rounds += 1;
1272                    judged = haves.len();
1273                    // ── one line per block, and NOTHING once an ACK has gone
1274                    //    out ────────────────────────────────────────────────
1275                    //
1276                    // The `_ => NAK` this replaces answered every later block
1277                    // with a `NAK` **after** the ACK had already ended the
1278                    // negotiation, and git's client stops reading the moment it
1279                    // sees a plain ACK. The surplus `NAK` therefore sat in the
1280                    // pipe until the client switched to sideband and read `N`
1281                    // as a band number:
1282                    //
1283                    // ```text
1284                    // fetch-pack: protocol error: bad band #78
1285                    // ```
1286                    //
1287                    // MEASURED 2026-08-05, `fx/knut`, client cut 40
1288                    // commits back: three blocks of `have`s, so gunnar wrote
1289                    // NAK / ACK / NAK and the third line corrupted the stream.
1290                    // `fx/funnel-core` at five commits back sends one block and
1291                    // was clean, which is why the small fixture in the interop
1292                    // suite never showed it — the trigger is a client with
1293                    // enough history to send a SECOND block.
1294                    //
1295                    // git's rule, `upload-pack.c:get_common_commits`, is on
1296                    // `have_obj.nr` and not on the round: with `multi_ack` off
1297                    // it writes `NAK` at a flush only while it has recognised
1298                    // **no** `have` at all, and the single `ACK` goes out inline
1299                    // at the first one it does recognise. `acked` is gunnar's
1300                    // `have_obj.nr > 0`.
1301                    if multi_ack_detailed {
1302                        // The SAME `negotiate` the v2 `command=fetch` path
1303                        // runs, over the same per-connection `HaveMemo`, so
1304                        // `ready` is decided once for both protocol versions
1305                        // and `Negotiation::ack`'s suppression of redundant
1306                        // parent-chain ACKs applies here too (one implementation).
1307                        let negotiation = negotiate(
1308                            &graph,
1309                            &request.wants,
1310                            &haves,
1311                            /* wait_for_done */ false,
1312                            &self.haves,
1313                        )?;
1314                        for id in &negotiation.ack {
1315                            wire_io::text(out, format!("ACK {id} common").as_bytes())?;
1316                        }
1317                        if let Some(id) = negotiation.common.last() {
1318                            acked = Some(*id);
1319                        }
1320                        let ready = negotiation.ready && acked.is_some();
1321                        if let (true, false, Some(id)) = (ready, ready_sent, acked) {
1322                            wire_io::text(out, format!("ACK {id} ready").as_bytes())?;
1323                            ready_sent = true;
1324                        }
1325                        // Unconditional, and NOT the surplus `NAK` the comment
1326                        // above warns about: that one followed a *terminal*
1327                        // single-ACK, after which git reads nothing. In
1328                        // multi-ack the client keeps reading until `done`, and
1329                        // git writes this `NAK` at every round boundary
1330                        // (`have_obj.nr == 0 || multi_ack`). A client that does
1331                        // not get one waits for a round that has ended.
1332                        wire_io::text(out, b"NAK")?;
1333                        out.flush()?;
1334                        if no_done && ready {
1335                            let id = acked.expect("`ready` is gated on a common `have`");
1336                            wire_io::text(out, format!("ACK {id}").as_bytes())?;
1337                            out.flush()?;
1338                            terminal_written = true;
1339                            break;
1340                        }
1341                    } else {
1342                        match (acked, self.first_common(&haves)) {
1343                            (Some(_), _) => {}
1344                            (None, Some(id)) => {
1345                                acked = Some(id);
1346                                wire_io::text(out, format!("ACK {id}").as_bytes())?;
1347                                out.flush()?;
1348                            }
1349                            (None, None) => {
1350                                wire_io::text(out, b"NAK")?;
1351                                out.flush()?;
1352                            }
1353                        }
1354                    }
1355                    if session.is_stateless() {
1356                        // The round IS the request. See the doc comment: the
1357                        // client's next `have` list arrives on a new
1358                        // connection, carrying its `want`s again.
1359                        summary.common = haves
1360                            .iter()
1361                            .filter(|id| has_for_negotiation(self.git, id))
1362                            .count();
1363                        return Ok(summary);
1364                    }
1365                }
1366                Some(Packet::Data(data)) => {
1367                    budget.spend(data.len(), "v0 negotiation")?;
1368                    let text = data.strip_suffix(b"\n").unwrap_or(&data);
1369                    if text == b"done" {
1370                        done = true;
1371                    } else if let Some(rest) = text.strip_prefix(b"have ") {
1372                        haves.push(parse_oid(&String::from_utf8_lossy(rest), self.hash_kind())?);
1373                    }
1374                }
1375                Some(other) => {
1376                    return Err(Error::protocol(format!(
1377                        "unexpected {other:?} during v0 negotiation"
1378                    )))
1379                }
1380            }
1381        }
1382
1383        // ── after `done`, a single-ACK server that has already ACKed says
1384        //    NOTHING ─────────────────────────────────────────────────────────
1385        //
1386        // This used to be `acked.or_else(first_common)` and wrote an `ACK`
1387        // either way, so a negotiation that found a common commit sent the
1388        // **same ACK twice** — once from the flush arm above, once here. The
1389        // second one lands after the client has switched its reader into
1390        // sideband mode, which demultiplexes `A` as a band number:
1391        //
1392        // ```text
1393        // fetch-pack: protocol error: bad band #65
1394        // fatal: protocol error: bad pack header
1395        // ```
1396        //
1397        // MEASURED 2026-08-05 against stock git 2.53.0 over real SSH,
1398        // `fx/funnel-core` from the production-shaped corpus, client cut five
1399        // commits back:
1400        //
1401        // | fetch | v0 | v2 |
1402        // |---|---|---|
1403        // | full clone | 28 objects, OK | OK |
1404        // | **incremental** | **`bad band #65`, 0 objects** | 28 objects, OK |
1405        //
1406        // So **every** v0 fetch that shares history with the server was
1407        // corrupt, and only a clone — which sends no `have` and gets no ACK —
1408        // came through. `GIT_TRACE_PACKET` shows the duplicate plainly:
1409        // `fetch< ACK 36d2cb41…` followed by `sideband< ACK 36d2cb41…`.
1410        //
1411        // git's own rule, `upload-pack.c:get_common_commits`: with `multi_ack`
1412        // off, `done` returns without writing when any `have` was recognised,
1413        // and writes `NAK` only when none was. Its client is the other half of
1414        // the same contract — `fetch-pack.c:find_common` zeroes `flushes` on a
1415        // plain `ACK`, so `while (flushes || multi_ack)` after `done` reads
1416        // **no** lines at all and the next byte it expects is the pack.
1417        //
1418        // The `first_common` fallback is gone with it: it could only fire when
1419        // the flush arm had already run and decided otherwise, and reaching a
1420        // different verdict about the same `haves` two lines apart is the shape
1421        // of the bug rather than a safety net.
1422        //
1423        // In **multi-ack** the rule is the other one git writes at `done`: an
1424        // `ACK <last_hex>` when anything was common, `NAK` when nothing was.
1425        // The asymmetry is not an inconsistency — in single-ACK the client
1426        // stopped reading at the earlier terminal `ACK`, and in multi-ack it is
1427        // still reading and this line is the terminal one. `terminal_written`
1428        // is the `no-done` case, where it has already gone out with the pack.
1429        // ── the `have`s of the block `done` closes (GHUNT-2, 2026-10-28) ───────────
1430        //
1431        // git counts every recognised `have` as it ARRIVES (`get_common_commits`:
1432        // `have_obj.nr`, `last_hex`), flush or no flush; this loop judges them only at a
1433        // flush. A fetch with fewer `have`s than git's first flush (16) sends them and
1434        // `done` in one block, so they were never judged: the server answered `NAK`, and
1435        // stock git printed `warning: no common commits` on every small v0 fetch (the pack
1436        // itself was right, `common` below reads every have). Judged here, git's way: in
1437        // multi-ack the terminal `ACK` names the LAST common one; in single-ACK the first
1438        // common one is the `ACK` git would have written inline at its `have`.
1439        if !terminal_written && judged < haves.len() {
1440            let tail = &haves[judged..];
1441            if multi_ack_detailed {
1442                if let Some(id) = tail
1443                    .iter()
1444                    .rev()
1445                    .find(|id| has_for_negotiation(self.git, id))
1446                {
1447                    acked = Some(*id);
1448                }
1449            } else if acked.is_none() {
1450                if let Some(id) = self.first_common(tail) {
1451                    wire_io::text(out, format!("ACK {id}").as_bytes())?;
1452                    acked = Some(id);
1453                }
1454            }
1455        }
1456        if !terminal_written {
1457            if multi_ack_detailed {
1458                match acked {
1459                    Some(id) => wire_io::text(out, format!("ACK {id}").as_bytes())?,
1460                    None => wire_io::text(out, b"NAK")?,
1461                }
1462            } else if acked.is_none() {
1463                wire_io::text(out, b"NAK")?;
1464            }
1465            out.flush()?;
1466        }
1467
1468        request.haves = haves;
1469        let common: Vec<ObjectId> = request
1470            .haves
1471            .iter()
1472            .filter(|id| graph.contains_have(id))
1473            .copied()
1474            .collect();
1475        summary.fetch_rounds += 1;
1476        summary.common = common.len();
1477        let written = self.send_pack(
1478            &graph,
1479            &refs,
1480            &request.wants,
1481            &common,
1482            &request,
1483            &shallow_outcome,
1484            out,
1485            sideband,
1486        )?;
1487        summary.objects_sent = written.written.num_objects;
1488        summary.objects_selected = written.objects_selected;
1489        summary.objects_copied_from_pack = written.written.objects_copied_from_pack;
1490        summary.objects_recompressed = written.written.objects_recompressed;
1491        summary.answered_from_bitmap = written.answered_from_bitmap;
1492        summary.thin_bases_offered = written.written.thin_bases_offered;
1493        summary.thin_bases_vetoed = written.written.thin_bases_vetoed;
1494        summary.closure_bases_declined = written.written.closure_bases_declined;
1495        if sideband {
1496            // A sidebanded v0 response ends with an **unframed** flush-pkt.
1497            // Without it the client sits waiting for another band-1 line and
1498            // reports "unexpected disconnect while reading sideband packet"
1499            // when the pipe closes, a message that points at the transport
1500            // and not at the missing four bytes. (Found by the real-git
1501            // interop suite, 2026-08-03; the raw, non-sidebanded form needs no
1502            // terminator because the pack's own trailer ends it.)
1503            wire_io::flush(out)?;
1504        }
1505        out.flush()?;
1506        Ok(summary)
1507    }
1508
1509    fn parse_v0_want_line(&self, text: &[u8], request: &mut FetchRequest) -> Result<()> {
1510        let line = String::from_utf8_lossy(text).into_owned();
1511        let (name, value) = split_argument(&line);
1512        let missing = || Error::protocol(format!("v0 line {line:?} is missing its value"));
1513        match name {
1514            "want" => request
1515                .wants
1516                .push(parse_oid(value.ok_or_else(missing)?, self.hash_kind())?),
1517            "shallow" => request
1518                .shallow
1519                .client_shallow
1520                .push(parse_oid(value.ok_or_else(missing)?, self.hash_kind())?),
1521            "deepen" => {
1522                request.shallow.depth = Some(
1523                    value
1524                        .ok_or_else(missing)?
1525                        .parse::<usize>()
1526                        .map_err(|_| Error::protocol(format!("deepen wants a number: {line:?}")))?,
1527                )
1528            }
1529            "deepen-since" => {
1530                request.shallow.since =
1531                    Some(value.ok_or_else(missing)?.parse::<i64>().map_err(|_| {
1532                        Error::protocol(format!("deepen-since wants unix seconds: {line:?}"))
1533                    })?)
1534            }
1535            "deepen-not" => request
1536                .shallow
1537                .not
1538                .push(value.ok_or_else(missing)?.to_owned()),
1539            "deepen-relative" => request.shallow.relative = true,
1540            "filter" => {
1541                if !self.config.offered.filter {
1542                    return Err(Error::UnsupportedFilter {
1543                        spec: value.unwrap_or_default().to_owned(),
1544                        reason: "this server does not offer the filter capability",
1545                    });
1546                }
1547                request.filter = Some(Filter::parse(value.ok_or_else(missing)?)?)
1548            }
1549            "" => {}
1550            other => {
1551                return Err(Error::protocol(format!(
1552                    "unknown v0 request line {other:?}"
1553                )))
1554            }
1555        }
1556        Ok(())
1557    }
1558
1559    fn first_common(&self, haves: &[ObjectId]) -> Option<ObjectId> {
1560        haves
1561            .iter()
1562            .find(|id| has_for_negotiation(self.git, id))
1563            .copied()
1564    }
1565
1566    // ── shared ───────────────────────────────────────────────────────────
1567
1568    /// Report a fatal condition to the client in whichever way this point of
1569    /// the protocol allows: an `ERR` line before a pack has started, band 3
1570    /// once the sideband is open.
1571    pub fn report_error(&self, out: &mut dyn Write, sideband: bool, message: &str) -> Result<()> {
1572        if sideband {
1573            crate::wire_io::sideband_error(out, message)
1574        } else {
1575            err_packet(out, message)
1576        }
1577    }
1578
1579    /// `HEAD`, which is **not a row in `refs()`**.
1580    ///
1581    /// `locked_on_target.md`'s `HEAD`-is-not-a-row question, settled from the gix arm's own words:
1582    /// `all()` walks `refs/` and *"deliberately excludes the pseudo-refs such
1583    /// as `HEAD`"*, which is the contract every backend honours. So it has its
1584    /// own accessor pair on `GitServe` and this asks the store, not the
1585    /// snapshot. Its target, though, is resolved **against the snapshot** — the
1586    /// same one the advertisement is built from — so `HEAD` cannot be
1587    /// advertised pointing at an oid no other line in the same advertisement
1588    /// agrees with.
1589    fn head_ref(&self, refs: &RefSnapshot) -> Result<Option<AdvertisedRef>> {
1590        let Some(row) = self.git.head().map_err(crate::graph::backend)? else {
1591            return Ok(None);
1592        };
1593        let symref_target = row.symref_target.clone();
1594        let resolved = match &row.oid {
1595            Some(bytes) => Some(crate::graph::oid(bytes)?),
1596            None => match &symref_target {
1597                Some(name) => refs.resolve(name)?,
1598                None => None,
1599            },
1600        };
1601        Ok(Some(AdvertisedRef {
1602            name: gunnar_store::HEAD.to_owned(),
1603            target: resolved,
1604            peeled: None,
1605            symref_target,
1606        }))
1607    }
1608
1609    fn collect_refs(
1610        &self,
1611        prefixes: &[String],
1612        peel: bool,
1613        symrefs: bool,
1614    ) -> Result<Vec<AdvertisedRef>> {
1615        let refs = RefSnapshot::read(self.git)?;
1616        self.advertise_from(&refs, prefixes, peel, symrefs)
1617    }
1618
1619    /// [`UploadPack::collect_refs`] against a snapshot the caller already took.
1620    fn advertise_from(
1621        &self,
1622        refs: &RefSnapshot,
1623        prefixes: &[String],
1624        peel: bool,
1625        symrefs: bool,
1626    ) -> Result<Vec<AdvertisedRef>> {
1627        let graph = self.graph();
1628        let mut out = Vec::new();
1629
1630        // HEAD first, exactly as `git-upload-pack` orders it.
1631        if prefixes.is_empty() || prefixes.iter().any(|p| "HEAD".starts_with(p.as_str())) {
1632            if let Some(mut head) = self.head_ref(refs)? {
1633                if !symrefs {
1634                    head.symref_target = None;
1635                }
1636                out.push(head);
1637            }
1638        }
1639
1640        for row in refs.rows() {
1641            if !prefixes.is_empty() && !prefixes.iter().any(|p| row.name.starts_with(p.as_str())) {
1642                continue;
1643            }
1644            let resolved = refs.resolve_row(row)?;
1645            // The store's own peel column when it has one, and only then the
1646            // walk. `RefRow::peeled` is *"for an annotated tag, the commit it
1647            // peels to"*; `None` means the backend does not say, never that
1648            // the tag peels to nothing, so the fallback is a real peel and not
1649            // an unpeeled advertisement.
1650            let peeled = match (peel, resolved) {
1651                (true, Some(id)) => match RefSnapshot::peeled(row)? {
1652                    Some(known) => Some(known),
1653                    None if graph.kind(&id)? == Some(object::Kind::Tag) => {
1654                        Some(graph.peel(&id)?.id)
1655                    }
1656                    None => None,
1657                },
1658                _ => None,
1659            };
1660            out.push(AdvertisedRef {
1661                name: row.name.clone(),
1662                target: resolved,
1663                peeled,
1664                symref_target: symrefs.then(|| row.symref_target.clone()).flatten(),
1665            });
1666        }
1667        Ok(out)
1668    }
1669
1670    /// Every object id a client is allowed to `want`.
1671    fn advertised_oids(&self) -> Result<HashSet<ObjectId>> {
1672        let mut out = HashSet::new();
1673        for entry in self.collect_refs(&[], true, false)? {
1674            if let Some(id) = entry.target {
1675                out.insert(id);
1676            }
1677            if let Some(id) = entry.peeled {
1678                out.insert(id);
1679            }
1680        }
1681        Ok(out)
1682    }
1683
1684    /// **Under a policy, "there is no such object" and "there is, and you may
1685    /// not have it" are ONE answer.**
1686    ///
1687    /// They were two. `wants_exist` ran first and answered `ObjectMissing`, and
1688    /// only a want that DID exist went on to be measured against the
1689    /// advertisement and answered `WantNotAdvertised`. The difference is an
1690    /// oracle, and the thing it answers is precisely what the policy exists to
1691    /// hide: `WantNotAdvertised`'s own doc says serving arbitrary ids "leaks
1692    /// objects that are not reachable from any reference, which is how
1693    /// 'deleted' branches stay fetchable" — and telling a caller that an oid IS
1694    /// here, one `want` at a time, leaks the same fact one bit at a time. An
1695    /// oid from a force-pushed branch, a deleted branch, or somebody else's
1696    /// paste can be tested against a public repository for free.
1697    ///
1698    /// This is the same merge `gunnar-ssh` makes between "no such repository"
1699    /// and "you have no grant": one string on the wire, the difference kept in
1700    /// the server's log.
1701    ///
1702    /// `wants_exist` is not called at all under a policy now, rather than
1703    /// called and its answer laundered. Existence is implied by the check that
1704    /// remains: an advertised oid is a reference target, and a reachable one
1705    /// was just walked to. A want that passes is servable by construction, and
1706    /// a reference pointing at an object the store does not hold is repository
1707    /// corruption that the pack build reports as the server's problem — which
1708    /// is what it is.
1709    ///
1710    /// [`WantPolicy::Any`] keeps the plain existence check, and it is not an
1711    /// oracle there: under `Any` everything that exists is servable, so
1712    /// "missing" is the whole truth and there is nothing to hide behind it.
1713    fn check_wants(&self, wants: &[ObjectId]) -> Result<()> {
1714        if wants.is_empty() || self.config.want_policy == WantPolicy::Any {
1715            return self.wants_exist(wants);
1716        }
1717        let advertised = self.advertised_oids()?;
1718        // The reachable closure is built at most once, and only if some want
1719        // was not itself advertised.
1720        let mut reachable: Option<HashSet<ObjectId>> = None;
1721        for want in wants {
1722            if advertised.contains(want) {
1723                continue;
1724            }
1725            if self.config.want_policy == WantPolicy::AdvertisedTips {
1726                return Err(Error::WantNotAdvertised { oid: *want });
1727            }
1728            let closure = match &reachable {
1729                Some(closure) => closure,
1730                None => reachable.insert(self.reachable_from_refs()?),
1731            };
1732            if !closure.contains(want) {
1733                return Err(Error::WantNotAdvertised { oid: *want });
1734            }
1735        }
1736        Ok(())
1737    }
1738
1739    fn wants_exist(&self, wants: &[ObjectId]) -> Result<()> {
1740        for want in wants {
1741            if !self
1742                .git
1743                .has(want.as_bytes())
1744                .map_err(crate::graph::backend)?
1745            {
1746                return Err(Error::missing(*want, "a want"));
1747            }
1748        }
1749        Ok(())
1750    }
1751
1752    /// Every object reachable from any reference.
1753    ///
1754    /// One repository-sized set, and only one. This used to be
1755    /// `select_objects(…).objects.into_iter().collect()`, which holds a `Vec` of
1756    /// every id in the repository *while* the `HashSet` built out of it fills —
1757    /// ~820 MB of peak at ten million sha256 ids where ~500 MB is needed. See
1758    /// [`reachable_closure`].
1759    fn reachable_from_refs(&self) -> Result<HashSet<ObjectId>> {
1760        let tips: Vec<ObjectId> = self
1761            .collect_refs(&[], false, false)?
1762            .into_iter()
1763            .filter_map(|entry| entry.target)
1764            .collect();
1765        let graph = self.graph();
1766        Ok(reachable_closure(&graph, &tips)?.into_inner())
1767    }
1768
1769    /// The wants that peel to commits, deduplicated, in the order the client
1770    /// named them.
1771    ///
1772    /// # Both sets are here because the input is the client's
1773    ///
1774    /// `wants` is whatever the peer sent and is bounded only by what the
1775    /// request reader is willing to accept, so every per-want cost in here is a
1776    /// cost the peer chooses. Two of them used to be superlinear:
1777    ///
1778    /// * `asked` stops a repeated `want` being **peeled twice**. `peel` is an
1779    ///   object read, so a client naming the same tag ten thousand times used
1780    ///   to buy ten thousand store reads for one answer. This is the half a
1781    ///   counting store can see, and `tests/want_cost.rs` holds it.
1782    /// * `seen` replaces a `Vec::contains` over the answer so far, which was a
1783    ///   linear scan run once per want, so `O(wants²)` `ObjectId` comparisons.
1784    ///   Ten thousand distinct wants — an ordinary mirror clone — was fifty
1785    ///   million comparisons.
1786    ///
1787    /// The second half has **no deterministic instrument**, and that is worth
1788    /// stating rather than papering over: both spellings return identical
1789    /// output and touch the store identically, so the only difference is time,
1790    /// and this crate's cost tests deliberately count touches rather than
1791    /// timing anything (see `tests/connectivity_cost.rs`). It is fixed here and
1792    /// guarded only by the half above.
1793    fn commit_wants(&self, graph: &Graph<'_>, wants: &[ObjectId]) -> Result<Vec<ObjectId>> {
1794        let mut asked: HashSet<ObjectId> = HashSet::new();
1795        let mut seen: HashSet<ObjectId> = HashSet::new();
1796        let mut out = Vec::new();
1797        for want in wants {
1798            if !asked.insert(*want) {
1799                continue;
1800            }
1801            let peeled = graph.peel(want)?;
1802            if peeled.kind == object::Kind::Commit && seen.insert(peeled.id) {
1803                out.push(peeled.id);
1804            }
1805        }
1806        Ok(out)
1807    }
1808
1809    #[allow(clippy::too_many_arguments)]
1810    fn send_pack(
1811        &self,
1812        graph: &Graph<'_>,
1813        refs: &RefSnapshot,
1814        wants: &[ObjectId],
1815        common: &[ObjectId],
1816        request: &FetchRequest,
1817        shallow_outcome: &ShallowOutcome,
1818        out: &mut dyn Write,
1819        sideband: bool,
1820    ) -> Result<SentPack> {
1821        // The one place either protocol version opens a sideband, and therefore
1822        // the one place that has to record it. Past this line a failure's reason
1823        // rides band 3; before it, an `ERR` pkt-line. See
1824        // [`UploadPack::sideband_open`].
1825        self.sideband_open
1826            .store(sideband, std::sync::atomic::Ordering::Relaxed);
1827        // The serving defaults live in one place, `SelectOptions::for_serving`,
1828        // and the request-specific fields are laid over them here. Building the
1829        // whole struct inline is what let a test measure a configuration the
1830        // server never runs in; see `Selection::answered_from_bitmap`.
1831        let options = SelectOptions {
1832            boundary: shallow_outcome.boundary.clone(),
1833            have_boundary: shallow_outcome.client_boundary.clone(),
1834            filter: request.filter.as_ref(),
1835            restrict: request
1836                .shallow
1837                .is_deepening()
1838                .then_some(&shallow_outcome.include),
1839            missing: Missing::Fail,
1840            ..SelectOptions::for_serving(&self.config.limits)
1841        };
1842
1843        // The wants, plus the parents of everything this fetch just promised to
1844        // unshallow. Those parents are not a second want: they come from the
1845        // server's own walk of its own graph, already filtered by `restrict` to
1846        // what the deepening decided, so they widen the pack without widening
1847        // what the client is allowed to name. `check_wants` stays the gate on
1848        // the client's side of that line.
1849        let mut tips = wants.to_vec();
1850        if !shallow_outcome.unshallow_parents.is_empty() {
1851            // A set, not `tips.contains`: `wants` is the client's list.
1852            let mut named: HashSet<ObjectId> = tips.iter().copied().collect();
1853            for id in &shallow_outcome.unshallow_parents {
1854                if named.insert(*id) {
1855                    tips.push(*id);
1856                }
1857            }
1858        }
1859        let mut selection = {
1860            let _s = crate::stage::enter("select");
1861            select_objects(graph, &tips, common, &options)?
1862        };
1863
1864        if request.include_tag {
1865            let _s = crate::stage::enter("include_tag");
1866            self.add_reachable_tags(graph, refs, &mut selection.objects)?;
1867        }
1868
1869        let show_progress = sideband && !request.no_progress;
1870        progress(
1871            out,
1872            show_progress,
1873            &format!(
1874                "gunnar: packing {} objects from {} commits\n",
1875                selection.objects.len(),
1876                selection.commits.len()
1877            ),
1878        )?;
1879
1880        // ── Copy, do not re-deflate ─────────────────────────────────────────
1881        //
1882        // `ObjectStore::packed()` is `None` for a store with no packs to copy
1883        // from: `mem`, and any cold tier that keeps inflated content per oid
1884        // instead. Those fall through to the whole-object writer they have
1885        // always used.
1886        //
1887        // `ofs_delta` is the client's own answer about whether it can decode
1888        // `OBJ_OFS_DELTA`, and until 2026-08-03 it gated the copy path
1889        // entirely: a client that did not advertise it got the pre-pack-copy
1890        // server, no delta reuse and no entry copying at all. MEASURED on the
1891        // churned fixture, that cost such a client **18.4x the bytes** — for
1892        // nothing, because `OBJ_REF_DELTA` needs no capability and carries the
1893        // identical delta bytes. The capability now decides how a copied delta
1894        // *names its base* (`WriteOptions::ofs_delta`), not whether copying
1895        // happens.
1896        // ── the unselected delta base ───────────────────────────────────────
1897        //
1898        // `WriteOptions::base_closure` lets `write_pack_copy` add a delta's
1899        // unselected base rather than expand the delta. MEASURED over the
1900        // corpus's single-branch clones: −2.82% wire bytes, −51.9% CPU.
1901        //
1902        // Three conditions, and each one is a different kind of reason:
1903        //
1904        //  1. **the negotiation found nothing in common.** This is the one that
1905        //     decides most of the value, and it is measured rather than argued.
1906        //     On a *clone*, an unselected base is an object nobody is getting,
1907        //     and shipping it buys back a whole exploded object. On a
1908        //     *negotiated* fetch it is usually an object the client already
1909        //     holds — it sits below the cut — so shipping it is the duplication
1910        //     thin packs exist to avoid. MEASURED 2026-08-05 on
1911        //     `tests/pack_copy.rs`'s churned fixture, one fetch 8 commits deep
1912        //     from a client that withheld `thin-pack`: **31 360 → 55 513 bytes,
1913        //     +77%**, adding 57 bases to recover 24 deltas. A fetch's answer to
1914        //     that delta is the thin voucher, not this.
1915        //  2. **a filtered request** turns it off. `--filter=blob:limit=1k` says
1916        //     which objects the client will accept, an unselected base is by
1917        //     construction an object the selection did not choose, and a small
1918        //     blob's delta base is usually a large one — so a closure under a
1919        //     filter ships exactly the object the filter excluded, silently.
1920        //  3. **the server's object ceiling** bounds how large a base may be. It
1921        //     is `Some` on every request, so it cannot be folded into the same
1922        //     answer as the other two; doing that turned the closure off
1923        //     everywhere and `tests/base_closure.rs` is what caught it.
1924        // ⚠ **THE THREE CONDITIONS ABOVE NO LONGER CROSS THE SEAM, and one of
1925        // them is a real loss.** `WriteOptions::base_closure` was a knob on
1926        // gunnar's own pack writer; the engine owns the format now and
1927        // `git_storage_trait::Caps` is deliberately the CLIENT's capabilities
1928        // (`thin`, `ofs_delta`) and not the server's policy, which is the right
1929        // line to draw. Conditions 1 and 2 survive by construction — a non-thin
1930        // pack must be closed over its bases whatever anyone prefers, and
1931        // `thin` below already carries "the client said it can take an open
1932        // one". Condition **3** does not: there is no longer a way to tell an
1933        // engine how large an unselected delta base it may pull in, so
1934        // `Limits::object_bytes` bounds what this end *selects* and no longer
1935        // bounds what the engine *adds*. The measurements above are kept
1936        // because they are the record of why the knob existed; the knob is
1937        // gone. Recorded here rather than silently dropped.
1938        // ── May this answer be a THIN pack? ─────────────────────────────────
1939        //
1940        // Pack-copy copies 100% of a clone's entries and only 97.6% of a
1941        // fetch's, and the missing 2.4% is not a rounding error — it is every
1942        // object whose delta base sits on the client's side of the negotiation
1943        // cut, each one inflated and re-deflated **whole**. MEASURED
1944        // 2026-08-04 on a 60-file churn fetched 8 commits deep: 24 of 40
1945        // objects expanded, 3.98x stock git's thin pack of the same objects.
1946        // Naming those bases instead of expanding them is the fix, and it
1947        // computes nothing: the deltas are the pushing client's, already in
1948        // gunnar's pack. `gunnar-delta` stays off this path.
1949        //
1950        // Three conditions, and every one of them is a correctness condition
1951        // rather than a tuning knob, because a thin pack whose bases the
1952        // receiver lacks is not a smaller answer but a **failed fetch**:
1953        //
1954        //  1. the client advertised `thin-pack`. Nothing else may be read as
1955        //     consent to receive one.
1956        //  2. the negotiation found something in common. With no `common` the
1957        //     request is a clone, the client holds nothing, and there is
1958        //     nothing a base could safely point at. `selection.client_has` is
1959        //     then **empty** — since 2026-08-05 it carries only what the
1960        //     receiver already held, never the objects being sent — so this
1961        //     would be a no-op. Saying it here means the clone path is provably
1962        //     untouched rather than incidentally so.
1963        //  3. the client is not shallow. A `have X` from a shallow client is
1964        //     not proof it holds `X`'s ancestry, which is the whole reason
1965        //     `ShallowOutcome::client_boundary` exists; the walk honours that
1966        //     boundary, and this declines to bet the fetch on it.
1967        //  4. the request carries **no filter**, and this one became live on
1968        //     2026-08-11. `have` is a set of commit *tips*, and every consumer
1969        //     of it — `selection.client_has` here, `client_bases` in the znippy
1970        //     engine — turns it into what the receiver holds by closing over it.
1971        //     For a partial clone that closure is a **lie in the dangerous
1972        //     direction**: a `blob:none` client's tips reach every blob under
1973        //     them and it was deliberately sent none of them, so a base vouched
1974        //     for this way can be an object it has never seen and its
1975        //     `index-pack --fix-thin` dies with `pack has N unresolved deltas`.
1976        //     It cost nothing while no engine read `have`; znippy's does now.
1977        //
1978        // Past the gate every individual base is still checked against
1979        // `selection.client_has`, and one that does not answer costs its
1980        // object the delta and nothing else.
1981        let thin = request.thin_pack
1982            && !common.is_empty()
1983            && request.filter.is_none()
1984            && shallow_outcome.boundary.is_empty()
1985            && shallow_outcome.client_boundary.is_empty();
1986
1987        let outcome = {
1988            let _s = crate::stage::enter("pack_write");
1989            let mut pack = PackOutput::new(out, sideband);
1990            // ── the engine emits; this streams ────────────────────────────
1991            //
1992            // What stood here was gix: `write_pack_copy_thin` drove
1993            // `iter_from_counts`, and its `None` arm inflated every object
1994            // through gix's object traits. Both are gone with the packfile
1995            // engine, and the seam that chose between them (`ObjectStore::packed`)
1996            // went with them, because its return type named the pack writer.
1997            //
1998            // The engine owns the format now. It closes the request over its
1999            // delta bases, orders bases before deltas, re-encodes each entry
2000            // header and copies every payload byte for byte. This end knows
2001            // nothing about entry headers, which is the point.
2002            //
2003            // `None` is refused rather than improvised around: a whole-object
2004            // fallback produces a pack that passes `index-pack --strict` and
2005            // `fsck` while sending a measured 18.4x the wire bytes, which is
2006            // the failure nobody notices.
2007            // ── ✅ DECIDED 2026-08-10 — the first parameter is an EXACT SET ──
2008            //
2009            // The ruling went the way this call site needed. The
2010            // contract's first parameter is now named `objects` and reads
2011            // *"emit a packfile containing exactly `objects`"*. **The engine
2012            // must not compute a closure.** `GitServe::select` owns the
2013            // close-over-tips half and keeps it, so there is still exactly one
2014            // way to ask each question.
2015            //
2016            // This was not a rename. The znippy arm's `emit_pack` **was closing
2017            // over its input** — it opened with `self.select(objects, have)?`
2018            // and emitted that — so this call site was, as written, an
2019            // over-send waiting for the first engine to be wired up:
2020            //
2021            // | request | closure of the selection | the selection |
2022            // |---|---|---|
2023            // | clone, no filter | identical | identical |
2024            // | `--filter=blob:none` | **adds back every excluded blob** | correct |
2025            // | `--depth=N` | **adds back the grafted-away ancestry** | correct |
2026            //
2027            // An over-send passes `index-pack --strict` and `fsck`. It is the
2028            // failure nobody notices, and it is the failure class this whole
2029            // seam exists to abolish. It is now pinned on the engine side by
2030            // `znippy-plugin-git`'s
2031            // `emit_pack_emits_exactly_the_set_it_is_given_and_never_its_closure`,
2032            // which asserts an object **count** and was seen RED at 4 against
2033            // an expected 1.
2034            //
2035            // **`selected` below is genuinely the exact set to emit**, and that
2036            // is checked rather than assumed: `select_objects` applies the
2037            // partial-clone `filter` at insertion (`Graph::keeps_blob` /
2038            // `keeps_tree`), the shallow `boundary` and `have_boundary`, and a
2039            // deepening's `restrict`; `include_tag` then adds to
2040            // `selection.objects` above. Every one of those decisions is a
2041            // deliberate *exclusion*, and every one of them is undone by a
2042            // closure.
2043            //
2044            // `have` is the negotiated common **tips**, and that half is right
2045            // as frozen: it is what lets an engine derive for itself which
2046            // bases a thin delta may name — never to derive membership. Tips
2047            // and not the walk's exclusion closure, deliberately — the closure
2048            // is repository-sized, the engine can rebuild it from its own
2049            // index, and `ReachSet::client_has` is documented as *what the
2050            // receiver held BEFORE this transfer*, which is exactly what a set
2051            // of pre-transfer tips names and what `Selection::client_has` had
2052            // to be narrowed to on 2026-08-05.
2053            //
2054            // ✅ **The znippy arm reads `have` since 2026-08-11**, and this is
2055            // what it reads it for: with `caps.thin` set, a delta whose base
2056            // falls outside the request but inside the closure of these tips
2057            // goes out as a `REF_DELTA` naming that base, carrying the stored
2058            // delta stream unchanged, instead of being rebuilt whole. `vouched`
2059            // below is that set's size, and `recompressed` falling while it is
2060            // non-zero is the pass working.
2061            //
2062            // ⚠ **Do not assert `stats.objects == selected.len()` here.** The
2063            // contract's word is "exactly", and it means *no reachability
2064            // closure*; it does not mean the counts must match. A packfile's
2065            // `OFS_DELTA` names its base by a backwards distance **within the
2066            // pack**, so an engine copying stored entries has to pull in any
2067            // delta base that fell outside the selection or emit something
2068            // unparseable. That is a format requirement, not a decision about
2069            // what the client asked for, and `caps.thin` is the allowance to
2070            // skip it. `stats.objects` is therefore `selected.len()` plus the
2071            // bases the format forced, and it is logged rather than asserted.
2072            //
2073            // There is no `Ok(None)` and no fallback. An engine that cannot
2074            // emit says so as an error: a whole-object fallback produces a pack
2075            // that passes `fsck` while sending a measured **18.4x** the wire
2076            // bytes.
2077            let caps = gunnar_store::git_storage_trait::Caps {
2078                ofs_delta: request.ofs_delta,
2079                thin,
2080            };
2081            let selected: Vec<&[u8]> = selection.objects.iter().map(|id| id.as_bytes()).collect();
2082            let vouched: Vec<&[u8]> = common.iter().map(|id| id.as_bytes()).collect();
2083            // The trailer IS the pack's identity and it is the last bytes of
2084            // the stream, so it is read off what was emitted rather than
2085            // recomputed — the same property the buffered writer had, kept
2086            // through a streaming emitter by watching the tail go past.
2087            // `PackStats` deliberately does not carry a pack id, and it should
2088            // not have to: a receipt that *stated* the id would be a second
2089            // authority on it, and this one cannot disagree with the bytes.
2090            let mut tapped = TrailerTap::new(&mut pack, self.hash_kind.len_in_bytes());
2091            let stats = self
2092                .git
2093                .emit_pack(&selected, &vouched, &caps, &mut tapped)
2094                .map_err(crate::graph::backend)?;
2095            let pack_id = crate::graph::oid(&tapped.trailer()?)?;
2096            let outcome = crate::pack::WriteOutcome {
2097                num_objects: stats.objects as u32,
2098                pack_id,
2099                bytes_written: stats.bytes,
2100                // pack-copy's applied-output assertion, and it is READ BACK off
2101                // what the engine reported rather than asserted here. A byte
2102                // count and a wall clock cannot tell a pack-copy from a
2103                // re-deflate; these two can, and the day an engine starts
2104                // re-deflating, `recompressed` climbs instead of a test staying
2105                // green.
2106                objects_copied_from_pack: stats.copied as usize,
2107                objects_recompressed: stats.recompressed as usize,
2108                // Deltas are the pushing client's, preserved rather than
2109                // computed, so nothing here deltified anything.
2110                objects_deltified: 0,
2111                // Still zero, and now for a smaller reason: the narrowing pass
2112                // exists but `PackStats` does not carry its counts, so there is
2113                // nothing honest to put here. `recompressed` is the observable
2114                // — it falls as bases are offered — and inventing a number for
2115                // these three would be worse than reporting none.
2116                thin_bases_offered: 0,
2117                closure_bases_declined: 0,
2118                thin_bases_vetoed: 0,
2119            };
2120            pack.flush()?;
2121            outcome
2122        };
2123
2124        tracing::debug!(
2125            // `gunnar::git`, not the module-path default. This line was on
2126            // `gunnar_wire::upload_pack` and was the LAST entry the observability
2127            // suite's `known` remainder held for this crate: it is the only line
2128            // the whole clone path emits, and `RUST_LOG=gunnar::git=debug` — the
2129            // filter this repository documents for looking at a transfer — did
2130            // not select it. An operator turning the git log up to find out what
2131            // a clone did got the push path and nothing else.
2132            target: crate::receive_pack::GIT_TARGET,
2133            event = "git.upload_pack.sent",
2134            objects = outcome.num_objects,
2135            // The other half of `objects`, and the reason it is here:
2136            // `objects` is what the ENGINE emitted and this is what
2137            // `select` chose. On a negotiated fetch over a packed store
2138            // the first was measured at 8.72x the second, and no line
2139            // anywhere said so.
2140            selected = selection.objects.len(),
2141            bytes = outcome.bytes_written,
2142            copied = outcome.objects_copied_from_pack,
2143            recompressed = outcome.objects_recompressed,
2144            // How much this transfer was allowed to point a delta at. The two
2145            // selection paths reach it differently — an id set on the walk, a
2146            // bitmap otherwise — and this is where a regression in either would
2147            // show up as `recompressed` climbing beside it.
2148            vouched = selection.client_has.len(),
2149            // WHICH path produced that vouch. Without it every line above is
2150            // ambiguous between two paths that measurably disagree; see
2151            // [`UploadSummary::answered_from_bitmap`].
2152            bitmap = selection.answered_from_bitmap,
2153            thin,
2154            sideband,
2155            "served a packfile"
2156        );
2157        Ok(SentPack {
2158            written: outcome,
2159            answered_from_bitmap: selection.answered_from_bitmap,
2160            objects_selected: selection.objects.len(),
2161        })
2162    }
2163
2164    /// `include-tag`: add annotated tags whose target is already in the pack.
2165    fn add_reachable_tags(
2166        &self,
2167        graph: &Graph<'_>,
2168        refs: &RefSnapshot,
2169        objects: &mut Vec<ObjectId>,
2170    ) -> Result<()> {
2171        let mut selected: HashSet<ObjectId> = objects.iter().copied().collect();
2172        let mut extra = Vec::new();
2173        for row in refs.with_prefix(Some("refs/tags/")) {
2174            // A symbolic reference under `refs/tags/` is not a tag; the old
2175            // `Target::Object(id) else { continue }` said exactly this and this
2176            // says it against the row.
2177            let Some(bytes) = &row.oid else { continue };
2178            let id = crate::graph::oid(bytes)?;
2179            if selected.contains(&id) || !graph.contains(&id)? {
2180                continue;
2181            }
2182            if graph.kind(&id)? != Some(object::Kind::Tag) {
2183                continue;
2184            }
2185            let peeled = graph.peel(&id)?;
2186            if selected.contains(&peeled.id) {
2187                for tag in peeled.tags {
2188                    // `selected` absorbs what `extra` used to be scanned for
2189                    // — one set, one membership test, and a tag chain
2190                    // that appears under two references is still added once.
2191                    if selected.insert(tag) {
2192                        extra.push(tag);
2193                    }
2194                }
2195            }
2196        }
2197        objects.extend(extra);
2198        Ok(())
2199    }
2200}
2201
2202/// How many `have` facts one connection may remember, derived from the request
2203/// ceiling rather than chosen separately (one implementation).
2204///
2205/// A `have` costs the client a whole pkt-line — four header bytes, `have `, and
2206/// a hex object id, so 45 bytes at sha1 and 77 at sha256. Dividing the request
2207/// ceiling by the *cheapest* of those is deliberate: it makes the memo's ceiling
2208/// the number of distinct `have`s a client could send in one ceiling's worth of
2209/// bytes, which is the honest ratio between what the peer spends and what the
2210/// server remembers.
2211fn memo_capacity(limits: &Limits) -> Option<usize> {
2212    /// `0004` + `have ` + 40 hex digits + `\n`.
2213    const CHEAPEST_HAVE_LINE: usize = 4 + 5 + 40 + 1;
2214    limits.request_bytes.map(|bytes| bytes / CHEAPEST_HAVE_LINE)
2215}
2216
2217struct V2Command {
2218    name: String,
2219    capabilities: PeerCapabilities,
2220    arguments: Vec<Vec<u8>>,
2221}
2222
2223/// Split a v0 `want <oid> <capabilities>` line into its command and its
2224/// capability string.
2225///
2226/// A NUL is honoured too, because that is what some older clients and every
2227/// `receive-pack` client uses, and accepting both costs one branch.
2228fn split_v0_capabilities(line: &[u8]) -> Option<(&[u8], &[u8])> {
2229    if let Some(nul) = line.iter().position(|&b| b == 0) {
2230        return Some((&line[..nul], &line[nul + 1..]));
2231    }
2232    let rest = line.strip_prefix(b"want ")?;
2233    let space = rest.iter().position(|&b| b == b' ')?;
2234    Some((&line[..b"want ".len() + space], &rest[space + 1..]))
2235}
2236
2237/// Drop repeated ids, keeping the first occurrence of each.
2238///
2239/// # Once, here, rather than in each consumer
2240///
2241/// The want list is the peer's, and it is bounded only by what the request
2242/// reader accepts, so **every** per-line cost in it is a cost the peer chooses.
2243/// A repeated `want` used to be paid for four separate times over: an existence
2244/// probe in `wants_exist`, a `peel` in `commit_wants`, another `peel` as a tip
2245/// of the selection walk, and a pass in `negotiate`. Deduplicating in each of
2246/// those is four places to keep agreeing; deduplicating the list once, before
2247/// anybody reads it, is one (one implementation).
2248///
2249/// Order is preserved because it is observable: the selection walk emits
2250/// objects in tip order, so sorting here would quietly reorder packs.
2251fn dedupe_oids(ids: &mut Vec<ObjectId>) {
2252    if ids.len() < 2 {
2253        return;
2254    }
2255    let mut seen: HashSet<ObjectId> = HashSet::with_capacity(ids.len());
2256    ids.retain(|id| seen.insert(*id));
2257}
2258
2259/// Split `name value` at the first space; a line with no space is all name.
2260fn split_argument(line: &str) -> (&str, Option<&str>) {
2261    match line.split_once(' ') {
2262        Some((name, value)) => (name, Some(value)),
2263        None => (line, None),
2264    }
2265}
2266
2267fn parse_oid(hex: &str, hash_kind: Kind) -> Result<ObjectId> {
2268    let id = ObjectId::from_hex(hex.trim().as_bytes())
2269        .map_err(|err| Error::protocol(format!("{hex:?} is not an object id: {err}")))?;
2270    if id.kind() != hash_kind {
2271        return Err(Error::ObjectFormatMismatch {
2272            ours: caps::object_format_name(hash_kind),
2273            theirs: caps::object_format_name(id.kind()).to_owned(),
2274        });
2275    }
2276    Ok(id)
2277}
2278
2279/// A pass-through writer that remembers the **last `n` bytes** it saw.
2280///
2281/// One use: a packfile's trailer is its identity and it is the last
2282/// `hash_kind.len_in_bytes()` bytes of the stream. `GitServe::emit_pack`
2283/// streams straight to the client and its
2284/// [`PackStats`](gunnar_store::git_storage_trait::PackStats) deliberately does
2285/// not carry a pack id — a receipt that *stated* the id would be a second
2286/// authority on it, and two authorities can disagree (one implementation). So
2287/// the id is read off the bytes that actually went out, which is what the old
2288/// buffered writer did and the property worth keeping.
2289///
2290/// It costs one `n`-byte array and one `copy_within` per write call, and nothing
2291/// per byte beyond the copy every writer already makes. It buffers **nothing**:
2292/// every byte is forwarded before it is recorded, so a slow client is not held
2293/// up and a large pack does not accumulate here.
2294struct TrailerTap<'w> {
2295    inner: &'w mut dyn Write,
2296    /// The tail, oldest first, never longer than `want`.
2297    tail: Vec<u8>,
2298    want: usize,
2299}
2300
2301impl<'w> TrailerTap<'w> {
2302    fn new(inner: &'w mut dyn Write, want: usize) -> Self {
2303        TrailerTap {
2304            inner,
2305            tail: Vec::with_capacity(want),
2306            want,
2307        }
2308    }
2309
2310    /// The last `want` bytes written.
2311    ///
2312    /// An error rather than a shorter slice when fewer went past: a pack
2313    /// without a full trailer is not a pack, and inventing an id for it would
2314    /// put a wrong pack id in a log line that exists to identify what was sent.
2315    fn trailer(&self) -> Result<Vec<u8>> {
2316        if self.tail.len() < self.want {
2317            return Err(Error::Store(gunnar_store::Error::backend(format!(
2318                "the engine emitted {} bytes, which is shorter than the {}-byte pack trailer \
2319                 every packfile ends with",
2320                self.tail.len(),
2321                self.want
2322            ))));
2323        }
2324        Ok(self.tail.clone())
2325    }
2326}
2327
2328impl Write for TrailerTap<'_> {
2329    fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
2330        let written = self.inner.write(data)?;
2331        let fresh = &data[..written];
2332        if fresh.len() >= self.want {
2333            self.tail.clear();
2334            self.tail
2335                .extend_from_slice(&fresh[fresh.len() - self.want..]);
2336        } else {
2337            let overflow = (self.tail.len() + fresh.len()).saturating_sub(self.want);
2338            self.tail.drain(..overflow);
2339            self.tail.extend_from_slice(fresh);
2340        }
2341        Ok(written)
2342    }
2343
2344    fn flush(&mut self) -> std::io::Result<()> {
2345        self.inner.flush()
2346    }
2347}