Overview
Git uses sophisticated protocols to efficiently transfer packfile data between repositories. These protocols minimize data transfer by negotiating what objects the client already has and sending only the necessary differences.
There are two protocol sets: one for fetching data (upload-pack/fetch-pack) and one for pushing (receive-pack/send-pack).
Supported Transports
Git supports four primary transports:
SSH Secure, authenticated transport using SSH remote execution
Git Protocol Fast, unauthenticated daemon protocol on port 9418
HTTP/HTTPS Widely compatible, firewall-friendly transport (see gitprotocol-http)
File Local filesystem access using pipes
All Git protocols use the pkt-line format for framing:
pkt-line = 4-byte-hex-length + data
Examples:
"0006a\n" → length 6, data "a\n"
"0000" → flush packet (signals end of section)
"0009done\n" → length 9, data "done\n"
Special Packets
Flush packet: 0000 - Marks end of a data section
Error packet: PKT-LINE("ERR " + message) - Terminates the connection
Delimiter packet: 0001 - Separates sections (protocol v2)
Python pkt-line
Node.js pkt-line
def write_pkt_line ( data ):
if data is None :
return b '0000' # flush packet
length = len (data) + 4
return f ' { length :04x} ' .encode() + data
def read_pkt_line ( stream ):
length_hex = stream.read( 4 )
if length_hex == b '0000' :
return None # flush packet
length = int (length_hex, 16 )
data = stream.read(length - 4 )
return data
Transport Initiation
Git Transport
The Git daemon expects:
git-proto-request = command SP pathname NUL
[ host-parameter NUL ]
[ NUL extra-parameters ]
Example:
0033git-upload-pack /project.git\0host=myserver.com\0
With extra parameters (e.g., protocol version):
003egit-upload-pack /project.git\0host=myserver.com\0\0version=1\0
SSH Transport
SSH transport invokes commands directly:
# Absolute path (ssh:// URLs)
ssh user@example.com "git-upload-pack '/project.git'"
# Relative to home (scp-style URLs)
ssh user@example.com "git-upload-pack 'project.git'"
# Home directory expansion
ssh user@example.com "git-upload-pack '~alice/project.git'"
Extra parameters can be sent via the GIT_PROTOCOL environment variable if ssh.variant supports it.
File Transport
Runs upload-pack or receive-pack locally via pipe:
git-upload-pack /path/to/repo.git
Fetching Data: Reference Discovery
The server immediately responds with available references:
$ echo -e -n "0045git-upload-pack /repo.git\0host=example.com\0\0version=1\0" | \
nc -v example.com 9418
000eversion 1
00887217a7c7e582c46cec22a130adf4b9d7d950fba0 HEAD\0multi_ack thin-pack
side-band side-band-64k ofs-delta shallow no-progress include-tag
00441d3fcd5ced445d1abc402225c0b8a1299641f497 refs/heads/integration
003f7217a7c7e582c46cec22a130adf4b9d7d950fba0 refs/heads/master
003cb88d2441cac0977faf98efc80305012112238d9d refs/tags/v0.9
003c525128480b96c89e6418b1e40909bf6c5b2d580f refs/tags/v1.0
003fe92df48743b7bc7d26bcaabfddde0a1e20cae47c refs/tags/v1.0^{}
0000
advertised-refs = *1("version 1")
(no-refs / list-of-refs)
*shallow
flush-pkt
first-ref = PKT-LINE(obj-id SP refname NUL capability-list)
other-ref = PKT-LINE(obj-id SP refname ["^{}"])
Refs MUST be sorted by name (C locale)
HEAD appears first if valid
Capabilities appear after NUL on first ref
Peeled tags (^) immediately follow their tag
Common Capabilities
multi_ack / multi_ack_detailed
Enables richer ACK responses during negotiation to find common commits faster.
Allows sending packfiles that reference objects not included (assumes client has them).
side-band / side-band-64k
Multiplexes packfile data with progress and error messages on separate channels.
Permits offset-based delta encoding in packfiles for better compression.
Supports shallow clones with limited history depth.
Fetching Data: Packfile Negotiation
The client tells the server what it wants and what it has:
Upload Request
upload-request = want-list
*shallow-line
*1depth-request
[filter-request]
flush-pkt
first-want = PKT-LINE("want" SP obj-id SP capability-list)
additional-want = PKT-LINE("want" SP obj-id)
shallow-line = PKT-LINE("shallow" SP obj-id)
depth-request = PKT-LINE("deepen" SP depth) /
PKT-LINE("deepen-since" SP timestamp) /
PKT-LINE("deepen-not" SP ref)
Example:
C: 0054want 74730d410fcb6603ace96f1dc55ea6196122532d multi_ack \
side-band-64k ofs-delta\n
C: 0032want 7d1665144a3a975c05f1f43902ddaf084e784dbe\n
C: 0032want 5a3f6be755bbb7deae50065988cbfa1ffa9ab68a\n
C: 0000
Have Negotiation
Client sends OIDs it already has:
C: 0032have 7e47fe2bd8d01d481f44d7af0531bd93d3b21c01\n
C: 0032have 74730d410fcb6603ace96f1dc55ea6196122532d\n
C: [... up to 32 haves, then flush ...]
C: 0000
S: 003aACK 7e47fe2bd8d01d481f44d7af0531bd93d3b21c01 continue\n
S: 003aACK 74730d410fcb6603ace96f1dc55ea6196122532d continue\n
S: 0008NAK\n
In multi_ack mode, the client sends blocks of 32 haves, allowing continuous negotiation without waiting for all haves to be sent.
Negotiation Strategies
multi_ack mode:
Server ACKs common commits with “ACK obj-id continue”
After finding acceptable base, blindly ACKs all haves
Sends NAK, waits for “done” or more haves
multi_ack_detailed mode:
“ACK obj-id common” - Identifies common commits
“ACK obj-id ready” - Signals readiness to send packfile
No multi_ack:
Server sends single “ACK obj-id” for first common object
Remains silent until client sends “done”
Termination
C: 0009done\n
S: 0031ACK 74730d410fcb6603ace96f1dc55ea6196122532d\n
S: [PACKFILE]
The canonical client gives up after 256 unacknowledged haves, assuming no common history. However, this only activates if at least one “ACK continue” was received previously.
Packfile Data Transfer
After negotiation, the server streams the packfile:
packfile = "PACK" + version + object-count + objects + checksum
See gitformat-pack for detailed packfile structure.
Side-band Multiplexing
If side-band-64k was negotiated:
Each packet:
4-byte length
1-byte channel:
1 = packfile data
2 = progress messages (→ stderr)
3 = error messages
N-byte data (up to 65519 bytes)
Demultiplexing
Go Demultiplexing
def read_sideband ( stream ):
while True :
pkt = read_pkt_line(stream)
if pkt is None :
break
channel = pkt[ 0 ]
data = pkt[ 1 :]
if channel == 1 :
# Packfile data
process_packfile_data(data)
elif channel == 2 :
# Progress
sys.stderr.write(data.decode())
elif channel == 3 :
# Error
raise Exception ( f "Server error: { data.decode() } " )
Pushing Data: Reference Discovery
Similar to fetching, but different capabilities:
S: 006274730d410fcb6603ace96f1dc55ea6196122532d refs/heads/local\0
report-status delete-refs ofs-delta\n
S: 003e7d1665144a3a975c05f1f43902ddaf084e784dbe refs/heads/debug\n
S: 003f74730d410fcb6603ace96f1dc55ea6196122532d refs/heads/master\n
S: 0000
Push Capabilities
report-status / report-status-v2 - Detailed update status
delete-refs - Allow reference deletion
ofs-delta - Use offset deltas in packfile
atomic - All-or-nothing updates
push-options - Send additional metadata
Pushing Data: Update Request
update-requests = *shallow ( command-list | push-cert )
command-list = PKT-LINE(command NUL capability-list)
*PKT-LINE(command)
flush-pkt
command = create / delete / update
create = zero-id SP new-id SP name
delete = old-id SP zero-id SP name
update = old-id SP new-id SP name
Example:
C: 00677d1665144a3a975c05f1f43902ddaf084e784dbe \
74730d410fcb6603ace96f1dc55ea6196122532d \
refs/heads/debug\n
C: 006874730d410fcb6603ace96f1dc55ea6196122532d \
5a3f6be755bbb7deae50065988cbfa1ffa9ab68a \
refs/heads/master\n
C: 0000
C: [PACKFILE]
Packfile MUST NOT be sent for delete-only operations
Packfile MUST be sent for create/update (even if empty)
Server validates that old-id matches current ref value
Push Certificates
For signed pushes:
push-cert = PKT-LINE("push-cert" NUL capability-list LF)
PKT-LINE("certificate version 0.1" LF)
PKT-LINE("pusher" SP ident LF)
PKT-LINE("pushee" SP url LF)
PKT-LINE("nonce" SP nonce LF)
*PKT-LINE("push-option" SP option LF)
PKT-LINE(LF)
*PKT-LINE(command LF)
*PKT-LINE(gpg-signature-lines LF)
PKT-LINE("push-cert-end" LF)
Report Status
After receiving the packfile:
report-status = unpack-status
1*(command-status)
flush-pkt
unpack-status = PKT-LINE("unpack" SP ("ok" / error-msg))
command-status = PKT-LINE(("ok" / "ng") SP refname [SP error-msg])
Example:
S: 000eunpack ok\n
S: 0018ok refs/heads/debug\n
S: 002ang refs/heads/master non-fast-forward\n
Extended format includes option lines for proc-receive hook: S: 0018ok refs/heads/feature\n
S: 001foption refname refs/pull/123\n
S: 0025option old-oid <40-hex>\n
S: 0025option new-oid <40-hex>\n
Shallow Clones
Request limited history depth:
C: 000ddeepen 1\n
C: 0000
S: 0035shallow 7e47fe2bd8d01d481f44d7af0531bd93d3b21c01\n
S: 0000
Shallow Update
Server responds with shallow/unshallow lines:
shallow-update = *shallow-line
*unshallow-line
flush-pkt
shallow-line = PKT-LINE("shallow" SP obj-id)
unshallow-line = PKT-LINE("unshallow" SP obj-id)
Partial Clone
Request filtered packfile:
C: 002bfilter blob:limit=1m\n
The server omits objects not matching the filter (see git rev-list --filter).
The resulting pack must be marked as a .promisor pack, and missing objects can be fetched on-demand later.
Error Handling
ERR-line = PKT-LINE("ERR" SP explanation-text)
Error packets immediately terminate the connection:
def handle_packet ( pkt ):
if pkt.startswith( b 'ERR ' ):
raise ProtocolError(pkt[ 4 :].decode())
Protocol Versions
Version 1 Original protocol with stateless HTTP support. Described in this document.
Version 2 Modern protocol with server-side filtering, better capability negotiation. See gitprotocol-v2.
Enable side-band-64k
Larger packets reduce overhead for high-bandwidth connections.
Use ofs-delta
Offset-based deltas compress better than reference deltas.
multi_ack_detailed
Reduces round trips during have/want negotiation.
Protocol v2
Eliminates unnecessary ref advertisement in many cases.
Security Considerations
Git daemon (port 9418) has NO authentication - use only for public repositories
SSH provides authentication and encryption
HTTPS provides encryption and optional authentication
receive-pack over Git protocol makes repositories world-writable