Expand description
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 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 and
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
use gunnar_wire::{ProtocolVersion, upload_pack::UploadPack};
let version = ProtocolVersion::from_git_protocol(std::env::var("GIT_PROTOCOL").ok().as_deref());
UploadPack::new(git, hash_kind).serve(version, input, output)?;Re-exports§
pub use fetch_pack::ls_remote;pub use fetch_pack::ls_remote_any;pub use fetch_pack::Advertisement;pub use fetch_pack::FetchPack;pub use fetch_pack::FetchProgress;pub use fetch_pack::FetchReport;pub use fetch_pack::LocalRefs;pub use fetch_pack::RefUpdatePlan;pub use filter::Filter;pub use limits::Limits;pub use limits::WalkPool;pub use limits::DEFAULT_OBJECT_LIMIT_BYTES;pub use limits::DEFAULT_REQUEST_LIMIT_BYTES;pub use limits::DEFAULT_WALK_LIMIT_BYTES;pub use limits::WALK_BYTES_PER_OBJECT;pub use pack::IndexedPack;pub use pack::PackVersion;pub use pack::WriteOutcome;pub use receive_pack::PackCeiling;pub use receive_pack::PackIngest;pub use receive_pack::PackLedger;pub use receive_pack::PackReceipt;pub use receive_pack::Preamble;pub use receive_pack::PushFacts;pub use receive_pack::PushGate;pub use receive_pack::PushOutcome;pub use receive_pack::ReceivePack;pub use receive_pack::RefCommand;pub use receive_pack::RefStatus;pub use receive_pack::GIT_TARGET;pub use refs::RefSnapshot;pub use upload_pack::FetchRequest;pub use upload_pack::LsRefsRequest;pub use upload_pack::UploadPack;pub use upload_pack::UploadSummary;pub use upload_pack::WantPolicy;
Modules§
- caps
- Capabilities, and the table that keeps the advertisement honest.
- fetch_
pack - The other chair at the table: the client seat of
git fetch, protocol v2, with nogixand no filesystem. - filter
- Partial clone, via the
filter <spec>argument. - limits
- The fetch path’s allocation ceilings.
- pack
- The pack-shaped values the protocol carries. Vocabulary, not machinery.
- receive_
pack git-receive-pack, i.e. serving push. Protocol v0, which is the only protocol push has ever had.- refs
- One consistent view of the reference namespace, and how a symbolic reference is followed inside it.
- shallow
- Shallow and deepening:
--depth,--shallow-since,--shallow-exclude. - upload_
pack git-upload-pack: serving fetch and clone.
Structs§
- Negotiation
- What one round of negotiation concluded.
Enums§
- Error
- Everything
gunnar-wirecan fail with. - Protocol
Version - Which version of the smart protocol a connection speaks.
- Session
- How much of a conversation one call to a state machine gets to see.
Constants§
- CONNECTIVITY_
BUDGET - The ceiling on objects visited by
receive-pack’s connectivity check. - DEFAULT_
BUDGET - The default ceiling on object ids one traversal on the serve side may hold at once.
- TRANSPORT_
BUFFER_ BYTES - How much a transport must buffer under these writers, and why one sideband packet is the unit that decides it.
Functions§
- err_
packet - Send an
ERR <message>pkt-line, the way a v0 server reports a fatal condition before any sideband exists. Public for the transport’s first-repository door, which refuses in the client’s own protocol after the advertisement and before any sideband. - objects_
to_ send - The objects one push must carry: everything reachable from
wantthat is not reachable fromhave, in this store’s ids.
Type Aliases§
- Result
- This crate’s result type.