git-ents.gitmain
⌘K
foforge
commit 8586440
sync: seal merge tips without a binding trailer

The merge commit names no ref of its own; the gate recomputes the binding from the merged content and the all-roots walk (gate.identity-binding), which holds across the merge because both parents descend from the same genesis. Testutil seeds entities and results the same trailer-free way and builds a ResultRecord in record_result.

refactor: drop the Advance-ref trailer from seal refactor: build a ResultRecord in testutil record_result Assisted-by: Claude:claude-opus-4-8

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

crates/kernel/ents-sync/src/lib.rs @@ -50,7 +50,7 @@ //! //! ``` //! use ents_gate::{Config, Update, Verdict, verify}; -//! use ents_model::{Provenance, namespace, trailer::Trailers}; +//! use ents_model::{Provenance, namespace}; //! use ents_sync::resolve::{Heads, Merged, merge_heads}; //! use ents_testutil::{ //! CommitSpec, Keypair, MemRefStore, ObjectStore, enroll_member, write_commit, write_meta_entity, @@ -70,26 +70,26 @@ //! let config: gix::refs::FullName = namespace::CONFIG_REF.try_into().expect("valid"); //! write_meta_entity(&refs, &objects, config, &Config { epoch: Some(200) }, Some(&jdc), 200); //! -//! let name: gix::refs::FullName = "refs/meta/issues/1".try_into().expect("valid"); //! let issue = Issue { //! title: "t".into(), body: "b".into(), state: "open".into(), //! }; -//! let trailers = Trailers { ents_ref: Some(name.clone()), schema_version: None }; -//! let msg = |s: &str| format!("{s}\n\n{}", trailers.render()); //! -//! // A common base, then two divergent children editing disjoint fields. +//! // A common base (the genesis), then two divergent children editing +//! // disjoint fields. The issue's id is the genesis commit's own oid +//! // (`meta-ref.identity-binding`), so the ref is named from it. //! let base_tree = facet_git_tree::serialize_into(&issue, &objects).expect("ser"); -//! let base = write_commit(&objects, &CommitSpec { tree: base_tree, parents: vec![], message: msg("Open"), seconds: 300 }, Some(&jdc)); +//! let base = write_commit(&objects, &CommitSpec { tree: base_tree, parents: vec![], message: "Open".into(), seconds: 300 }, Some(&jdc)); +//! let name: gix::refs::FullName = format!("refs/meta/issues/{base}").try_into().expect("valid"); //! //! let mut renamed = issue.clone(); //! renamed.title = "renamed".into(); //! let ours_tree = facet_git_tree::serialize_into(&renamed, &objects).expect("ser"); -//! let ours = write_commit(&objects, &CommitSpec { tree: ours_tree, parents: vec![base], message: msg("Rename"), seconds: 400 }, Some(&jdc)); +//! let ours = write_commit(&objects, &CommitSpec { tree: ours_tree, parents: vec![base], message: "Rename".into(), seconds: 400 }, Some(&jdc)); //! //! let mut closed = issue.clone(); //! closed.state = "closed".into(); //! let theirs_tree = facet_git_tree::serialize_into(&closed, &objects).expect("ser"); -//! let theirs = write_commit(&objects, &CommitSpec { tree: theirs_tree, parents: vec![base], message: msg("Close"), seconds: 400 }, Some(&jdc)); +//! let theirs = write_commit(&objects, &CommitSpec { tree: theirs_tree, parents: vec![base], message: "Close".into(), seconds: 400 }, Some(&jdc)); //! //! let author = gix::actor::Signature { //! name: "jdc".into(), email: "jdc@ents.test".into(),
crates/kernel/ents-sync/src/resolve.rs @@ -24,7 +24,6 @@ use gix_hash::ObjectId; use gix_object::{Commit, Find, Kind, Write, WriteTo as _}; -use ents_model::trailer::Trailers; use crate::error::{Error, Result}; use crate::merge::{Merge, three_way}; @@ -39,7 +38,8 @@ /// divergence, or the contributor's inbox / self-run tip in an adoption. #[derive(Debug, Clone)] pub struct Heads { - /// The ref the resulting merge tip advances (its `Advance-ref` trailer). + /// The ref the resulting merge tip advances; the gate recomputes this + /// name from the merge's signed content (`gate.identity-binding`). pub refname: FullName, /// The authorized side's current tip, or `None` if the ref is new. pub ours: Option<ObjectId>, @@ -69,8 +69,9 @@ /// The typed trees of `ours` and `theirs` are merged three-way against /// their merge base; a clean merge is recorded as a merge commit whose /// parents are `[ours, theirs]` (just `[theirs]` when the canonical ref is -/// new), authored and committed by `author`, bound to [`Heads::refname`] by -/// the `Advance-ref` trailer, and signed by `sign`. `sign` returns the +/// new), authored and committed by `author`, and signed by `sign`; the +/// merge names no ref of its own, and the gate recomputes the binding for +/// [`Heads::refname`] from the merged content. `sign` returns the /// armored SSHSIG PEM for the commit's payload — exactly what git stores in /// the `gpgsig` header — so the composition root injects the placing /// member's key without this crate ever holding one. @@ -149,34 +150,24 @@ } }; - let tip = seal( - objects, - tree, - parents, - &heads.refname, - author, - summary, - sign, - )?; + let tip = seal(objects, tree, parents, author, summary, sign)?; Ok(Merged::Tip(tip)) } /// Build and sign the merge commit — the tip whose signature, not any tree -/// content, is what satisfies the tip invariant. +/// content, is what satisfies the tip invariant. The commit names no ref +/// of its own; the gate recomputes the binding from the merged content and +/// the all-roots walk (`gate.identity-binding`), which holds across this +/// merge because both parents descend from the same genesis. fn seal( objects: &impl Write, tree: ObjectId, parents: Vec<ObjectId>, - refname: &FullName, author: &gix::actor::Signature, summary: &str, sign: impl FnOnce(&[u8]) -> String, ) -> Result<ObjectId> { - let trailers = Trailers { - ents_ref: Some(refname.clone()), - schema_version: None, - }; - let message = format!("{summary}\n\n{}", trailers.render()); + let message = summary.to_owned(); let mut commit = Commit { tree, parents: parents.into(),
crates/kernel/ents-testutil/src/seed.rs @@ -1,7 +1,6 @@ //! Seeding helpers: members, meta entities, results, and code-ref history. -use ents_model::trailer::Trailers; -use ents_model::{Member, MemberId, Provenance, Status, namespace}; +use ents_model::{Member, MemberId, Provenance, ResultRecord, Status, namespace}; use gix::refs::FullName; use gix_hash::ObjectId; use gix_object::{Find, Kind, Write}; @@ -28,9 +27,10 @@ } /// Serialize `entity` as its typed tree and land it on `refname` as a -/// mutation commit carrying the `Advance-ref:` trailer, signed by `signer` -/// when one is given. Parents come from `refname`'s current tip. Returns -/// the new tip. +/// mutation commit signed by `signer` when one is given. Parents come from +/// `refname`'s current tip. Returns the new tip. The refname is bound to +/// the signed content by the gate (`meta-ref.identity-binding`), not by +/// any commit trailer. /// /// # Examples /// @@ -59,11 +59,7 @@ ) -> ObjectId { let tree = facet_git_tree::serialize_into(entity, objects).expect("fixture entity always serializes"); - let trailers = Trailers { - ents_ref: Some(refname.clone()), - schema_version: None, - }; - let message = format!("Mutate {}\n\n{}", refname.as_bstr(), trailers.render()); + let message = format!("Mutate {}", refname.as_bstr()); let parents = crate::refs_get(refs, &refname).into_iter().collect(); let tip = write_commit( objects, @@ -103,7 +99,7 @@ provenance: Provenance, seconds: i64, ) -> ObjectId { - let member = Member::new(key.public_openssh(), provenance); + let member = Member::new(id, key.public_openssh(), provenance); write_member(refs, objects, id, &member, Some(key), seconds) } @@ -121,7 +117,7 @@ /// let objects = ObjectStore::default(); /// let key = Keypair::from_seed(1); /// -/// let mut member = Member::new(key.public_openssh(), Provenance::AdminRegistered); +/// let mut member = Member::new("jdc", key.public_openssh(), Provenance::AdminRegistered); /// member.revoke(); /// write_member(&refs, &objects, "jdc", &member, Some(&key), 900); /// ``` @@ -141,6 +137,12 @@ /// starts with `short_oid`, at the canonical /// `refs/meta/results/<effect>/<short_oid>` ref (`effect.results-writeback`). /// +/// The tree is a [`ResultRecord`] carrying `effect` and a target oid whose +/// hex begins with `short_oid` (`model.result-identity`): when `short_oid` +/// is a hex prefix it is right-padded with zeros to a full oid, so the +/// gate's identity binding recomputes the ref; when it is not hex, a null +/// target is used, sufficient for query scan tests that never gate. +/// /// The result commit is unsigned unless `signer` is given — query tests /// exercise scan semantics, gate tests exercise signatures. /// @@ -165,7 +167,18 @@ ) -> ObjectId { let refname = namespace::result_ref(effect, short_oid).expect("valid result segments in fixture"); - write_meta_entity(refs, objects, refname, &status, signer, seconds) + let target = target_for(short_oid); + let record = ResultRecord::new(effect, target, status); + write_meta_entity(refs, objects, refname, &record, signer, seconds) +} + +/// An oid whose hex form begins with `short_oid`: right-pad a hex prefix +/// with zeros to 40 chars, or fall back to the null oid when `short_oid` +/// is not hex (query scan fixtures do not gate on the target). +fn target_for(short_oid: &str) -> ObjectId { + let padded = format!("{short_oid:0<40}"); + ObjectId::from_hex(padded.as_bytes()) + .unwrap_or_else(|_| ObjectId::null(gix_hash::Kind::Sha1)) } /// Append `count` empty-tree commits on top of `refname`'s current tip