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
//! **The git smart protocol, server side.** `upload-pack` and `receive-pack`
//! as state machines with no transport attached.
//!
//! This is the crate gunnar exists to have. `nornir-git` already speaks the
//! *client* half of git-over-SSH against codeberg and GitHub — reused, not written; what
//! does not exist anywhere in the constellation is a server that **answers**.
//!
//! # Transport-agnostic, on purpose
//!
//! Every entry point takes `&mut dyn BufRead` and `&mut dyn Write`. There is no
//! socket, no async runtime, no SSH here. `gunnar-ssh` bridges a russh channel
//! into those two traits; a test bridges a pipe, and `git`'s own `ext::`
//! transport bridges a subprocess's stdio. The state machine cannot tell them
//! apart, which is what makes it testable against the real `git` binary without
//! an SSH server existing yet.
//!
//! # What is implemented
//!
//! | | protocol | notes |
//! |---|---|---|
//! | [`upload_pack`] | **v2**, with a **v0 fallback** | `ls-refs`, `fetch`, shallow, partial-clone filters, `ref-in-want` |
//! | [`receive_pack`] | **v0** | the only protocol push has; `atomic`, `push-options`, `report-status-v2` |
//! | [`fetch_pack`] | **v2** | the *client* seat of a fetch, and [`ls_remote`] |
//!
//! [`fetch_pack`] is the one that is not a server. It is here because it is the
//! same conversation: it reuses this crate's framer, its capability tables, its
//! advertised-ref vocabulary and — for the pack it receives — the very
//! [`PackIngest`](receive_pack::PackIngest) a push already lands through, since
//! a fetched pack and a pushed pack are the same bytes. Writing it against
//! `gix-protocol` instead would have cost the appliance graph **+128 crates**
//! and a curl or reqwest HTTP client; the module doc has the measurement.
//!
//! The v0 fallback is not a formality. `git` 2.53 negotiates v0 whenever
//! `GIT_PROTOCOL` does not reach the server process. Measured 2026-08-03 over
//! `ext::`, where git passes `GIT_EXT_SERVICE` but not `GIT_PROTOCOL`. A server
//! without a v0 path would simply not answer those clients.
//!
//! # Two things this crate is careful about
//!
//! **The advertisement is generated from the parser's own table.** A v2 client
//! will not send `filter` unless `filter` is advertised, so a server that
//! parses more than it advertises has support that never activates and looks
//! exactly like working code. [`caps`] makes that unexpressible rather than
//! merely tested.
//!
//! **`object-format` comes from the repository's own hash kind**, carried into
//! [`UploadPack::new`](upload_pack::UploadPack::new) and
//! [`ReceivePack::new`](receive_pack::ReceivePack::new) from
//! [`gunnar_store::RepoStore::hash_kind`], never from a default.
//! Answering `sha1` to a sha256 repository yields an *empty* response rather
//! than an error. That is the bug documented in `nornir-git`'s `ssh.rs` that
//! cost a session, so a mismatch is rejected loudly here, and the interop tests run
//! the whole suite against a sha256 fixture rather than inferring it from a
//! sha1 green.
//!
//! # Example
//!
//! ```no_run
//! use gunnar_wire::{ProtocolVersion, upload_pack::UploadPack};
//! # fn run(git: &dyn gunnar_store::git_storage_trait::GitServe,
//! # hash_kind: gunnar_store::gix_hash::Kind,
//! # input: &mut dyn std::io::BufRead, output: &mut dyn std::io::Write)
//! # -> gunnar_wire::Result<()> {
//! let version = ProtocolVersion::from_git_protocol(std::env::var("GIT_PROTOCOL").ok().as_deref());
//! UploadPack::new(git, hash_kind).serve(version, input, output)?;
//! # Ok(()) }
//! ```
// So `tests/common/mod.rs` can spell this crate's own types **one way** in both
// contexts it is compiled in: as an external crate by every integration test,
// and — through the `#[path]` below — inside the library's unit tests, where
// `gunnar_wire::` would otherwise not resolve. That fixture used to dodge the
// problem by being generic over `E: From<gunnar_pack::Error>` so it never had
// to name a `gunnar-wire` type at all, and that dodge is precisely what kept
// `gunnar-pack` a normal dependency of this crate. One line here, and the
// fixture can say what it means. See `src/pack.rs`.
extern crate self as gunnar_wire;
/// The byte-level suites' repository fixture, borrowed by the unit tests.
///
/// `#[path]` into `tests/` rather than a second copy under `src/`: a fixture
/// builder that could drift from the one every integration test in this crate
/// runs against is exactly the duplication one implementation forbids, and the
/// unit tests that assert on traversal cost need the same histories the
/// protocol tests use. Declared here rather than beside its user because
/// `#[path]` resolves through real directories, and `src/graph/` is not one.
pub use ;
pub use ;
pub use Filter;
/// How much of a conversation one call to a state machine gets to see.
///
/// # Why this exists, and why it is not a second implementation
///
/// Over SSH the client and the server hold **one connection** for the whole
/// exchange: the advertisement, every `have`/`want` round and the packfile all
/// travel down it, and the server's negotiation state lives in the stack frame
/// that is serving it.
///
/// Smart HTTP has no such frame. Each `POST` is an independent request that may
/// land on a different worker, so the client re-sends its whole request on
/// every round and the server must answer **one round and stop**. That is git's
/// `--stateless-rpc`, and it is the one behavioural difference between the two
/// transports. Everything else — the framing, `ls-refs`, the negotiation, the
/// pack writer — is identical, which is why `gunnar-http` wraps these state
/// machines instead of restating them (one implementation).
///
/// The difference is exactly three things:
///
/// | | [`Connected`](Session::Connected) | [`StatelessRpc`](Session::StatelessRpc) |
/// |---|---|---|
/// | who writes the advertisement | this crate, inline | `GET /info/refs`, a separate call to [`upload_pack::UploadPack::advertise`] |
/// | v2 commands per call | until the client stops | exactly one |
/// | v0 `have` rounds per call | until the client sends `done` | one; a round with no `done` answers `ACK`/`NAK` and returns |
///
/// A `Session` is **not** a protocol version and does not interact with one:
/// both v0 and v2 ride both transports.
pub use ;
pub use ;
pub use Negotiation;
pub use ;
pub use ;
pub use RefSnapshot;
pub use ;
pub use ;
/// Which version of the smart protocol a connection speaks.
///
/// The server does not choose. Over SSH the client asks by sending
/// `GIT_PROTOCOL=version=2` as an environment variable, and a client that
/// cannot, or a transport that drops it, gets v0. `gunnar-ssh` reads the
/// variable off the channel and passes the answer in.