Skip to main content

Crate gunnar_wire

Crate gunnar_wire 

Source
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

protocolnotes
upload_packv2, with a v0 fallbackls-refs, fetch, shallow, partial-clone filters, ref-in-want
receive_packv0the only protocol push has; atomic, push-options, report-status-v2
fetch_packv2the 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 no gix and 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-wire can fail with.
ProtocolVersion
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 want that is not reachable from have, in this store’s ids.

Type Aliases§

Result
This crate’s result type.