gunnar-wire 1.5.1

git smart-protocol state machines: upload-pack (v2 with a v0 fallback) and receive-pack (v0), transport-agnostic
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
//! Capabilities, and the table that keeps the advertisement honest.
//!
//! # The bug this module exists to make impossible
//!
//! A protocol-v2 client will not send an argument unless the corresponding
//! **feature** appears in the value of the capability it belongs to. A server
//! that parses `filter <spec>` but advertises `fetch=shallow wait-for-done` has
//! partial-clone support that can never activate. Worse, it looks *exactly*
//! like working code: no error is raised, no test fails, the client simply
//! never exercises the path. Stock `git` 2.53 does this deliberately
//! (`uploadpack.allowFilter` defaults off), which is why the shape is easy to
//! copy by accident.
//!
//! So the advertisement is not written down anywhere. There is one table
//! ([`FETCH_ARGUMENTS`], [`LS_REFS_ARGUMENTS`]) that says, per argument, which
//! feature gates it, and both the **parser** and the **advertisement** are
//! derived from it. Adding an argument to the parser without advertising its
//! feature is not something you can forget to do; it is something you cannot
//! express. That is a fix by construction rather than a guard watching two
//! copies agree (one implementation), and `tests/advertisement.rs` still asserts the
//! resulting invariant from the outside, red-first.
//!
//! # The other bug, which the paragraph above did NOT prevent
//!
//! That paragraph was true of the **v2 arguments** and of nothing else, and it
//! was read as covering everything — an early audit
//! said outright that *"advertised but not honoured is not something this
//! codebase can express"*. It was expressible in three other places, and it was
//! expressed:
//!
//! * `upload_pack_v0_capabilities` was a hand-written `vec![]`,
//! * `receive_pack_capabilities` was a second one, and
//! * the **v2 capability names** were a third, in `upload_pack.rs`.
//!
//! `"push-options".to_owned()` sat in the second of those. It was advertised,
//! `receive_pack::ReceivePack::serve` parsed the section, and the only consumer
//! anywhere in the tree was `.len()` in a log field. A client that ran
//! `git push -o ci.skip` was told on the wire that the server speaks push
//! options and the option went nowhere — a wrong answer on the wire, not a
//! missing feature.
//!
//! [`ADVERTISED`] closes it the same way: **one place decides what is
//! advertised, for all three surfaces**, every row carries a [`Honoured`]
//! saying where it is honoured, and `tests/advertisement.rs` checks each claim
//! against the source with comments stripped — because
//! `contains("push-options")` is satisfied by the paragraph explaining
//! push-options two lines above the call site.

use gix_hash::Kind;

/// The product half of the `agent=` capability.
pub const PRODUCT: &str = "gunnar";

/// The `agent=` value gunnar advertises: `gunnar/<crate version>`.
pub fn default_agent() -> String {
    format!("{PRODUCT}/{}", env!("CARGO_PKG_VERSION"))
}

/// git's name for a hash algorithm, as it appears in `object-format=`.
pub fn object_format_name(kind: Kind) -> &'static str {
    match kind {
        Kind::Sha1 => "sha1",
        Kind::Sha256 => "sha256",
        // `gix_hash::Kind` is `#[non_exhaustive]`. A hash gunnar has never
        // heard of cannot be advertised as one it has, so it gets a name no
        // client will match and `check_object_format` rejects every request
        // against it. Loudly wrong beats quietly wrong.
        _ => "unsupported",
    }
}

/// The inverse of [`object_format_name`].
pub fn object_format_from_name(name: &str) -> Option<Kind> {
    match name {
        "sha1" => Some(Kind::Sha1),
        "sha256" => Some(Kind::Sha256),
        _ => None,
    }
}

/// Whether an argument carries a value after its name.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Value {
    /// The whole line is the argument name: `done`, `thin-pack`.
    None,
    /// A single space, then the value: `want <oid>`, `ref-prefix <p>`.
    Required,
}

/// One argument a protocol-v2 command accepts.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Argument {
    /// The name as it appears on the wire.
    pub name: &'static str,
    /// Whether a value follows.
    pub value: Value,
    /// The feature that must appear in the capability's advertised value
    /// before a client is allowed to send this argument, or `None` for the
    /// arguments every v2 client may always send.
    pub feature: Option<&'static str>,
}

const fn plain(name: &'static str, value: Value) -> Argument {
    Argument {
        name,
        value,
        feature: None,
    }
}

const fn gated(name: &'static str, value: Value, feature: &'static str) -> Argument {
    Argument {
        name,
        value,
        feature: Some(feature),
    }
}

/// Every argument `command=ls-refs` accepts.
pub const LS_REFS_ARGUMENTS: &[Argument] = &[
    plain("symrefs", Value::None),
    plain("peel", Value::None),
    plain("ref-prefix", Value::Required),
    gated("unborn", Value::None, "unborn"),
];

/// Every argument `command=fetch` accepts.
///
/// The four `deepen*` forms and `shallow` are all gated on the single
/// `shallow` feature, exactly as git defines it: advertising `shallow` is a
/// promise to accept all five, which is why they are implemented rather than
/// rejected.
pub const FETCH_ARGUMENTS: &[Argument] = &[
    plain("want", Value::Required),
    plain("have", Value::Required),
    plain("done", Value::None),
    plain("thin-pack", Value::None),
    plain("no-progress", Value::None),
    plain("include-tag", Value::None),
    plain("ofs-delta", Value::None),
    gated("shallow", Value::Required, "shallow"),
    gated("deepen", Value::Required, "shallow"),
    gated("deepen-relative", Value::None, "shallow"),
    gated("deepen-since", Value::Required, "shallow"),
    gated("deepen-not", Value::Required, "shallow"),
    gated("filter", Value::Required, "filter"),
    gated("want-ref", Value::Required, "ref-in-want"),
    gated("wait-for-done", Value::None, "wait-for-done"),
];

