git-ents.gitmain
⌘K
foforge
receive.adoc8.1 KBhistorycomment on this file

Receive

"Push" conflates object transfer with a verified ref transaction; locally, transfer is vacuous. The library exposes exactly one unit for mutation: receive(refs, objects, events, proposal). Gate evaluation, effect matching, and enqueue live inside receive, above the RefStore, object-store, and EventSink traits it is handed.

Receive Is the Unit of Mutation

receive(refs, objects, events, proposal) MUST be the sole entry point through which a meta-ref or branch ref is mutated. Gate evaluation (<<gate.tip-signed>> through <<gate.atomic-cas>>), effect matching, and enqueue MUST live inside receive, above its trait parameters, never duplicated in a caller. This requirement governs origination — proposing new state to a store. Replicating refs a trusted remote has already admitted through its own receive (<<sync.forge-transfer>>) MAY apply them directly: the source’s gate already judged that state, so re-verification on fetch is an opt-in audit rather than an obligation, and the work set stays reconstructible by scan regardless (<<receive.reconstructible>>). Commits the replicating machinery authors itself — divergence and adoption merges (<<gate.same-actor-divergence>>, <<gate.adoption-merge>>) — are origination, not replication.

Proposal Is the Fourth Argument’s Shape

receive’s `proposal argument MUST carry: the set of proposed ref transitions, each a (refname, old-oid, new-oid) triple; the objects accompanying those transitions; and any transport-auth evidence the frontend collected (a signed-push credential, a smart-HTTP session), or none, for frontends where the transport itself carries no separate authentication. Gate evaluation (<<receive.unit>>) MUST be checkable against exactly this shape, independent of which frontend constructed the proposal. This transport-auth evidence is a connection-level ACL input for refs/heads/* (<<gate.principled-split>>) only; gate evaluation for meta-ref admission MUST ignore it entirely and MUST NOT consult it in place of the tip invariant (<<gate.signature-artifact>>). No policy for that refs/heads/* ACL input is specified yet (<<gate.branch-acl-undefined>>); receive MUST thread the evidence through uninterpreted until one is.

Attributed Author, Signing Committer

A mutation commit’s committer MUST be the signing identity — the actor whose key produces the commit’s gpgsig header. The author MAY be a distinct attributed member, so history reads "<member> via <frontend>" (<<roots.web-signing>>); when no attribution applies, author and committer MUST be identical. Gate evaluation and authorization (<<gate.tip-signed>>) MUST key off the signer, never the author: attribution is provenance for readers of history, not authority.

An Entity Declared Across Multiple Refs Writes Them as One Proposal

Some entities are declared to require more than one ref at once as part of their canonical shape — a review and its retention pin are the motivating case: two refs that MUST both exist for the review to exist at all. Creating or updating such an entity MUST carry every one of its refs' transitions in a single Proposal (<<receive.proposal-shape>>) through one call to receive, never as two or more independent proposals. The ref-store’s atomic multi-ref compare-and-swap (<<arch.refstore-read-cas-split>>) then admits or refuses the whole batch together, so such an entity is never observable with only some of its refs written. This is distinct from the inbox/canonical relationship (<<meta-ref.inbox>>), where a contributor’s ref and the canonical ref are deliberately written at different times by different actors, not as one unit.

One Receive, Every Frontend

Every mutation frontend MUST share the identical receive implementation with the hosted server; only the trait implementations passed to it may differ. The CLI and the local UI MUST call receive in-process against the local object database. Smart-HTTP MUST unpack the incoming wire pack into the ObjectStore before calling receive. Local and hosted MUST NOT share a push path; they MUST share receive, with only the trait implementations swapped, which is the correctness anchor for writes.

RefStore Seam

The RefStore seam receive is handed MUST satisfy the read/CAS split specified in <<arch.refstore-read-cas-split>>.

Object Access Via gitoxide

Object access inside receive MUST use gix_object::Find, Exists, and Write; a private object-access trait MUST NOT be introduced where gitoxide already has one. A receive running inside a git hook MUST read existing repository state through the common object directory, never through the quarantine directory git injects via GIT_OBJECT_DIRECTORY during a push: quarantined incoming objects are the proposal under verification, not durable state, and MUST NOT be treated as stored until the transaction commits.

EventSink Payload Contract

EventSink MUST be the sole destination for post-receive matches: null locally, a durable queue hosted. receive MUST statically extract each effect query’s ref footprint so a ref transition maps to the affected queries, and MUST enqueue re-evaluation for each match.

Pushes Are Never Blocked by Effects

receive MUST NOT block a push on effect evaluation; the durable enqueue in <<receive.event-sink>> MUST be the entire synchronous cost. Post-receive MUST remain a dumb matcher with no evaluation logic of its own.

Exactly-Once Outcomes

The queue fed by receive is at-least-once; the dedup key MUST be (effect, oid), so that redelivery yields exactly-once outcomes with zero state kept outside the repository.

The Queue Is Reconstructible From Repository State

The set of pending obligations an EventSink enqueues MUST be derivable from repository state alone, per the work set defined in <<query.workset>>. An EventSink implementation MAY lose queued events on crash, provided the composition root reconciles obligations from repository state at startup before serving further pushes. A durable queue MUST be treated as a performance optimization, never a correctness requirement; the dedup key in <<receive.dedup>> is unchanged by reconciliation.

Redaction Records Are Admin-Only

A push to refs/meta/redactions/* MUST be rejected unless the pushing member is admin-registered, regardless of any other role rule refs/meta/config may define. Authorizing a yank withholds bytes from every future reader of the repository, which needs more trust than an ordinary branch push, so this rule MUST be enforced explicitly and MUST be statable as the single refname glob refs/meta/redactions/*, mirroring <<effect.admin-only>>.

Redaction Enforcement at Ingest

receive MUST check every incoming object against the redaction list recorded under refs/meta/redactions/* at ingest time, so a redacted hole cannot be silently refilled by re-pushing the same bytes. This binds new admission only: for an object redacted after it was already accepted, receive refusing future pushes of the same bytes does not by itself withhold the bytes already sitting in the object store. A redacted object’s bytes MUST be withheld from the object store and from every generated pack; the oid MUST remain in history as evidence. Guaranteeing this for bytes already stored before their redaction is not `receive’s job — it belongs to whichever component actually generates outgoing packs, which is not yet specified for every root (<<roots.redaction-pack-serving>>). A reader resolving a redacted object MUST receive a redaction marker, never an error.