Expand description
The other chair at the table: the client seat of git fetch, protocol
v2, with no gix and no filesystem.
crate::upload_pack answers a fetch. This asks one. The two are the same
conversation read from opposite ends, and they share every piece of it that
can be shared: the pkt-line framer ([crate::wire_io]), the capability and
argument tables (crate::caps — the same tables the server parses
against, so a client cannot send an argument the server would reject), the
advertised-ref vocabulary (AdvertisedRef), and the packfile sink
(PackIngest) that every git push already lands through.
§Why this is written here and not delegated to gix-protocol
MEASURED 2026-09-08 on this workspace, with
cargo tree -p gunnar-wire -e normal --prefix none | sed 's/ v[0-9].*//' | sort -u | wc -l
(distinct crate names; the raw line count is 51 because five crates resolve
at two versions):
| crates in the normal graph | |
|---|---|
gunnar-wire before this module | 46 |
gunnar-wire after it | 46 — the same 46, and the same three gix-*: gix-hash, gix-trace, gix-features |
gix-protocol standalone | 184 |
| what delegating would have added | +128, and HTTP is a further feature (http-client-curl / http-client-reqwest) |
There is zero curl in the constellation today, and this module keeps it
at zero. The only gix crate it touches is gix-hash — a 32-byte array and
a two-variant enum, which is gunnar’s object-id vocabulary and was already in
the graph.
The count is not the whole argument, and on its own it would be a weak one.
The other half is that gix’s porcelain does not fit the machine this runs on:
gunnar-client’s fetch.rs names gix:: 60 times — ten of them
gix::Repository — and needs a .git directory and a worktree. The appliance has
neither — it runs the znippy engine (Arrow-IPC, objects and refs) as
PID 1 with no filesystem underneath. A fetch that must first materialise a
repository on disk cannot run there at all, at any dependency count.
And the work that delegation would have saved is work this crate had already
done: upload_pack.rs answers a fetch, negotiate.rs runs the have/want
dance, receive_pack.rs accepts a pack, refs.rs advertises, wire_io.rs
frames. What was missing was the other seat, not the protocol.
§This module does not store objects, and it does not write references
Two deliberate holes, and both are the same principle.
Objects go to a caller-supplied PackIngest — the identical trait
receive_pack hands a pushed pack to, because a fetched pack and a
pushed pack are the same bytes. znippy implements it once and both
directions land through it. Writing a second sink here would have been a
second storage authority, and two authorities disagree (one implementation).
References are planned and not applied: FetchReport::updates says
what would move, and the caller — gunnar-server, holding the store and the
policy gates — decides whether it moves. A fetch that wrote refs from inside
this crate would apply a mirror’s whole plan with no gate, no atomicity
across the batch, and no way for an operator to see it first.
§What it streams, and what it therefore never holds
The packfile is handed to PackIngest::ingest as a &mut dyn BufRead that
is demultiplexed from the server’s sideband one pkt-line at a time. The
most this module ever holds of a packfile is one 65 515-byte sideband
payload, whatever the size of the repository. A mirror of a large repository
that required the pack in RAM would be a mirror that cannot run on the
appliance, which is the only place this is going.
§Two rounds, at most, and why that is enough
Round one sends every want and every local tip as a have, and reads the
acknowledgments section. If the server says ready the packfile follows in
the same response and there is no round two. Otherwise round two repeats the
request with done, and the server must answer with a pack.
That is the simple, correct form, deliberately: git’s client walks its own
history backwards over many rounds because it wants to find a near cut
point without sending its entire ref list. gunnar’s have-source is a mirror’s
store, whose tips are already the complete offer — there is no round three to
send, because there is nothing left to say. Multi-round negotiation is a
measurement away, not a design away; nothing here would have to change shape
to grow it, only FetchPack::fetch’s loop.
§Protocol v2 only
Named, not implied. The server half here speaks v2 and a v0 fallback,
because stock git negotiates v0 whenever GIT_PROTOCOL does not reach the
server process and a server that could not answer those clients would simply
not work. A client has the opposite freedom: it chooses. Every remote
gunnar fetches from — gunnar itself, GitHub, Codeberg — speaks v2, so a v0
client path would be a second implementation of this file that nothing runs.
If a remote is ever found that refuses v2, this is where the fallback goes.
Structs§
- Advertisement
- The reference namespace a remote advertised, and what it said it is.
- Fetch
Pack - The client side of
git-upload-pack, configured for one fetch. - Fetch
Report - What one fetch did.
- RefUpdate
Plan - One reference this fetch would move, and what it would move it from.
Traits§
- Fetch
Progress - Where a transfer reports itself while it is still running.
- Local
Refs - The local half of a fetch: what this store already has.
Functions§
- ls_
remote git ls-remote, with no repository and no gix.- ls_
remote_ any git ls-remotethat takes the remote’s object format instead of insisting on one (2026-10-18).- mirror
- The ref plan a mirror uses:
refs/heads/*andrefs/tags/*.