# Research: receive-pack (push) walkthrough captures **Status**: complete **Date**: 2026-09-25 **Client**: git 2.43.0 **Method**: real `git push` (file://, git:// daemon, and smart-http against a ground-truth mock receive-pack server validated against real git behavior), plus raw stdio requests driven into real `git receive-pack` for policy-error ground truth. This is the normative push-path wire record that ADR-013 is built on. Scratch logs were in /tmp/opencode/oq04/ (not kept); every fact below is restated in full here. ## Advertisement (GET info/refs + duplex) - Smart-http GET: `# service=git-receive-pack` + flush, then a **V0-shaped ref advertisement**: ` \0` on the FIRST ref line only, remaining ref lines bare, flush. The `capabilities^{}` zero-id sentinel line replaces the ref lines ONLY when the repo has no refs. Content-Type: `application/x-git-receive-pack-advertisement`. - Duplex (git daemon): same ref advertisement WITHOUT the service prefix; server speaks first; no version negotiation at all — receive-pack is V0/V1-framed even for modern git (protocol.version=2 does not apply). - Real receive-pack caps: `report-status report-status-v2 delete-refs side-band-64k quiet atomic ofs-delta object-format=sha1 agent=git/2.43.0` (+ `push-options` when receive.advertisePushOptions=true). The client selects a subset NUL-attached on its FIRST command line (observed: `report-status-v2 side-band-64k quiet object-format=sha1 agent=...`; note the leading space after the NUL). - git 2.43 rejects a bare capability dump (no refs, no sentinel) and rejects a trailing `capabilities^{}` line when refs exist (`fatal: unexpected capabilities^{}`). ## Request (POST git-receive-pack) - Body: command pkt-lines ` ` (caps NUL-attached on the first line only) + flush + [push-options section: bare pkt-lines + flush, only when push-options was negotiated] + PACK stream (raw, to end of body). - Deletes: old= new=zero-id. Creates: old=zero-id. - Shallow clients push `shallow ` lines BEFORE the command lines (captured from a --depth 1 clone push). Real receive-pack ACCEPTS them (unpack ok, refs applied) when the pack completes history; rejects with `ng shallow update not allowed` when the server would have to graft (receive.shallowUpdateDeepen unset). - HTTP framing: POST Content-Type `application/x-git-receive-pack-request`; small bodies Content-Length; large (above http.postBuffer, git 2.43 default 1 MB) → chunked, and a 4-byte `0000` probe POST answered 200-empty (same shape as upload-pack, POC-3). - git:// framing: first pkt `git-receive-pack /\0host=\0`, then the advertisement (server speaks first, no service prefix), then the request on the stream. ## Thin pack - `git push` sends THIN packs by default (push.thin=true): the pack excludes objects the client believes the server already has (observed: the unchanged parent tree and its blob excluded; the new commit, tree, blob sent). NO `thin-pack` capability is involved on push (that string is a fetch arg). Ingestion MUST resolve delta bases from the server's own odb — the `Option` no-lookup composition is only valid when the pack is known-not-thin. - Verified: `git index-pack --strict --stdin` FAILS on the captured pack ("did not receive expected object"); `--fix-thin` succeeds. ## Status report - When side-band-64k was selected: the report is band-1 pkt-line chunks; the band payload is ITSELF pkt-line-framed: `unpack ok|ng `, then per-ref `ok ` / `ng `, terminated by an INNER flush pkt inside the band; the response then ends with an OUTER flush. Without sideband (file:// local path): report lines are bare pkt-lines to a single flush. - Observed per-ref ng reasons (real receive-pack): `shallow update not allowed`, `deletion prohibited`, `non-fast-forward`, `atomic push failure` (rollback of other refs). `unpack ok` even when some refs fail; pack-level failure → `unpack ng ` (git source: the index-pack error text; not live-captured). - Atomic: captured `ng refs/heads/dev deletion prohibited` + `ng refs/heads/main atomic push failure` in one report (dev was the real failure; main was rolled back). - Human-readable diagnostics ride band-2 (`remote: error: denying non-fast-forward refs/heads/main (you should pull first)`). - Client aborts on framing errors: a report sent WITHOUT sideband when the client selected side-band-64k → `fatal: protocol error: bad line length character` (observed live) — the report MUST be sideband-wrapped when side-band-64k was selected. ## CAS / race semantics - The advertisement IS the client's CAS check: a stale old-id that does not appear in the advertisement → client refuses to send (non-FF or fetch-first message, NO commands transmitted). - Server-side CAS is still required: races between advertisement and apply, and clients that send wrong old-ids deliberately. old must match current (or zero-id create) per ref; else `ng `. - Empty command list (immediate `0000`) = client-side nothing-to-do. ## Version/framing surface - No version line anywhere in receive-pack: no `version=2` exchange on push; the push path is V0/V1-framed regardless of protocol.version. - report-status-v2 selected by the client when offered; response shape is identical to v1 for plain pushes (v2 adds option lines only when options exist). ## Raw stdio captures into real `git receive-pack` (hand-built requests) - Corrupt pack: `unpack protocol error (pack signature mismatch detected)` + `ng unpacker error` — unpack runs BEFORE ref checks; a failed pack reports per-ref 'unpacker error' (not the detailed reason). - Truncated request: `unpack eof before pack header was fully read`. - Invalid ref name with a VALID pack: `unpack ok` + `ng refs/heads/../evil funny refname` — name validation is per-ref, after unpack, with reason 'funny refname'. (Client-side git refuses such refnames itself; this is the malicious-client path.) - Advertisement confirmed on the raw path: first ref line carries the caps NUL-attached; `unpack ng ` at pack level, per-ref lines after. ## Pack-read ordering (definitive) Real receive-pack ALWAYS expects a pack after the command flush (missing pack → `unpack eof before pack header was fully read` + all refs `unpacker error`). Per-ref checks (CAS, name validation, policy) run AFTER the pack is indexed — observed: stale-old ng, 'funny refname' ng, 'deletion prohibited' ng all arrive only after `unpack ok`. Fail-fast CAS before reading the pack is an optimization, not the protocol order. ## quiet semantics The client selects `quiet` on its first command line in every capture; band-2 error lines still arrived (`remote: error: denying non-fast-forward ...`). quiet suppresses progress chatter, not error/diagnostic bands.