git-ents.gitmain
⌘K
foforge
lib.rs125 lines · 6.3 KB · rusthistorycomment on this file
1//! `sync`: remote synchronization for the forge — the one capability
2//! `git ents` adds over the local primitives (`docs/spec/sync.adoc`).
3//!
4//! Sync fetches and pushes `refs/meta/*` and, crucially, turns the gate's
5//! verdict into a decision the user acts on before pushing. Its single hard
6//! responsibility is the schema-aware three-way merge over typed trees
7//! (`crate::merge`); everything else — transfer, pre-flight, inbox routing —
8//! is plumbing above traits that already exist by this phase (`RefStore`,
9//! `Find`/`Write`, and `ents_gate::verify`). This crate never re-implements
10//! the gate's judgment (it *calls* [`ents_gate::verify`], `gate.call-sites`)
11//! and never writes to a ref except through the `RefStore` seam it is handed.
12//!
13//! Divergence resolution and adoption are deliberately *one* machinery
14//! (`crate::resolve`), not two code paths (`sync.adoption-machinery`): a
15//! member merging their own racing machines, a maintainer folding an inbox
16//! entity onto its canonical ref, and a maintainer adopting a contributor's
17//! self-run results all go through the same [`resolve::merge_heads`], which
18//! keeps the folded-in head as a parent so its author's signature survives —
19//! a merge, never a cherry-pick (`sync.adoption-no-cherry-pick`).
20//!
21//! # Spec coverage
22//!
23//! From `docs/spec/sync.adoc`:
24//!
25//! - `sync.forge-transfer` — [`transfer::fetch`], [`transfer::push`]: both
26//! copy each meta-ref's full object closure, commit objects verbatim, so
27//! history and signatures move with the ref.
28//! - `sync.pre-flight` — [`preflight::preflight`]: the identical
29//! [`ents_gate::verify`] every call site runs, so a pre-flight verdict is
30//! a prediction that can only be stale (`gate.call-sites`).
31//! - `sync.inbox-routing` — [`preflight::inbox_route`], surfaced by
32//! [`preflight::PreFlight::inbox`] and [`transfer::Pushed::Inbox`]: any
33//! negative advisory verdict offers the author's own inbox segment.
34//! - `sync.divergence-merge` — [`merge::three_way`] and
35//! [`resolve::merge_heads`]: a schema-aware three-way merge whose tip,
36//! once signed, satisfies the tip invariant.
37//! - `sync.adoption-machinery` — [`resolve::merge_heads`] is the *same*
38//! function divergence uses; adoption is only a different pair of heads.
39//! - `sync.adoption-no-cherry-pick` — [`resolve::merge_heads`] always keeps
40//! `theirs` as a parent; it never re-authors the contributor's commit.
41//! - `sync.local-advisory` — sync never blocks a local write on a verdict;
42//! the consequence it owns is the inbox offer ([`mod@preflight`],
43//! [`transfer::push`] is the sole place a verdict gates a *remote* write).
44//!
45//! # Examples
46//!
47//! A same-actor divergence — two of one member's machines each editing a
48//! *different* field of the same issue — resolved into a signed merge tip
49//! the gate then accepts (`sync.divergence-merge`).
50//!
51//! ```
52//! use ents_gate::{Config, Update, Verdict, verify};
53//! use ents_model::{Provenance, namespace};
54//! use ents_sync::resolve::{Heads, Merged, merge_heads};
55//! use ents_testutil::{
56//! CommitSpec, Keypair, MemRefStore, ObjectStore, enroll_member, write_commit, write_meta_entity,
57//! };
58//!
59//! // A stand-in for `ents-forge`'s `Issue` (this crate cannot depend on
60//! // `ents-forge`): any Facet-derived entity exercises the merge.
61//! # #[derive(facet::Facet, Clone)]
62//! # struct Issue { title: String, body: String, state: String }
63//! #
64//! let refs = MemRefStore::default();
65//! let objects = ObjectStore::default();
66//! let jdc = Keypair::from_seed(1);
67//!
68//! // Enroll (bootstrap) and turn verification on by setting the epoch.
69//! enroll_member(&refs, &objects, "jdc", &jdc, Provenance::AdminRegistered, 100);
70//! let config: gix::refs::FullName = namespace::CONFIG_REF.try_into().expect("valid");
71//! write_meta_entity(&refs, &objects, config, &Config { epoch: Some(200) }, Some(&jdc), 200);
72//!
73//! let issue = Issue {
74//! title: "t".into(), body: "b".into(), state: "open".into(),
75//! };
76//!
77//! // A common base (the genesis), then two divergent children editing
78//! // disjoint fields. The issue's id is the genesis commit's own oid
79//! // (`meta-ref.identity-binding`), so the ref is named from it.
80//! let base_tree = facet_git_tree::serialize_into(&issue, &objects).expect("ser");
81//! let base = write_commit(&objects, &CommitSpec { tree: base_tree, parents: vec![], message: "Open".into(), seconds: 300 }, Some(&jdc));
82//! let name: gix::refs::FullName = format!("refs/meta/issues/{base}").try_into().expect("valid");
83//!
84//! let mut renamed = issue.clone();
85//! renamed.title = "renamed".into();
86//! let ours_tree = facet_git_tree::serialize_into(&renamed, &objects).expect("ser");
87//! let ours = write_commit(&objects, &CommitSpec { tree: ours_tree, parents: vec![base], message: "Rename".into(), seconds: 400 }, Some(&jdc));
88//!
89//! let mut closed = issue.clone();
90//! closed.state = "closed".into();
91//! let theirs_tree = facet_git_tree::serialize_into(&closed, &objects).expect("ser");
92//! let theirs = write_commit(&objects, &CommitSpec { tree: theirs_tree, parents: vec![base], message: "Close".into(), seconds: 400 }, Some(&jdc));
93//!
94//! let author = gix::actor::Signature {
95//! name: "jdc".into(), email: "jdc@ents.test".into(),
96//! time: gix::date::Time { seconds: 500, offset: 0 },
97//! };
98//! let heads = Heads { refname: name.clone(), ours: Some(ours), theirs };
99//! let Merged::Tip(tip) =
100//! merge_heads(&objects, &heads, &author, "Merge divergent heads", |p| jdc.sign(p)).expect("merges")
101//! else { panic!("a same-actor divergence merges cleanly") };
102//!
103//! // The merged tree carries *both* disjoint edits — the schema-aware
104//! // property this crate's tests pin field-by-field. Here we show the
105//! // consequence that matters to sync: the merge tip satisfies the tip
106//! // invariant, so the gate accepts it advancing the ref from `ours`.
107//! let snapshot = refs.fetched_copy();
108//! snapshot.set(name.as_ref(), ours);
109//! let verdict = verify(&snapshot, &objects, &Update { name, new: Some(tip) }).expect("evaluates");
110//! assert!(matches!(verdict, Verdict::Pass(_)));
111//! ```
112
113mod error;
114mod objects;
115
116pub mod merge;
117pub mod preflight;
118pub mod resolve;
119pub mod transfer;
120
121pub use error::{Error, Result};
122pub use merge::{Merge, three_way};
123pub use preflight::{PreFlight, inbox_route, preflight};
124pub use resolve::{Heads, Merged, merge_heads};
125pub use transfer::{Diverged, FetchReport, Pushed, fetch, push};