DEV Community

Syed Anzar
Syed Anzar

Posted on

What Actually Happens When You Run `git push`

What Actually Happens When You Run git push

You type git push origin main and your code appears on the remote. But between those two words lies a complex protocol exchange that most developers never see -- and even fewer understand.

This article peels back the layers. We'll walk through the git push protocol step by step, from the initial handshake to the final ref update, exploring what the code actually does under the hood.


The High-Level Overview

When you run git push <remote> <ref>:

  1. Discovery -- The client asks the server what refs it has and what objects are available
  2. Negotiation -- The client and server figure out the minimal data needed to update the desired refs
  3. Transfer -- The client streams the missing objects to the server in packfile format
  4. Update -- The server validates and writes the new refs

This is the "smart protocol" -- specifically the receive-pack/send-pack exchange used over SSH, Git, and HTTP transports.


Step 1: Reference Discovery

The client (send-pack) connects to the server (receive-pack) and immediately receives a listing of the server's current state.

The server responds with a pkt-line stream, sorted by name in C locale order. Each line contains:

  • The object ID (SHA-1) the reference currently points to
  • The reference name

Crucial detail: If HEAD is a valid ref, it appears as the first advertised ref. If HEAD is invalid, it's excluded entirely.

The stream also includes capabilities -- features the server supports, such as:

  • report-status -- server will report update status afterward
  • delete-refs -- client can delete refs
  • ofs-delta -- use OFS (offset) delta compression in the packfile
  • atomic -- atomic transaction for ref updates
  • push-options -- custom push options for hooks

Why this matters: The client uses these capabilities to determine what commands it can send and what format the packfile will use. Without report-status, the client won't know which refs succeeded or failed.

Real-world consequence: If you've ever wondered why git push sometimes gives you a summary of what was updated and sometimes doesn't -- it's because the server advertised (or didn't advertise) the report-status capability.


Step 2: Negotiation -- What Objects Are Missing?

Now the client knows the server's current state. It needs to determine: which objects does the server lack that I need to send?

The client sends a series of want lines listing the object IDs it wants the server to have. It also sends have lines listing objects it already possesses, so the server can construct a minimal packfile containing only the missing objects.

The negotiation works like this:

Client                          Server
  |  want <missing-object-1>    |
  |  want <missing-object-2>    |
  |  have <object-I-have-1>       |
  |  have <object-I-have-2>       |
  |  flush-pkt                    |
  |----------------------------->|
  |  ACK <common-base>            |
  |<-----------------------------|
  |  PACK <packfile>              |
  |<-----------------------------|
Enter fullscreen mode Exit fullscreen mode

The server responds with an ACK (acknowledging common bases) or NAK (no common base found). If the server has all the objects already, it may send a done line and terminate early.

The deepen option: The client can send a deepen line specifying how many commits of history it wants. A depth of 0 means "I want everything." A depth of N means "just the last N commits, plus objects needed to complete them." This enables shallow clones over the wire.

OFS-delta compression: If the server advertised ofs-delta, the packfile will use OFS (offset) delta encoding, which is more efficient for series of commits where each commit builds on the previous one. The offset is stored as a variable-length integer, reducing overhead compared to full SHA-1 deltas.


---

## Step 3: Packfile Transfer

Once negotiation is complete, the server streams a `PACK` file containing the objects the client needs.

The packfile format:

Enter fullscreen mode Exit fullscreen mode

PACK


...


Each object in the packfile includes:

- **Type** (commit, tree, blob, tag)
- **Size** (variable-length encoding)
- **Compressed data** (zlib-compressed)

The packfile contains the minimum set of objects needed to reconstruct the requested refs -- no more, no less. This is why `git push` can be much more efficient than transferring an entire repository.

> **Why packs, not loose objects?** Git stores objects as either loose files (`<object-id>` in `.git/objects/pack/`) or in packfiles (compressed archives). Packfiles are used for network transfer because they can contain deltas (storing only the differences between objects) rather than full objects, dramatically reducing bandwidth.

> **Thin packs:** For efficiency, the server may send a "thin pack" -- a packfile that doesn't include all necessary base objects. The client must then request those missing bases separately. This is controlled by the `--thin` flag to `git push`. Thin packs are useful when the client already has most objects locally and only needs a few additional ones.

