Repository navigation
Commit 686fd36
feat(git/cas_publish): the §Push step 2-7 commit point
Adds crates/sprout-relay/src/api/git/cas_publish.rs — the pure async
function that turns a post-receive-pack workspace into a durable manifest
CAS. Composes Dawn's GitStore primitives (put_pack, put_manifest,
get_pointer, put_pointer) and Dawn's Manifest schema (canonical_bytes,
validate, Inv_Closed at compose time) into the spec's step 2-7 sequence:
read pointer (e, d_before) §step 3
fetch + verify m_before §step 3 + A1 detectability
snapshot refs + symref-HEAD from disk (HEAD inherits parent on detach)
pack new objects via pack-objects --revs §step 1-2
put_pack(bytes) -> packs/<sha256> §step 2 (A1)
compose m_after (parent packs + new pack, parent = digest only) §step 5
m_after.validate() (Sami/Max/Perci #2-#4)
put_manifest(canonical_bytes) -> manifests/<sha256> §step 6
put_pointer(IfMatch(e) | IfNoneMatchStar) §step 7 (CAS)
Won -> CasSuccess { manifest, manifest_key }
Lost -> CasError::Conflict { winner_manifest, winner_manifest_key }
(→ HTTP 409, with winner for disk reconcile)
The function returns *before* a Response is constructed — it is called
from finalize_push, which is the unique site that builds a push 2xx, so
the structural seam still enforces Theorem 1 (success-after-CAS).
## Review fixes folded in
Sami's review (#1–#6) + Perci's #1 + Max's pre-CAS-validation blocker
are all addressed in this commit:
- **parent = bare 64-hex digest, not full key** (Perci #1, Max). Pointer
body is `<digest>`; `Manifest.parent` stores the same digest, matching
`Inv_RefDerivedFromParent` literally. `read_parent` strips the
`manifests/` prefix before assigning. Dawn's new `MalformedParent`
validator catches any drift at the write seam.
- **Pre-CAS validation** (Sami #2, Max). `m_after.validate()?` runs
between `compose_after` and `put_manifest`. Unsafe refnames, malformed
oids, empty HEAD — all surface as `CasError::ManifestInvalid(...)`
(4xx-class) before any S3 write, *not* as "valid CAS, un-clone-able
output." Typed variant (not reused `ManifestReadFailed`) so logs /
status mapping distinguish "client input rejected" from "stored parent
failed A1" (Max + Dawn).
- **Detached-HEAD fallback** (Sami #3). `snapshot_workspace_state`
returns empty `head` on detached HEAD; `cas_publish` falls back to
`parent.head` if non-empty. `validate()` rejects the first-push-+-
detached case (Sami #4 — no parent to inherit from, manifest is
un-clone-able).
- **Conflict carries the winner** (Sami #5 + Dawn). `Conflict {
winner_manifest, winner_manifest_key }` lets `finalize_push` invoke
Eva's `reconcile_to_manifest` mechanically from the error arm, without
a second pointer GET in the caller. `warn!()` at the `LostRace` site
logs (pointer, expected etag, attempted manifest) for debugging
concurrent-push patterns. Boxed for `clippy::result_large_err`.
- **Empty-pack comment** (Sami #6). Clarified `capture_pack` returns
`None` in both the delete-all (`refs_after.is_empty()`) and refs-only
(`pack-objects` empty stdout) cases.
- **`pointer_key` consolidated** in `manifest.rs` (Sami #1, Dawn,
Max — Sami's "single source of truth" argument). `cas_publish`
imports it; the duplicate definition is gone.
- **`validate-invocation` test added** in `cas_publish.rs` (Sami's
recommendation). Pins that a future refactor dropping the `validate?`
call between `compose_after` and `put_manifest` is caught by unit
test, not by every subsequent un-clone-able read.
## What this deliberately does NOT do (each with citation)
- No retry on LostRace. Per Sami's TLA-action guidance: the receive-pack
output is derived against a now-superseded parent; reusing it would
violate Inv_RefDerivedFromParent. Client re-pushes, which re-hydrates
+ re-runs receive-pack against the advanced pointer — the only safe
retry, which git already performs. Spec §Push step 7: 'GOTO 3 (retry)
or respond non-ff' — both arms safe; we take non-ff.
- No kind:30618 emission. That is derived after CAS — finalize_push
calls Sami's build_ref_state_event over m_after.refs / m_after.head
on Ok. Spec §Implementation Correspondence: 'kind:30618 is derived
after CAS, never the commit.'
- No advisory lock. Spec §Push 'no advisory lock in v1' — writer
serialization is the CAS. A mutex would hide the contention Inv_NoFork
proves safe.
## Tests
10 unit tests pin digest_from_key (manifest/<...> prefix invariant),
compose_after (Inv_Closed coverage, sort, dedupe, refs-only-no-new-pack,
first-push, parent-is-digest-not-key), validate invocation (unsafe
refname + first-push-empty-HEAD both rejected pre-CAS). 244 relay tests
green; clippy --tests -D warnings clean.
The integration into finalize_push lands separately — Eva owns the
AppState::git_store wiring + main.rs startup probe gate. This module
is callable today: cas_publish(&store, repo_path, owner, repo,
&refs_before) -> Result<CasSuccess, CasError>.
Refs:
- docs/git-on-object-storage.md §Push step 2-7, §Implementation
Correspondence, §Mechanized Verification (Inv_NoFork,
Inv_RefEffectApplied, Inv_RefDerivedFromParent, Inv_Closed).
Also makes transport::harden_git_env pub(crate) for reuse by
cas_publish's two subprocess sites (for-each-ref, pack-objects).
Co-authored-by: Tyler Longwell <[email protected]>
Co-authored-by: Quinn <[email protected]>
Signed-off-by: tlongwell-block <[email protected]>1 parent 0bcb045 commit 686fd36
3 files changed
Lines changed: 711 additions & 1 deletion
0 commit comments