Skip to main content

Module fetch_pack

Module fetch_pack 

Source
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 module46
gunnar-wire after it46 — the same 46, and the same three gix-*: gix-hash, gix-trace, gix-features
gix-protocol standalone184
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.
FetchPack
The client side of git-upload-pack, configured for one fetch.
FetchReport
What one fetch did.
RefUpdatePlan
One reference this fetch would move, and what it would move it from.

Traits§

FetchProgress
Where a transfer reports itself while it is still running.
LocalRefs
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-remote that takes the remote’s object format instead of insisting on one (2026-10-18).
mirror
The ref plan a mirror uses: refs/heads/* and refs/tags/*.