Reading — step 1 of 1
Read
Remote Protocol: How git fetch and git push Work
Local git is now familiar. But what happens when you git push origin main?
Git speaks a custom protocol over HTTP, SSH, or its native port — all
exchanging the same conceptual messages: which refs do you have, here's a
packfile of what's missing.
Smart HTTP — the dominant modern transport
For git fetch:
1. GET /info/refs?service=git-upload-pack
(server: list of refs + capabilities)
2. POST /git-upload-pack
(client: "I want these SHAs, I already have these SHAs")
(server: packfile streamed back)
For git push:
1. GET /info/refs?service=git-receive-pack
(server: current refs)
2. POST /git-receive-pack
(client: "update ref X from oldSHA to newSHA" + packfile)
(server: per-ref ok / failure responses)
Step 1: Ref advertisement
The first request gets the server's current state. Response (pkt-line format):
001e# service=git-upload-pack\n
0000
0044<sha> HEAD\0multi_ack thin-pack side-band ofs-delta agent=git/2.40\n
003f<sha> refs/heads/main\n
003e<sha> refs/heads/dev\n
0000
Each pkt-line is <4-hex-len><data>, terminated by 0000 (a "flush
packet"). The capabilities after \0 advertise extensions both sides
understand: thin-pack (deltas may reference non-pack objects), side-band
(multiplex progress/error/data), ofs-delta (use offset deltas), etc.
Step 2: Want/Have negotiation (fetch)
0032want <sha-A> multi_ack thin-pack ofs-delta\n
0030want <sha-B>\n
0000
0032have <sha-X>\n
0032have <sha-Y>\n
0009done\n
The client says: "I want A and B; I already have X and Y." The server walks the commit graph to find which objects the client is missing, then streams a packfile containing exactly those.
For first-time clone: no have lines — server sends a pack with
everything reachable from each want.
Step 3: The packfile transfer
After negotiation, the server sends a packfile (the format from the
previous lesson) over the same connection. With side-band enabled,
the stream is multiplexed:
| Channel | Content |
|---|---|
| 1 | pack data |
| 2 | progress messages |
| 3 | fatal error |
The client appends incoming pack bytes to a temp file, then runs
git index-pack to validate it and build a new .idx.
Push: report-status
client → server: pkt-line per ref-update + packfile of new objects
e.g., "0000000000... abc123def... refs/heads/feature"
(zeros mean "create this ref"; if newSHA is zeros, "delete")
server → client: "unpack ok\n"
per-ref: "ok refs/heads/feature\n" or "ng refs/heads/X reason"
The server validates the pack, checks that fast-forward isn't being
violated (unless -f), updates refs atomically, and reports back.
Hooks fire on the server too
When push lands:
pre-receive— sees the whole batch, can rejectupdate— runs once per refpost-receive— fires after refs are updated (the CI trigger)
Why this matters
Even if you never implement the wire protocol, knowing that:
- fetch / push are a refs-advertisement + want/have + packfile dance,
- all your commits are in objects, not in something special,
- servers are mostly the same as your local
.git/,
… explains why git push --force is just "overwrite the ref pointer,"
why git fetch doesn't move your branches (it only updates remote-tracking
refs), and why a server-side repo and a working clone look almost the same
on disk.
Real implementations
- libgit2 has a clean smart-HTTP client (~1500 LoC).
- git-http-backend is the CGI server in the canonical git distribution.
- Gitea / Gitlab wrap git-receive-pack with auth + hooks.
For this course we stop here — you understand the moving parts. Implementing the bytes is a follow-up project (and a great resume line).
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…