/// Look an argument up by name.
pub fn argument(table: &'static [Argument], name: &str) -> Option<&'static Argument> {
    table.iter().find(|a| a.name == name)
}

/// The features of `table`, in first-seen order and without duplicates.
///
/// This is the only place an advertised feature list comes from.
pub fn features(table: &'static [Argument]) -> Vec<&'static str> {
    let mut out: Vec<&'static str> = Vec::new();
    for arg in table {
        if let Some(feature) = arg.feature {
            if !out.contains(&feature) {
                out.push(feature);
            }
        }
    }
    out
}

/// Render `name=<features>` for a v2 capability, or bare `name` when the
/// command has no gated arguments at all.
pub fn capability(name: &str, table: &'static [Argument]) -> String {
    let features = features(table);
    if features.is_empty() {
        name.to_owned()
    } else {
        format!("{name}={}", features.join(" "))
    }
}

/// What a client may turn off in the fetch capability.
///
/// `filter` is the one capability an operator plausibly wants to decline
/// (serving a partial clone means serving arbitrary blob subsets), so it is a
/// knob rather than a constant. Declining it removes the feature from the
/// advertisement **and** makes the parser reject the argument, together, in
/// [`fetch_capability`] and [`crate::upload_pack::FetchRequest::parse`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Offered {
    /// Whether `filter` is advertised and accepted.
    pub filter: bool,
}

impl Default for Offered {
    fn default() -> Self {
        Offered { filter: true }
    }
}

impl Offered {
    /// Is `feature` on offer?
    pub fn has(&self, feature: &str) -> bool {
        match feature {
            "filter" => self.filter,
            _ => true,
        }
    }

    /// May `argument` be sent by a client, given what is on offer?
    pub fn accepts(&self, argument: &Argument) -> bool {
        argument.feature.is_none_or(|f| self.has(f))
    }
}

// ════════════════════════════════════════════════════════════════════════════
// The advertisement — ONE table, all three surfaces
// ════════════════════════════════════════════════════════════════════════════

/// Where the code that honours a capability lives.
///
/// Every entry in every advertisement table carries one of these, and
/// `tests/advertisement.rs` reads them: the named file, **with its comments
/// stripped**, must contain the named code. Comments are stripped because
/// `contains("push-options")` is satisfied by the paragraph explaining
/// push-options two lines above the call — a guard that a doc comment can
/// satisfy is a guard that survives the deletion of the thing it guards.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Honoured {
    /// Read by code in `file` containing the substring `code`.
    ///
    /// `file` is relative to the workspace root.
    By {
        /// Workspace-relative path.
        file: &'static str,
        /// A substring of the line that reads it, e.g. `peer.has("atomic")`.
        code: &'static str,
    },
    /// Read by a public item of a crate gunnar **depends on**, not by a file in
    /// this tree.
    ///
    /// This variant exists because the other one could not tell the truth here.
    /// `ofs-delta`'s obligation is discharged by `gunnar-pack`, which lived at
    /// `../edda/crates/gunnar-pack` and was cited as a workspace-relative PATH —
    /// a path that only resolves on a machine with an edda checkout beside
    /// gunnar. From a fresh clone (and, since 2026-09-09, from every clone, since
    /// the crate is a crates.io dependency) the citation named a file that is not
    /// there, and the guard failed for the absence of a sibling rather than for
    /// anything about gunnar.
    ///
    /// A path was the wrong shape for the claim anyway. What is being asserted is
    /// that a named ITEM exists in a named CRATE, and the compiler can assert
    /// that exactly — `advertisement.rs` carries a `use` of every item cited this
    /// way, so the guard is a build error rather than a substring search, and it
    /// holds whether the crate resolves from the registry or from a patched
    /// sibling checkout.
    ByDependency {
        /// The crate as cargo names it, e.g. `gunnar-pack`.
        krate: &'static str,
        /// The public item that does the work, e.g. `index_thin_pack`.
        item: &'static str,
    },
    /// There is nothing to do, and here is the argument for why.
    ///
    /// The reason is a sentence, not a marker: this variant is the escape hatch
    /// and it has to cost something to use. Today exactly three capabilities
    /// use it, and each says what makes it vacuous rather than unimplemented.
    Vacuously(&'static str),
}

/// What follows a capability's name on the wire.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapValue {
    /// Nothing: the name alone, `atomic`.
    Bare,
    /// `=sha1` / `=sha256`, from the repository's hash.
    ObjectFormat,
    /// `=gunnar/1.1.0`, from the caller's agent string.
    Agent,
    /// `=<features>`, generated from a v2 argument table by [`features`].
    ///
    /// A bare name when the table gates nothing, which is what
    /// [`capability`] already does.
    Features(&'static [Argument]),
}

/// One capability gunnar advertises, and the thing that honours it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Capability {
    /// The name as it appears on the wire, without any `=value`.
    pub name: &'static str,
    /// What follows the name.
    pub value: CapValue,
    /// The [`Offered`] knob that can withdraw this capability, if any.
    ///
    /// `None` means it is always advertised. `Some(feature)` means it appears
    /// only when [`Offered::has`] says so — and the parser must refuse the
    /// argument in the same breath, which is what `Offered` exists to make
    /// impossible to get wrong.
    pub offered: Option<&'static str>,
    /// Where it is honoured.
    pub honoured: Honoured,
    /// The [`WantPolicy`] this row is a promise about, if any.
    ///
    /// `None` for every capability whose truth does not depend on what a
    /// client is allowed to `want` — which is all of them but the two
    /// `allow-*-sha1-in-want` rows.
    ///
    /// This exists because the drift `Honoured` catches runs one way only. A
    /// row honoured by nothing is caught by the guard; a **policy honoured by
    /// no row** is not, because there is no row to look at. `WantPolicy` was
    /// exactly that: enforced by `check_wants` on every request and named in no
    /// advertisement, so a stock client refused the request on its own side and
    /// the server never heard it. See [`WantPolicy::advertises`].
    pub requires: Option<crate::upload_pack::WantPolicy>,
}

