Skip to content
Remote Protocol: How git fetch and git push Work
step 1/1

Reading — step 1 of 1

Read

~3 min readBranching, Diff & Merge

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:

ChannelContent
1pack data
2progress messages
3fatal 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 reject
  • update — runs once per ref
  • post-receive — fires after refs are updated (the CI trigger)

Why this matters

Even if you never implement the wire protocol, knowing that:

  1. fetch / push are a refs-advertisement + want/have + packfile dance,
  2. all your commits are in objects, not in something special,
  3. 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…