sync: seal merge tips without a binding trailer
commit
8586440sync: 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
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
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