How does the Git network protocol work, including smart HTTP, SSH, and the v2 protocol, and how do packfile negotiation and server-side hooks control what gets pushed and fetched?
Quick Answer
Git transfers data over smart HTTP (stateless, firewall-friendly), SSH (authenticated, efficient), or the Git protocol (fast, unauthenticated). Protocol v2 adds capability negotiation and server-side filtering for partial clones. Pack negotiation determines which objects to transfer by exchanging have/want refs. Server-side hooks (pre-receive, update, post-receive) enforce policies before refs are updated.
Detailed Answer
Think of the Git protocol like a postal service negotiation. When you want to receive packages (fetch), you call the warehouse (remote) and say 'I already have packages A, B, C' (have lines). The warehouse responds with 'I have packages D, E, F that you are missing' (want lines). They agree on the minimum set of packages to ship, pack them into a single crate (packfile), and send it over. When you send packages (push), the warehouse inspector (pre-receive hook) checks the crate before accepting it, potentially rejecting packages that violate policies.
Git supports three transport protocols. The smart HTTP protocol uses standard HTTPS with two endpoints: /info/refs?service=git-upload-pack (for fetch, server lists its refs) and /git-upload-pack (for the actual pack transfer). It is stateless, works through corporate firewalls and proxies, and supports authentication via HTTP headers (tokens, Basic auth). The SSH protocol establishes an encrypted connection and runs git-upload-pack or git-receive-pack on the server. It is the most common for authenticated operations, leveraging existing SSH key infrastructure. The native Git protocol (git:// on port 9418) is the fastest but provides no authentication or encryption, making it suitable only for public read-only access.
Protocol v2 (default since Git 2.26) redesigns the initial handshake. In v1, the server immediately sends its entire ref list (potentially thousands of refs for large repositories like the Linux kernel) regardless of what the client needs. In v2, the client sends a capability list first, and the server responds with only the requested information. The ls-refs command replaces the full ref advertisement, letting clients request specific ref patterns (e.g., only refs/heads/main). The fetch command supports server-side filtering (--filter for partial clone), sideband multiplexing for progress information, and incremental ref negotiation. This reduces the initial handshake from megabytes to kilobytes for repositories with many branches and tags.
Pack negotiation is the algorithm that determines which objects to transfer during fetch. The client sends 'want <sha>' lines for the refs it wants to update and 'have <sha>' lines for commits it already has. The server uses these to compute the minimal set of objects the client needs, packs them using delta compression, and sends the packfile. The negotiation algorithm is iterative: the client sends batches of 'have' lines, the server responds with ACK for recognized commits, and the client narrows its 'have' list. This converges quickly for repositories where the client and server share recent history. Server-side hooks intercept push operations: pre-receive runs once before any refs are updated (receives old-SHA new-SHA ref-name on stdin for each ref), update runs per-ref (allowing selective rejection), and post-receive runs after all refs are updated (for notifications and CI triggers). These hooks are the ultimate enforcement mechanism because they run on the server and cannot be bypassed by clients.
A critical gotcha: HTTP-based cloning through corporate proxies may fail if the proxy buffers the response and the packfile exceeds the buffer size. Configure http.postBuffer (e.g., git config http.postBuffer 524288000 for 500MB) to increase the client-side buffer. Another trap: protocol v1 servers send their entire ref list on every fetch, which can take 10+ seconds for repositories with 50,000+ refs. Migrating to a v2-capable server dramatically reduces this overhead. Also, pre-receive hooks that are too slow (running full CI checks synchronously) will cause push timeouts. Keep pre-receive hooks fast (under 10 seconds) and delegate heavy validation to asynchronous CI pipelines triggered by post-receive. Finally, Git's SSH transport uses the user's SSH agent or key files, but many CI systems require explicit SSH key configuration or deploy tokens for HTTPS.