Files
alkgit/docs/research/git-protocol.md
T
glm-5.3-flash a3cdef909b feat: workspace skeleton + phase 0 research docs
- Cargo workspace with 5 sub-crates: alkgit-core (storage),
  alkgit-transport (smart protocol), alkgit-http, alkgit-ssh, alkgitd
  (binary); sha1 pinned through the gix stack, sha256 passthrough feature
- docs/research/: vision, gitoxide alignment, alk stack fit, server-side
  protocol inventory, license/reference policy, POC plan
- AGENTS.md: conventions mirroring alkcall (no comments, thiserror, tokio,
  no secrets on wire/at rest, visible-surface=authorized-surface,
  gitoxide-only serving path, bounded resources, registry-resolved repos)

Verified: cargo build, clippy -D warnings, fmt, test, check --all-features
2026-09-19 15:36:21 +00:00

118 lines
5.7 KiB
Markdown

# Research: git smart protocol (server side) — what we must implement
**Status**: initial pass complete
**Date**: 2026-09-19
**Sources**: gitoxide source reading (client-side parsers as reference),
git http-protocol + packfile protocol specs (git-scm.com protocol docs),
observation of gitserver's shape as a sanity check that the surface is this
small (no code reused — MPL-2.0 incompatible with our MIT/Apache-2.0; see
`license-note.md`).
## TL;DR
The server-side surface is well-bounded: capability advertisement (V0/V1 and
V2 flavors), `ls-refs`, fetch negotiation (wants/haves → acks → pack),
receive-pack (pack ingestion + ref update CAS + status report), plus the
stateless HTTP framing and the SSH exec framing around them. gitoxide gives
us every primitive (pkt-line codec, pack parse/write, ref transactions,
fsck); what's missing is only the *orchestration*, which is ours to write —
and that's the layer where our auth/ACL model lives, which is exactly where
we want to own code.
## Protocol surface inventory
### V2 (the target; what modern git speaks first)
1. Client sends `GIT_PROTOCOL=version=2` (SSH: env line / http: header).
2. Server replies with capability advertisement: `version 2`, then
capabilities as pkt-lines: `agent=...`, `ls-refs=...`, `fetch=...`
(shallow, filter, sideband-64k, packfile-uris...), `object-format=sha1`.
3. `command=ls-refs` with args (peel, symrefs, ref-prefix) → server streams
ref lines then flush.
4. `command=fetch` with args (want lines, have lines, done, thin-pack,
no-progress, include-tag, shallow/deepen...) → server sends
`acknowledgments` section (acked ids or `NAK`), then either
`ready` + `packfile` section (pack streamed over sideband) if done, or
waits for more haves.
### V0/V1 (fallback for old clients / http stateless)
- First response line: `"<capabilities> <null-octet> <ref> <obj-id>"` style
advertisement with refs, then fetch loop: wants → haves (NAK/ACK) → pack
over sideband. Stateless-http variant requires the client to POST a
separate request for each round; `ack` state must be re-derived or the
multi-round negotiation declined (we can require V2 for http and keep V0/V1
for ssh only — OQ candidate).
### receive-pack (push; mostly version-independent)
- Client sends update requests (`<old> <new> <ref>`) + optional shallow lines
+ pack stream (possibly thin).
- Server: validate CAS per ref (old must match current unless zero-id create),
fsck/connectivity the pack, apply ref transaction atomically, reply with
`unpack <ok|ng>` + per-ref `ok|ng <ref> <reason>` lines.
### Framing per interface
- **SSH**: single exec request `git-upload-pack '<path>'` /
`git-receive-pack '<path>'` / `git-upload-archive`; pkt-line on stdin/stdout;
V2 via env var; sideband on fetch.
- **HTTP**: `GET /info/refs?service=<name>` (advertisement in
`# service=git-upload-pack` preamble for smart clients),
`POST /<name>` with pkt-line body; content-type
`application/x-git-{upload,receive}-pack-*`; chunked streaming both ways;
`Cache-Control: no-cache`.
## Server-side pack generation — the crux
`gitserver` (MPL-2.0, read-only reference) demonstrates the pragmatic path:
compute the pack from wants/haves and hand it to the response writer — they
use `gix` + hand-rolled protocol_v2 (742 LOC) and receive_pack (549 LOC) for
the whole protocol surface. We cannot copy that code, but its size confirms
the surface is small enough to own outright with gitoxide primitives:
- Pack generation options to POC:
a. `gix-pack bundle::write` with an in-memory sink (verify it can stream,
not just write files).
b. `gix-pack::data::output::bytes` entries-to-bytes writer fed by an object
walk over `gix-odb` (full control over want/have closure; no file
intermediates).
- Thin packs (delta against client haves) are an optimization — v0 can
declare `thin-pack` unsupported initially; V2 fetch arg parsing must still
accept/decline it gracefully.
- `filter` (partial clone) and `packfile-uris` can be declined in v1 of
alkgit; advertised capabilities must be honest (never advertise what we
don't serve — the gitea-class bug pattern is promising something and
enforcing elsewhere).
## Negotiation policy (ours to define)
Minimal correct initial policy:
- V2: respond with full `acknowledgments` (common ids) each round; send pack
on `done`.
- Enforce max rounds/haves budget per session (DoS bound), max pack size for
receive, wall-clock limits for long negotiations.
- Shallow (` deepen/shallow lines) is a v2-later concern; reject with a
clear pkt-line error initially (honest capability advertisement again).
## Security-relevant protocol notes
- `git-upload-archive` is a separate command — do not serve it initially.
- Path traversal: repo names on the wire (`git-upload-pack '~/x'` or
`../`) must normalize to a registry lookup — never to a filesystem path
from the wire. Repo IDs resolve to storage roots configured server-side.
- receive-pack ref names: validate against git ref rules (no `..`, no
control chars, no `refs/heads/foo.lock` games) — `gix-ref` name parsing
plus our own deny-list for e.g. `refs/` reserved namespaces.
- The advertisement phase must run ACL before emitting a single ref line
(ref names leak info; private repos must not advertise anything).
## Open items → architecture OQs
- OQ: support V0/V1 at all on http (or V2-only http, V0/V1 ssh)?
- OQ: shallow clone support timeline.
- OQ: thin-pack on fetch (requires delta against client have set).
- OQ: pack streaming composition (POC-1 outcome decides bundle-write vs
entries-to-bytes).
- OQ: `object-format=sha256` support policy (feature flag exists; no real
ecosystem need yet — default sha1, keep flag).