const fn cap(name: &'static str, code: &'static str, file: &'static str) -> Capability {
    Capability {
        name,
        value: CapValue::Bare,
        offered: None,
        honoured: Honoured::By { file, code },
        requires: None,
    }
}

/// The file both push-side capability readers live in.
const RECEIVE: &str = "crates/gunnar-wire/src/receive_pack.rs";
/// The file the fetch-side capability readers live in.
const UPLOAD: &str = "crates/gunnar-wire/src/upload_pack.rs";

/// `agent=`, which is the same row on all three surfaces.
const AGENT: Capability = Capability {
    name: "agent",
    value: CapValue::Agent,
    offered: None,
    honoured: Honoured::Vacuously(
        "an identifier for the peer's logs. There is no behaviour attached to \
         it at either end; git itself only records it.",
    ),
    requires: None,
};

/// `object-format=`, which is the same row on all three surfaces.
const OBJECT_FORMAT: Capability = Capability {
    name: "object-format",
    value: CapValue::ObjectFormat,
    offered: None,
    honoured: Honoured::By {
        file: "crates/gunnar-wire/src/caps.rs",
        code: "pub fn check_object_format",
    },
    requires: None,
};

/// Every capability `git-upload-pack` advertises in **protocol v0**.
///
/// Plain `multi_ack` is deliberately absent: it is an ACK mode with its own
/// semantics that nothing here implements, and advertising a negotiation richer
/// than the one implemented is the same class of bug this module exists to
/// prevent. It is absent by *not being in this table*, which is now the only
/// way to be absent.
///
/// `multi_ack_detailed` and `no-done` **are** here, and the comment on the first
/// row says why: without them stock git negotiates in single-ACK, which has no
/// correct answer over stateless-rpc and cannot finish a fetch over smart HTTP.
pub const UPLOAD_PACK_V0: &[Capability] = &[
    // ── the ACK mode, and why these two are not optional ────────────────────
    //
    // This table used to carry neither, on the reasoning that gunnar negotiated
    // in single-ACK mode and that advertising a richer ACK protocol than the
    // one implemented is the bug this module exists to prevent. The reasoning
    // was sound and the conclusion was wrong, and stock git rather than an
    // argument is what decided it.
    //
    // A v0 client picks its ACK mode from what the server offers. Offered
    // neither, git 2.53.0 falls back to single-ACK, in which a bare
    // `ACK <oid>` is TERMINAL: it means "negotiation is over, a packfile
    // follows". Over a CONNECTED session that can be served correctly, and
    // `b9fee55` did exactly that — git's rule is on `have_obj.nr`, so nothing
    // more goes out once the ACK has, and the surplus `NAK` that used to sit in
    // the pipe is gone. That fix is real and the `else` arm in
    // `serve_v0_session` still carries it.
    //
    // Over **stateless-rpc there is no correct answer at all**, which is the
    // part a connected fix cannot reach. The round ends with the HTTP response,
    // so the client takes the terminal `ACK`, opens a FRESH `POST` carrying
    // `want` and `done` and no `have`s, and reads that response as a raw
    // sideband packfile — while the server, seeing a request with no commons,
    // correctly opens it with `NAK`. There is nothing else it could send: the
    // request genuinely has no common `have` in it. git demuxes the `N` as a
    // band number and dies with `fetch-pack: protocol error: bad band #78`.
    //
    // MEASURED 2026-08-05, git 2.53.0, a corpus of 12 pushes cut back a
    // third: `git fetch http://…` exited **128 against `b9fee55` itself**, on
    // both sha1 and sha256, and 0 against `git http-backend` on the identical
    // repository. The advertisements differed in these two names and in nothing
    // else that changes the client's behaviour. That is why the mode is now
    // implemented rather than declined — not because the earlier fix was wrong.
    //
    // `no-done` is what stateless-rpc is really after: it lets the server send
    // `ACK <oid> ready`, the terminal `ACK` and the packfile in ONE response
    // rather than making the client spend a second HTTP request to say `done`.
    //
    // Plain `multi_ack` is still absent, and deliberately: it is a third ACK
    // mode with its own semantics that nothing here implements, so it is not
    // advertised. A client too old to know `multi_ack_detailed` gets
    // single-ACK, which is correct on the connected sessions it can reach.
    cap(
        "multi_ack_detailed",
        r#"peer.has("multi_ack_detailed")"#,
        UPLOAD,
    ),
    cap("no-done", r#"peer.has("no-done")"#, UPLOAD),
    cap("side-band-64k", r#"peer.has("side-band-64k")"#, UPLOAD),
    // The **v0** reader, not the v2 argument arm. This row cited
    // `"thin-pack" => out.thin_pack = true`, which is the v2 parser, while v0
    // read the capability nowhere at all — so every v0 fetch was served as a
    // whole pack against an advertisement promising otherwise (found and fixed
    // in `222f7b2`: 1.343x the bytes on a real incremental). A v0 row must cite
    // a v0 consumer, or the guard is satisfied by the other protocol's code.
    cap("thin-pack", r#"peer.has("thin-pack")"#, UPLOAD),
    cap("ofs-delta", r#"peer.has("ofs-delta")"#, UPLOAD),
    cap("no-progress", r#"peer.has("no-progress")"#, UPLOAD),
    cap("include-tag", r#"peer.has("include-tag")"#, UPLOAD),
    // Again the **v0** arms (`parse_v0_shallow_line`), not the v2 ones. Both
    // exist and both are real, but a v0 row citing the v2 parser would stay
    // green if the v0 half were deleted — which is exactly what happened to
    // `thin-pack` above.
    cap("shallow", r#""shallow" => request"#, UPLOAD),
    cap("deepen-since", "request.shallow.since =", UPLOAD),
    cap("deepen-not", r#""deepen-not" => request"#, UPLOAD),
    cap(
        "deepen-relative",
        r#""deepen-relative" => request.shallow.relative = true"#,
        UPLOAD,
    ),
    Capability {
        name: "filter",
        value: CapValue::Bare,
        // The one capability an operator plausibly declines. Withdrawing it
        // removes it from HERE and makes the parser refuse the argument, in
        // one place, which is what `Offered` is for.
        offered: Some("filter"),
        honoured: Honoured::By {
            file: UPLOAD,
            code: r#""filter" => out.filter"#,
        },
        requires: None,
    },
    // ── the two rows that were missing, and the failure they hid ────────────
    //
    // `check_wants` has always enforced a `WantPolicy`, and the v0
    // advertisement said nothing about it. `fetch-pack.c` will not send a
    // `want` for an unadvertised object unless one of these two names was in
    // the advertisement, so the request was refused **client-side** — no
    // server error, no log line, nothing to notice. That is the inverted form
    // of the drift the rest of this table catches: not a promise nothing keeps,
    // but something kept that was never promised.
    //
    // MEASURED 2026-08-05, stock git 2.53.0 over `ext::`, one commit two
    // steps behind `main`'s tip:
    //
    // | request | v0 | v2 |
    // |---|---|---|
    // | `fetch <oid>` of a reachable non-tip commit | `Server does not allow request for unadvertised object` | 6 objects, OK |
    // | `clone --filter=blob:none`, then check out | **4 refusals, checkout failed** | OK |
    //
    // The second row is the one that matters: a partial clone fetches its blobs
    // back by bare oid. v2 has no such capability at all (`upload_pack_v2` sets
    // `ALLOW_ANY_SHA1` unconditionally), which is why every v2 arm was green
    // while every v0 arm was broken.
    //
    // Both are honoured by `check_wants`, and deliberately by the *same* line:
    // there is one policy decision, not two, and a row citing a second site
    // would go green on code that agreed with it by accident.
    Capability {
        name: "allow-reachable-sha1-in-want",
        value: CapValue::Bare,
        offered: None,
        honoured: Honoured::By {
            file: UPLOAD,
            code: "fn check_wants",
        },
        requires: Some(crate::upload_pack::WantPolicy::ReachableFromRefs),
    },
    Capability {
        name: "allow-tip-sha1-in-want",
        value: CapValue::Bare,
        offered: None,
        honoured: Honoured::By {
            file: UPLOAD,
            code: "fn check_wants",
        },
        // git sets `ALLOW_TIP_SHA1` on its own or as part of `ALLOW_ANY_SHA1`,
        // and gunnar has no separate "tips only, plus hidden ones" policy —
        // `AdvertisedTips` already serves every tip it advertises, so the
        // capability would say nothing. Only `Any` is a promise beyond
        // reachability.
        requires: Some(crate::upload_pack::WantPolicy::Any),
    },
    OBJECT_FORMAT,
    AGENT,
];

/// Every capability `git-receive-pack` advertises. Push has only v0.
pub const RECEIVE_PACK: &[Capability] = &[
    cap("report-status", r#"peer.has("report-status")"#, RECEIVE),
    cap(
        "report-status-v2",
        r#"peer.has("report-status-v2")"#,
        RECEIVE,
    ),
    Capability {
        name: "delete-refs",
        value: CapValue::Bare,
        offered: None,
        // Not `peer.has("delete-refs")`, and that is correct rather than a
        // gap: the capability tells the CLIENT it may send a delete command,
        // and the server's obligation is to carry one out. The code that does
        // is the honouring.
        honoured: Honoured::By {
            file: RECEIVE,
            code: "if command.is_delete()",
        },
        requires: None,
    },
    cap("side-band-64k", r#"peer.has("side-band-64k")"#, RECEIVE),
    Capability {
        name: "quiet",
        value: CapValue::Bare,
        offered: None,
        honoured: Honoured::Vacuously(
            "`quiet` asks receive-pack to suppress its progress reporting. \
             gunnar's receive-pack emits no progress on that connection at \
             all, so there is nothing to suppress and nothing to read the \
             capability for. Vacuously honoured, and recorded as such rather \
             than counted as a reader.",
        ),
        requires: None,
    },
    cap("atomic", r#"peer.has("atomic")"#, RECEIVE),
    Capability {
        name: "ofs-delta",
        value: CapValue::Bare,
        offered: None,
        // The push side is the RECEIVER of ofs-deltas: advertising it tells
        // the client it may send them, and the obligation is to RESOLVE them.
        // There is no `peer.has("ofs-delta")` on this surface and there should
        // not be one — the consumer is the indexer that completes the chains.
        honoured: Honoured::ByDependency {
            krate: "gunnar-pack",
            item: "index_thin_pack",
        },
        requires: None,
    },
    cap("push-options", r#"peer.has("push-options")"#, RECEIVE),
    OBJECT_FORMAT,
    AGENT,
];

/// Every capability the **protocol v2** advertisement carries.
///
/// `ls-refs` and `fetch` carry generated feature lists; the others are their
/// own row. This is what `UploadPack::write_capability_advertisement` writes,
/// rather than a fourth hand-written list beside the table — which is what it
/// was until 2026-08-05, in a different file from the two above.
pub const UPLOAD_PACK_V2: &[Capability] = &[
    AGENT,
    Capability {
        name: "ls-refs",
        value: CapValue::Features(LS_REFS_ARGUMENTS),
        offered: None,
        honoured: Honoured::By {
            file: UPLOAD,
            code: "impl LsRefsRequest",
        },
        requires: None,
    },
    Capability {
        name: "fetch",
        value: CapValue::Features(FETCH_ARGUMENTS),
        offered: None,
        honoured: Honoured::By {
            file: UPLOAD,
            code: "impl FetchRequest",
        },
        requires: None,
    },
    Capability {
        name: "server-option",
        value: CapValue::Bare,
        offered: None,
        honoured: Honoured::Vacuously(
            "protocol v2 says a server SHOULD ignore server options it does \
             not understand, and gunnar defines none. The lines are accepted \
             into `PeerCapabilities` rather than rejected, so a client that \
             sends one is not broken by it. This is NOT the `push-options` \
             case: git hands push options to `pre-receive`, so discarding \
             those was a wrong answer, while discarding server options is \
             what the specification asks for.",
        ),
        requires: None,
    },
    OBJECT_FORMAT,
];

/// Every surface, so a guard can walk all of them without naming three
/// constants and going stale when a fourth appears.
pub const ADVERTISED: &[(&str, &[Capability])] = &[
    ("upload-pack v2", UPLOAD_PACK_V2),
    ("upload-pack v0", UPLOAD_PACK_V0),
    ("receive-pack", RECEIVE_PACK),
];

/// Render one capability's wire spelling, or `None` if it is not on offer.
///
/// `want_policy` is a parameter and not a default because the advertisement
/// genuinely depends on it, and a default would be the drift this whole table
/// exists to prevent: the two `allow-*-sha1-in-want` rows are promises about
/// what `check_wants` will serve, and a server that renders them from anything
/// other than the policy it judges by is advertising a different server.
pub fn render(
    cap: &Capability,
    hash_kind: Kind,
    agent: &str,
    offered: Offered,
    want_policy: crate::upload_pack::WantPolicy,
) -> Option<String> {
    if let Some(feature) = cap.offered {
        if !offered.has(feature) {
            return None;
        }
    }
    if let Some(required) = cap.requires {
        if !want_policy.advertises(required) {
            return None;
        }
    }
    Some(match cap.value {
        CapValue::Bare => cap.name.to_owned(),
        CapValue::ObjectFormat => format!("{}={}", cap.name, object_format_name(hash_kind)),
        CapValue::Agent => format!("{}={agent}", cap.name),
        CapValue::Features(table) => {
            // `Offered` also gates individual v2 features, so this is the same
            // filter `fetch_capability` applies — one writer, not two.
            let features: Vec<&str> = features(table)
                .into_iter()
                .filter(|f| offered.has(f))
                .collect();
            if features.is_empty() {
                cap.name.to_owned()
            } else {
                format!("{}={}", cap.name, features.join(" "))
            }
        }
    })
}

/// The wire lines for a whole surface, in table order.
///
/// **This is the only thing that decides what gunnar advertises.** Before
/// 2026-08-05 the v2 list was generated from the argument table while v0 and
/// receive-pack were hand-written `vec![]`s in this file and the v2 capability
/// names were a fourth hand-written list in `upload_pack.rs` — so
/// `push-options` was a bare string literal that nothing else in the tree had
/// to agree with, and it was advertised, parsed and discarded for the whole of
/// v1. An early audit claimed that *"advertised but
/// not honoured is not something this codebase can express"*; that was true of
/// the v2 arguments and false of the other three lists. Now it is true of all
/// of them, because there is nowhere else to write one down.
pub fn advertise(
    table: &'static [Capability],
    hash_kind: Kind,
    agent: &str,
    offered: Offered,
    want_policy: crate::upload_pack::WantPolicy,
) -> Vec<String> {
    table
        .iter()
        .filter_map(|cap| render(cap, hash_kind, agent, offered, want_policy))
        .collect()
}

/// The `fetch=…` capability line for what is on offer.
pub fn fetch_capability(offered: Offered) -> String {
    let features: Vec<&str> = features(FETCH_ARGUMENTS)
        .into_iter()
        .filter(|f| offered.has(f))
        .collect();
    if features.is_empty() {
        "fetch".to_owned()
    } else {
        format!("fetch={}", features.join(" "))
    }
}

/// The `ls-refs=…` capability line.
pub fn ls_refs_capability() -> String {
    capability("ls-refs", LS_REFS_ARGUMENTS)
}

/// The capabilities `git-upload-pack` advertises in **protocol v0**, where
/// they ride on the first reference line rather than in their own section.
///
/// `multi_ack` and `multi_ack_detailed` are deliberately absent: gunnar
/// negotiates in single-ACK mode, which every git since 2005 still speaks, and
/// advertising a richer ACK protocol than the one implemented is the same
/// class of bug this module exists to prevent.
pub fn upload_pack_v0_capabilities(
    hash_kind: Kind,
    agent: &str,
    offered: Offered,
    want_policy: crate::upload_pack::WantPolicy,
) -> Vec<String> {
    advertise(UPLOAD_PACK_V0, hash_kind, agent, offered, want_policy)
}

/// The capabilities `git-receive-pack` advertises.
pub fn receive_pack_capabilities(hash_kind: Kind, agent: &str) -> Vec<String> {
    // `WantPolicy` decides what a client may *fetch*; push has no `want` at
    // all, so no row in `RECEIVE_PACK` carries a `requires` and the value here
    // cannot change a byte. Passed as the default rather than given its own
    // parameter, because a knob that cannot move is one more thing to keep
    // agreeing about.
    advertise(
        RECEIVE_PACK,
        hash_kind,
        agent,
        Offered::default(),
        crate::upload_pack::WantPolicy::default(),
    )
}

/// The capability set a v0 peer sent on its first line, parsed.
///
/// Values are kept as `Option<String>` so `object-format=sha256` and a bare
/// `atomic` are the same shape.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PeerCapabilities {
    entries: Vec<(String, Option<String>)>,
}

impl PeerCapabilities {
    /// Parse a space-separated capability string.
    ///
    /// Leading and repeated spaces are tolerated: git's own `receive-pack`
    /// client emits `…\0 report-status-v2 …` with a leading space, which a
    /// naive `split(' ')` turns into a phantom empty capability.
    pub fn parse(text: &str) -> Self {
        let entries = text
            .split(' ')
            .filter(|s| !s.is_empty())
            .map(|item| match item.split_once('=') {
                Some((k, v)) => (k.to_owned(), Some(v.to_owned())),
                None => (item.to_owned(), None),
            })
            .collect();
        PeerCapabilities { entries }
    }

    /// Is this capability present, with or without a value?
    pub fn has(&self, name: &str) -> bool {
        self.entries.iter().any(|(k, _)| k == name)
    }

    /// The value of `name=<value>`, if it was sent with one.
    pub fn value(&self, name: &str) -> Option<&str> {
        self.entries
            .iter()
            .find(|(k, _)| k == name)
            .and_then(|(_, v)| v.as_deref())
    }

    /// Every capability name, in the order sent.
    pub fn names(&self) -> impl Iterator<Item = &str> {
        self.entries.iter().map(|(k, _)| k.as_str())
    }
}

/// Check a peer's `object-format` against the repository's actual hash kind.
///
/// The whole point of object-format negotiation: answering `sha1` to a sha256 repository does not
/// fail, it returns an *empty* response, so the mismatch has to be caught here
/// and turned into an error.
pub fn check_object_format(ours: Kind, theirs: Option<&str>) -> crate::Result<()> {
    let Some(theirs) = theirs else {
        return Ok(());
    };
    if object_format_from_name(theirs) == Some(ours) {
        Ok(())
    } else {
        Err(crate::Error::ObjectFormatMismatch {
            ours: object_format_name(ours),
            theirs: theirs.to_owned(),
        })
    }
}

// ════════════════════════════════════════════════════════════════════════════
// The census: every serving-side policy, and what on the wire declares it
// ════════════════════════════════════════════════════════════════════════════
//
// # Why a second table
//
// [`ADVERTISED`] answers *"does everything gunnar promises have a consumer?"*.
// It cannot answer the inverse — *"does everything gunnar enforces have a
// promise?"* — because the inverse has nothing to walk. A row nothing reads is
// visible; **a policy no row names** is not there at all.
//
// That inverse has produced four wrong answers in this tree in a single day,
// and the sharpest of them, `WantPolicy` (`8f4e6e0`), was invisible in the most
// complete way available: `check_wants` enforced it on every request, no
// capability named it, so stock git refused the `want` on its **own** side and
// gunnar never received the request. No error, no log line, no failing test —
// only a user whose partial clone could not check out.
//
// So every field of every serving-side `Config`, plus every field of
// [`Offered`], is listed here with a verdict, and
// `tests/advertisement.rs::every_serving_policy_is_declared_somewhere` reads the
// **struct definitions out of the source** and refuses to pass while one of
// them is missing from this list. Adding a knob that changes what a client gets
// is therefore not something you can forget to classify; it is something that
// does not compile past the test suite.

/// How a serving-side policy reaches the peer.
///
/// The three arms are not degrees of quality — they are three different
/// contracts, and picking the wrong one is the defect. `WantPolicy` was
/// implicitly treated as [`Self::Refuses`] and was in fact silent: the refusal
/// happened inside the *client*, which is the one place a server's error can
/// never reach.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Declared {
    /// Named on the wire as this capability, so a peer can see it **before it
    /// asks** and shape its request around it.
    ///
    /// Required whenever the peer's behaviour depends on knowing: git's client
    /// will not send `filter`, `thin-pack` or a `want` for an unadvertised
    /// object unless the corresponding capability was advertised.
    As(&'static str),
    /// Not on the wire, and it does not have to be, because when it applies the
    /// peer gets an **error that says so**.
    ///
    /// This arm is a claim about something arriving, so it carries the tests
    /// that watched it arrive. "It refuses" is only a safe answer if the
    /// refusal is observable at the far end; a limit that silently truncates a
    /// pack, or that is enforced inside the client, is not this arm.
    Refuses {
        /// `<workspace-relative file>::<test fn>`, one or more, each of which
        /// asserts the peer sees the refusal. Checked to exist.
        seen_by: &'static [&'static str],
        /// What the peer actually sees.
        reason: &'static str,
    },
    /// The peer gets an error that says so — and **nothing in this repository has
    /// watched it arrive**.
    ///
    /// Split out of [`Refuses`](Self::Refuses) on 2026-09-09, because `limits` was
    /// using that arm while citing three tests at
    /// `../edda/crates/storage-git-gix/parked-gix-2/gunnar-wire-gix/tests/fetch_memory.rs`.
    /// Two things were wrong with that and only one of them was the sibling path:
    /// `parked-gix-2` is quarantined salvage, named by neither edda's `include`
    /// list nor any published tarball, so the citation resolved on exactly one
    /// machine and nowhere else. The guard "checks the citation exists", and what
    /// it was checking was one developer's disk.
    ///
    /// So the uncovered case gets its own name instead of borrowing the covered
    /// one's. The reason must say what the peer sees AND what would cover it —
    /// this arm is an admission, and an admission that does not name its way out
    /// becomes furniture.
    RefusesUncovered(&'static str),
    /// Nothing a peer can observe, and here is the argument for why.
    ///
    /// The escape hatch, and it has to cost a sentence — the same rule
    /// [`Honoured::Vacuously`] carries, for the same reason.
    Invisible(&'static str),
}

/// One field of a serving-side configuration struct, and its verdict.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Policy {
    /// The field's name, exactly as the struct spells it.
    pub field: &'static str,
    /// How it reaches the peer.
    pub declared: Declared,
}

/// A struct whose fields are serving-side policy, with a verdict on every one.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PolicySurface {
    /// The surface's name, for a failure message.
    pub surface: &'static str,
    /// Workspace-relative path to the file the struct is written in.
    pub file: &'static str,
    /// The struct's opening line, verbatim, so the guard can find it.
    pub declaration: &'static str,
    /// Every field, in any order. The guard asserts this set equals the set in
    /// the source — in **both** directions, so a stale entry fails too.
    pub fields: &'static [Policy],
}

/// Every configuration surface that decides what a peer gets.
pub const SERVER_POLICIES: &[PolicySurface] = &[
    PolicySurface {
        surface: "upload-pack",
        file: "crates/gunnar-wire/src/upload_pack.rs",
        declaration: "pub struct Config {",
        fields: &[
            Policy {
                field: "agent",
                declared: Declared::As("agent"),
            },
            Policy {
                field: "offered",
                // The struct is one field wide and that field is `filter`;
                // `Offered` has its own row below, so this is not the place the
                // claim is checked.
                declared: Declared::As("filter"),
            },
            Policy {
                field: "want_policy",
                // The defect of 2026-08-05. Before `8f4e6e0` this was enforced
                // by `check_wants` and named by nothing, and the failure landed
                // inside the client where no server error could reach.
                declared: Declared::As("allow-reachable-sha1-in-want"),
            },
            Policy {
                field: "budget",
                declared: Declared::Refuses {
                    seen_by: &["crates/gunnar-cli/tests/wire_config.rs::\
                         a_traversal_budget_of_one_refuses_a_clone_rather_than_truncating_it"],
                    reason: "the walk stops at the ceiling and the request is REFUSED, so the \
                             client's `git clone` fails with the server's message. A budget \
                             that truncated the pack instead would be a wrong answer and would \
                             belong in no arm of this enum — which is why the test asserts the \
                             clone failed AND that no repository was left behind, rather than \
                             asserting an exit code.",
                },
            },
            Policy {
                field: "limits",
                declared: Declared::RefusesUncovered(
                    "UNCOVERED HERE, and the arm now says so rather than citing a path. All \
                     three ceilings raise a named error — `ObjectTooLarge`, `RequestTooLarge`, \
                     `WalkTooLarge` — that travels to the client as an `ERR` pkt-line or on \
                     band 3, and an object over the ceiling is REFUSED and not skipped, \
                     because a pack whose closure does not hold is a corrupt clone rather \
                     than a refused one; `limits.rs` argues that at length. The tests that \
                     watched two of the three arrive \
                     (`a_blob_over_the_ceiling_is_refused_by_name`, \
                     `a_v0_have_flood_is_refused_at_the_ceiling`, \
                     `a_v2_argument_flood_is_refused_at_the_ceiling`) are PARKED in edda's \
                     `storage-git-gix/parked-gix-2/` — quarantined salvage, in no published \
                     tarball and in no clone of this repository — because their fixture \
                     opened an FsStore and the packfile engine is amputated. THE WAY OUT is \
                     a MemStore fixture, which is gix-free and already in gunnar-store: port \
                     the three tests onto it, here, and this field goes back to `Refuses` \
                     with citations that resolve for everyone. The third ceiling, \
                     `walk_bytes`, IS covered — `walk_limit.rs` reads the sentence off the \
                     wire — and is a separate field for that reason.",
                ),
            },
            Policy {
                field: "walk_pool",
                declared: Declared::Refuses {
                    seen_by: &["crates/gunnar-wire/tests/walk_limit.rs::\
                         a_full_pool_is_a_different_sentence_from_a_request_that_will_never_fit"],
                    reason: "the server-wide memory ceiling, and the one policy here whose \
                             answer depends on what OTHER clients are doing. It refuses with \
                             `WalkPoolFull`, which is deliberately a different sentence from \
                             `WalkTooLarge`: this one says the machine is busy and a retry will \
                             work, and the other says the request will never fit. A client that \
                             cannot tell them apart retries the first forever and gives up on \
                             the second. Nothing is advertised, because nothing a client could \
                             be told before it asks would still be true when it did — the \
                             answer is a property of the moment.",
                },
            },
        ],
    },
    PolicySurface {
        surface: "receive-pack",
        file: "crates/gunnar-wire/src/receive_pack.rs",
        declaration: "pub struct Config {",
        fields: &[
            Policy {
                field: "agent",
                declared: Declared::As("agent"),
            },
            Policy {
                field: "budget",
                declared: Declared::Refuses {
                    seen_by: &[
                        "crates/gunnar-wire/tests/protocol.rs::\
                         the_connectivity_budget_refuses_a_push_rather_than_accepting_it_unchecked",
                        "crates/gunnar-wire/tests/protocol.rs::\
                         a_mixed_push_refuses_only_the_reference_that_does_not_close",
                    ],
                    reason: "the connectivity walk's ceiling. Exceeding it, like failing the \
                             walk itself, turns into a per-reference `RefStatus::Rejected` \
                             carrying the reason, which the status report puts in front of \
                             `git push`, and the reference does not move. The first test sets \
                             the budget to 1 over six unreachable commits and reads the status \
                             report; the second is what says the refusal is per reference and \
                             not per push. The first was written for this entry: until then \
                             the only citation available proved the reporting mechanism and \
                             never set a budget, which is a claim standing next to its \
                             evidence rather than on it.",
                },
            },
            Policy {
                field: "pack_ceiling",
                declared: Declared::Refuses {
                    seen_by: &[
                        "crates/gunnar-wire/tests/pack_ceiling.rs::\
                         a_push_over_its_ceiling_is_cut_mid_transfer",
                        "crates/gunnar-wire/tests/pack_ceiling.rs::\
                         a_push_of_exactly_the_ceiling_lands",
                        "crates/gunnar-server/tests/pack_ceiling_store.rs::\
                         a_cut_push_stores_nothing_and_leaves_the_repository_usable",
                        "crates/gunnar-wire/tests/concurrent_pack_ceiling.rs::\
                         simultaneous_pushes_are_not_each_admitted_against_the_same_measurement",
                    ],
                    reason: "the byte ceiling a push is held to WHILE it arrives, and the one \
                             policy here that a client cannot be told before it asks — because \
                             the thing it bounds is not knowable before it asks. A pack header \
                             carries an object count, not a byte count, so neither side knows \
                             the size of a push at negotiation time; there is no number to put \
                             in a capability and nothing a client could do differently if there \
                             were. So it refuses, and the refusal is the whole surface: the \
                             read is cut one byte past the ceiling, `unpack <reason>` carries \
                             the setter's own sentence (which names the ceiling and why \
                             — never the indexer's opinion of a truncated stream), \
                             and every reference is reported `ng`. The first citation is the \
                             one that matters and it asserts BYTES, not a verdict: a server \
                             that read the whole 400 GiB and then refused would satisfy any \
                             assertion about the report and is a different bug. The second is \
                             the boundary — a push of exactly the ceiling LANDS — and the \
                             third is the same cut against the real znippy engine, which is \
                             where `nothing was stored` costs something to be true. The \
                             fourth is the number's OTHER half, added 2026-09-08: the \
                             ceiling is one namespace's room and not one connection's, so \
                             eight pushes that measured it together spend it once between \
                             them (`PackCeiling::ledger`). Without it three green citations \
                             stood over a limit N simultaneous pushes could spend N times.",
                },
            },
            Policy {
                field: "notice",
                declared: Declared::Invisible(
                    "It is not a policy: the push it rides on is ALLOWED. `notice` is one \
                     human-readable line put on the progress band (`remote: …`) beside a \
                     report that is otherwise byte-for-byte what it would have been — the \
                     metered appliance's `plan expired <date>; renew at <url>` for a tenant in \
                     Grace. Nothing about the request has to be shaped around knowing it, so \
                     `As` would be a capability no client would read; nothing is refused, so \
                     `Refuses` would have no arrival to cite. The three things that make \
                     `Invisible` the honest arm rather than the convenient one, all readable \
                     at `receive_pack.rs:1445-1455`: it is sent ONLY inside the `if sideband` \
                     arm, so a client that negotiated no sideband gets the bare report and \
                     never a line where the report should be; it is framed separately on band \
                     2, so a client that ignores band 2 parses the report untouched; and it is \
                     written BEFORE the report on band 1, which is presentation order for a \
                     person and nothing a parser depends on. Its absence is exactly the \
                     behaviour that shipped before it existed. If it ever gates, blocks or \
                     shortens a push, it stops being this arm.",
                ),
            },
        ],
    },
    PolicySurface {
        surface: "what is on offer",
        file: "crates/gunnar-wire/src/caps.rs",
        declaration: "pub struct Offered {",
        fields: &[Policy {
            field: "filter",
            declared: Declared::As("filter"),
        }],
    },
];