Forge continues to serve Git protocol v0/v1 over both .git and non-.git
routes with the existing Basic authentication and private-repository checks.
side-band-64k is valid for both upload-pack and receive-pack. Upload-pack can
start an HTTP response before graph walking by returning a ReadableStream and
writing channel 2 progress packets while work proceeds. Receive-pack cannot send
server progress until the client has finished the HTTP request body: HTTP/1.1
Smart HTTP uses one request followed by one response, so upload and download are
not full duplex. The closest protocol-correct behavior is an early streaming
response after the body arrives, with channel 2 progress followed by the
report-status payload on channel 1. Clients that do not negotiate side-band get
the unwrapped report-status response and no progress packets.
The implementation follows Git's
gitprotocol-pack and
protocol-capabilities
documentation. The Workers response uses the standard
ReadableStream
API so headers can be returned before the body producer finishes.
Receive-pack reads only the bounded pkt-line command prefix in the API Worker.
The pack body then streams into a repository-scoped R2 multipart upload with
four bounded parts in flight. After that durable handoff, the private runner
streams it into its restricted sandbox. The request path never buffers the
whole pack in Worker memory. Chunked clients do not need to declare the final
pack length before transfer. Forge advertises
no-thin only when that runner path is available, so every delta base needed
to index the pack travels with the request. Native Git validates the pack
checksum and object structure before expanding loose immutable objects.
No http.postBuffer client workaround is used or recommended.
The runner executes native git index-pack --fsck-objects, expands the
self-contained pack into standard loose-object bytes, and uploads those bytes
through an explicit 16-worker R2 pool. Conditional object creation makes
retries idempotent and storage accounting counts only newly written bytes.
The first validation or storage failure prevents every ref update and produces
a valid unpack <reason> plus ng <ref> unpack failed report. Ref updates
retain the current per-ref old-SHA checks; success is never reported before
storage and the corresponding ref mutation finish.
An individual streamed pack is bounded at 490 MB (decimal bytes), with separate object-count
and per-object limits. Crossing that boundary aborts the multipart staging upload and records
terminal GIT_INGEST_PACK_TOO_LARGE evidence with the observed byte count; retrying unchanged
input is never presented as remediation. A disconnect leaves no ref pointing at partial data;
conditional immutable-object writes make an ordinary Git retry safe. Staged
packs are retained across transient private-runner transport, throttling, and
5xx failures for a bounded handoff window. This covers runner drain, provider
traffic activation, and exact-version verification without making a Git client
restart its upload. Staged packs are deleted after a successful receipt or a
terminal/exhausted handoff, and swept after 24 hours if a request disconnects.
The legacy in-Worker parser remains a feature-flag
fallback for rollback and local runtimes; production discovery advertises
no-thin only when the streaming path and private runner binding are both
enabled. The runner streams heartbeat bytes while native Git and object
publication are active, and the API emits receive-pack progress at ten-second
intervals. Long valid imports therefore remain live during staging and native
processing. A retry emits one bounded git_ingest_handoff_retry event per
attempt with the ingest identity, delay, and sanitized reason.
Receive-pack progress stages are receiving/parsing, resolving deltas, writing
objects, updating refs, and scheduling workflows. Progress describes work that
has started or completed and never claims premature completion. Workflow
execution is scheduled only after successful ref updates, attached to
executionCtx.waitUntil, and catches/logs its own failure so the completed Git
response is unaffected.
Upload-pack returns a ReadableStream after request parsing and authorization.
Graph walking reads only the bounded loose-object header for blobs, and pack
generation pipelines each stored zlib body through decompression and
recompression without expanding a large blob into Worker memory. This makes
large repositories cloneable through the same bounded-memory contract used for
pushes.
Larger Git clients send gzip-compressed negotiation bodies, so upload-pack first
decodes Content-Encoding: gzip with a 16 MiB decompressed-request ceiling.
Corrupt or unsupported encodings fail with an explicit HTTP error before graph
walking begins.
Its demand-driven pull function advances the async generator by only one
chunk per reader request with a zero high-water mark, so downstream
backpressure reaches graph walking and pack transfer. With side-band, channel 2
announces graph walking and object counts before pack generation, then channel 1
carries bounded pack chunks as soon as the intermediate generator can provide
them. Without side-band, the protocol prefix is followed by the raw pack stream.
Pack generation uses an incremental SHA-1 trailer and processes one object at a time. Pack headers, compressed object chunks, and the final checksum flow directly through side-band channel 1, so large fetches do not accumulate the complete pack or every compressed object in Worker memory. Stateless filtered promised-object wants pack the requested objects directly instead of reading them once for graph classification and again for transfer.
Discovery advertises multi_ack_detailed with Smart HTTP's no-done
capability. When an incremental fetch supplies a common have, upload-pack
returns ACK <sha> ready and immediately starts the pack stream, avoiding the
extra stateless HTTP round that would otherwise carry done. Clients that do
not negotiate no-done remain in ACK/NAK mode until they explicitly end
negotiation. Pack-cache format v2 invalidates entries created by the earlier
pre-done behavior.
Only no-have clone/fetch requests are cache candidates. The versioned cache key
includes repository id, sorted wanted SHA set (therefore current ref/tip state),
depth, sorted shallow boundaries, object format, and pack format version. Any
have line bypasses the cache, so a full clone pack is never reused for an
incremental negotiation. Ref changes naturally produce different wanted SHAs;
entries expire by TTL and need no synchronous invalidation. Streamed full-clone
packs are captured only up to a strict byte ceiling while the same chunks flow to
the client; larger packs continue streaming without being retained. A separate,
bounded reachability cache stores the ordered object list for the same immutable
request shape, so a pack-cache miss can still avoid repeating the graph walk.
Requests containing any have lines still bypass the completed-pack cache.
They may use the smaller reachability cache because its versioned key includes
the repository ref generation, sorted wanted SHA set, sorted confirmed-common
have set, depth, shallow boundaries, filter, and object format. This avoids
repeating an identical immutable graph walk without confusing a full-clone pack
with an incremental negotiation.
Concurrent identical reachability misses in the same Worker isolate share one bounded graph walk. The single-flight map holds at most 16 active keys, expires an active key from coordination after two minutes, and deletes every entry as soon as the task settles. It never acts as a completed-result cache and never stores partial object lists. Cross-isolate reuse remains the Cache API's best-effort responsibility.
Large generated packs are intentionally not retained in Worker memory. A future large-pack tier should stream a raw pack into a repository-scoped immutable R2 multipart object, publish a complete manifest only after checksum and upload completion, and coordinate producers through the existing repository Durable Object. Callers must still authorize before looking up the opaque cache key; followers may read only the completed object, with generation failure or timeout falling back to ordinary bounded streaming. Size, TTL, and per-repository/global storage budgets are required before that tier ships.
The implementation uses caches.default when available and degrades to an
uncached response in local/test runtimes. Cache writes use bounded responses and
waitUntil; HIT, MISS, and BYPASS are exposed in X-Forge-Pack-Cache
and the request summary log. Cached data is scoped to the repository id and
negotiation parameters, not owner/repository names or credentials.
Each discovery, receive-pack, and upload-pack request owns a request-local timing
record. Server-Timing contains only phases known when headers are committed;
the final structured summary log records operation, credential-free owner/repo
identifiers, byte/object counts, authentication, graph walk, delta resolution,
R2 reads/writes, pack generation, ref updates, workflow scheduling, cache state,
total duration, and final status. There are no per-object production logs and no
headers, passwords, tokens, PATs, or URLs in log data.
For a streaming response, phases completed after headers are committed cannot be
accurately added to Server-Timing; those durations remain available in the
single final structured log. Cache API operations are data-center-local and
cache insertion is best effort, attached to waitUntil.