> **Pack checksum:** After receiving the packfile, the client verifies its checksum. If the packfile is corrupted, the entire push fails and the client must retry.

Step 4: Reference Update and Status

After the packfile is received and validated, the server processes the reference update requests.

The client sends a list of update commands, each specifying:

  • The old object ID (what the ref currently points to, or all-zero for new refs)
  • The new object ID (what the ref should point to after the push)
  • The ref name

The server validates each update:

  1. Fast-forward check: For branch refs, the server verifies the new commit is a descendant of the old commit (unless --force was used)
  2. Update hooks: Server-side pre-receive and post-receive hooks can approve or reject the update
  3. Non-fast-forward rejection: By default, the server rejects updates that would lose commits

If report-status was advertised, the server sends a status report:

ok <refname>          # update succeeded
ng <refname> <error> # update failed

The report-status-v2 capability extends this to include information about references rewritten by proc-receive hooks, including the new name, new-oid, and old-oids for each updated ref.

The atomic flag: If the client sends --atomic, the server uses a transaction -- either all refs update successfully or none do. If any ref fails, the entire push is rolled back.

Force-with-lease: A safer alternative to --force, --force-with-lease only forces the update if the remote ref still points to the expected old commit. This prevents the "lost commits" scenario where someone else pushed between your fetch and your push.


---

## Under the Hood: The Protocol in Detail

For those who want to see the raw protocol, you can observe it with `git push --verbose` or by capturing the network traffic:


bash

See the pkt-line protocol exchange

GIT_TRACE=1 git push origin main


Or using `nc` (netcat) to connect directly to a Git port:


bash

Connect to a Git server on port 9418

echo -e -n "0039git-upload-pack /schacon/gitbook.git\0host=example.com\0" | nc -v example.com 9418


The output shows the pkt-line stream -- length-prefixed lines (the `00XX` prefix indicates the line length in bytes, including the prefix itself).

> **Pkt-line format:** Each message is sent as a line prefixed with a 4-digit hex length (padded with zeros). For example, `0039` means the following line is 0x39 = 57 bytes long. A special `0000` line signals the end of a batch.

Common Mistakes and Gotchas

1. Assuming git push always does a fast-forward

By default, git push refuses to update a ref if the new commit isn't a descendant of the old one. This protects against overwriting someone else's work. Use --force or --force-with-lease only when you know what you're doing.

2. Not understanding the difference between git push origin and git push origin main

Without a refspec, git push origin uses the push.default configuration (typically matching or upstream). With matching, Git pushes all local branches that have a remote counterpart of the same name. With upstream, it pushes only the configured upstream branch.

3. Thinking git push transfers all objects

The protocol is designed to transfer only the minimum objects needed. If the server already has most of the objects in a thick pack, the packfile may be very small -- sometimes even empty (e.g., when creating a new branch that points to an existing commit).

4. Confusing git push with git fetch

git fetch downloads objects and updates remote-tracking refs (e.g., origin/main) without touching local refs. git push uploads objects and updates the remote's actual refs. You can fetch without pushing, and you can push without recently fetching.

5. Not realizing the protocol differs by transport

The git push protocol is the same over SSH, Git, HTTP, and HTTPS -- but the handshaking differs. Over SSH, receive-pack is invoked as a remote command. Over HTTP, the exchange is wrapped in HTTP requests (POST for the packfile, GET for ref discovery). This is why git push may behave differently depending on your remote URL.


Practical Takeaways

  1. git push is a protocol exchange, not a single operation. It involves discovery, negotiation, transfer, and update.

  2. The server tells the client what it has (ref advertisement), and the client asks for only what's missing (want/have negotiation).

  3. The packfile contains a minimal set of objects, compressed with deltas, to reconstruct the requested refs.

  4. Status reporting depends on the server advertising report-status -- without it, you get no summary of what succeeded/failed.

  5. --atomic provides an all-or-nothing update, while --force-with-lease is a safer alternative to --force.

  6. The protocol is transport-agnostic -- the same logical exchange happens over SSH, Git, HTTP, and HTTPS, though the wiring differs.


Further Reading


This article synthesizes from the official Git documentation (git-scm.com), which is the authoritative source for Git protocol behavior. All protocol details trace back to the source code and specifications maintained by the Git project.

Top comments (0)