Skip to content

Commit 686fd36

Browse files
tlongwell-blockQuinn
andcommitted
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

File tree

0 commit comments

Comments
 (0)