git-ents.gitmain
⌘K
foforge
seed.rs231 lines · 7.7 KB · rusthistorycomment on this file
1//! Seeding helpers: members, meta entities, results, and code-ref history.
2
3use ents_model::{Member, MemberId, Provenance, ResultRecord, Status, namespace};
4use gix::refs::FullName;
5use gix_hash::ObjectId;
6use gix_object::{Find, Kind, Write};
7
8use crate::commit::{CommitSpec, write_commit};
9use crate::keys::Keypair;
10use crate::refs::MemRefStore;
11
12/// Write the empty tree into `objects` and return its id.
13///
14/// # Examples
15///
16/// ```
17/// use ents_testutil::{ObjectStore, empty_tree};
18///
19/// let objects = ObjectStore::default();
20/// let tree = empty_tree(&objects);
21/// assert_eq!(tree.to_string(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
22/// ```
23pub fn empty_tree(objects: &impl Write) -> ObjectId {
24 objects
25 .write_buf(Kind::Tree, b"")
26 .expect("in-memory object write cannot fail")
27}
28
29/// Serialize `entity` as its typed tree and land it on `refname` as a
30/// mutation commit signed by `signer` when one is given. Parents come from
31/// `refname`'s current tip. Returns the new tip. The refname is bound to
32/// the signed content by the gate (`meta-ref.identity-binding`), not by
33/// any commit trailer.
34///
35/// # Examples
36///
37/// ```
38/// use ents_testutil::{Keypair, MemRefStore, ObjectStore, write_meta_entity};
39/// use gix_ref_store::RefStoreRead;
40///
41/// let refs = MemRefStore::default();
42/// let objects = ObjectStore::default();
43/// let name: gix::refs::FullName = "refs/meta/redactions/1".try_into().expect("valid");
44///
45/// let redaction = ents_model::Redaction::new(
46/// gix_hash::ObjectId::null(gix_hash::Kind::Sha1),
47/// "leaked credential",
48/// );
49/// let tip = write_meta_entity(&refs, &objects, name.clone(), &redaction, None, 1_000);
50/// assert_eq!(refs.get(name.as_ref()).expect("readable"), Some(tip));
51/// ```
52pub fn write_meta_entity<T: for<'facet> facet::Facet<'facet>>(
53 refs: &MemRefStore,
54 objects: &(impl Write + Find),
55 refname: FullName,
56 entity: &T,
57 signer: Option<&Keypair>,
58 seconds: i64,
59) -> ObjectId {
60 let tree =
61 facet_git_tree::serialize_into(entity, objects).expect("fixture entity always serializes");
62 let message = format!("Mutate {}", refname.as_bstr());
63 let parents = crate::refs_get(refs, &refname).into_iter().collect();
64 let tip = write_commit(
65 objects,
66 &CommitSpec {
67 tree,
68 parents,
69 message,
70 seconds,
71 },
72 signer,
73 );
74 refs.set(refname.as_ref(), tip);
75 tip
76}
77
78/// Enroll `id` as a member whose key is `key`'s public half, active, with
79/// the given provenance, and return the member ref's new tip.
80///
81/// The enrollment commit is signed by the member's own key — the
82/// self-admitting shape the bootstrap window admits.
83///
84/// # Examples
85///
86/// ```
87/// use ents_model::Provenance;
88/// use ents_testutil::{Keypair, MemRefStore, ObjectStore, enroll_member};
89///
90/// let refs = MemRefStore::default();
91/// let objects = ObjectStore::default();
92/// enroll_member(&refs, &objects, "jdc", &Keypair::from_seed(1), Provenance::AdminRegistered, 500);
93/// ```
94pub fn enroll_member(
95 refs: &MemRefStore,
96 objects: &(impl Write + Find),
97 id: &str,
98 key: &Keypair,
99 provenance: Provenance,
100 seconds: i64,
101) -> ObjectId {
102 let member = Member::new(id, key.public_openssh(), provenance);
103 write_member(refs, objects, id, &member, Some(key), seconds)
104}
105
106/// Land an arbitrary [`Member`] state on `refs/meta/member/<id>` — the
107/// general form of [`enroll_member`], for revocations, unrevocations, and
108/// promotions, signed by whoever `signer` is (an admin, usually).
109///
110/// # Examples
111///
112/// ```
113/// use ents_model::{Member, Provenance};
114/// use ents_testutil::{Keypair, MemRefStore, ObjectStore, write_member};
115///
116/// let refs = MemRefStore::default();
117/// let objects = ObjectStore::default();
118/// let key = Keypair::from_seed(1);
119///
120/// let mut member = Member::new("jdc", key.public_openssh(), Provenance::AdminRegistered);
121/// member.revoke();
122/// write_member(&refs, &objects, "jdc", &member, Some(&key), 900);
123/// ```
124pub fn write_member(
125 refs: &MemRefStore,
126 objects: &(impl Write + Find),
127 id: &str,
128 member: &Member,
129 signer: Option<&Keypair>,
130 seconds: i64,
131) -> ObjectId {
132 let refname = namespace::member_ref(&MemberId::new(id)).expect("valid member id in fixture");
133 write_meta_entity(refs, objects, refname, member, signer, seconds)
134}
135
136/// Record a result of `status` for `effect` on the commit whose hex id
137/// starts with `short_oid`, at the canonical
138/// `refs/meta/results/<effect>/<short_oid>` ref (`effect.results-writeback`).
139///
140/// The tree is a [`ResultRecord`] carrying `effect` and a target oid whose
141/// hex begins with `short_oid` (`model.result-identity`): when `short_oid`
142/// is a hex prefix it is right-padded with zeros to a full oid, so the
143/// gate's identity binding recomputes the ref; when it is not hex, a null
144/// target is used, sufficient for query scan tests that never gate.
145///
146/// The result commit is unsigned unless `signer` is given — query tests
147/// exercise scan semantics, gate tests exercise signatures.
148///
149/// # Examples
150///
151/// ```
152/// use ents_model::Status;
153/// use ents_testutil::{MemRefStore, ObjectStore, record_result};
154///
155/// let refs = MemRefStore::default();
156/// let objects = ObjectStore::default();
157/// record_result(&refs, &objects, "unit", "abc123", Status::Pass, None, 1_000);
158/// ```
159pub fn record_result(
160 refs: &MemRefStore,
161 objects: &(impl Write + Find),
162 effect: &str,
163 short_oid: &str,
164 status: Status,
165 signer: Option<&Keypair>,
166 seconds: i64,
167) -> ObjectId {
168 let refname =
169 namespace::result_ref(effect, short_oid).expect("valid result segments in fixture");
170 let target = target_for(short_oid);
171 let record = ResultRecord::new(effect, target, status);
172 write_meta_entity(refs, objects, refname, &record, signer, seconds)
173}
174
175/// An oid whose hex form begins with `short_oid`: right-pad a hex prefix
176/// with zeros to 40 chars, or fall back to the null oid when `short_oid`
177/// is not hex (query scan fixtures do not gate on the target).
178fn target_for(short_oid: &str) -> ObjectId {
179 let padded = format!("{short_oid:0<40}");
180 ObjectId::from_hex(padded.as_bytes()).unwrap_or_else(|_| ObjectId::null(gix_hash::Kind::Sha1))
181}
182
183/// Append `count` empty-tree commits on top of `refname`'s current tip
184/// (creating the ref if absent), one second apart starting at
185/// `start_seconds`, and return the new commits oldest-first.
186///
187/// # Examples
188///
189/// ```
190/// use ents_testutil::{MemRefStore, ObjectStore, advance_ref};
191/// use gix_ref_store::RefStoreRead;
192///
193/// let refs = MemRefStore::default();
194/// let objects = ObjectStore::default();
195/// let commits = advance_ref(&refs, &objects, "refs/heads/main", 3, 100);
196/// assert_eq!(commits.len(), 3);
197///
198/// let name: gix::refs::FullName = "refs/heads/main".try_into().expect("valid");
199/// assert_eq!(refs.get(name.as_ref()).expect("readable"), commits.last().copied());
200/// ```
201pub fn advance_ref(
202 refs: &MemRefStore,
203 objects: &(impl Write + Find),
204 refname: &str,
205 count: usize,
206 start_seconds: i64,
207) -> Vec<ObjectId> {
208 let name: FullName = refname.try_into().expect("valid refname in fixture");
209 let tree = empty_tree(objects);
210 let mut tip = crate::refs_get(refs, &name);
211 let mut out = Vec::with_capacity(count);
212 for i in 0..count {
213 let seconds = start_seconds.saturating_add(i64::try_from(i).unwrap_or(i64::MAX));
214 let commit = write_commit(
215 objects,
216 &CommitSpec {
217 tree,
218 parents: tip.into_iter().collect(),
219 message: format!("{refname} commit {i} at {seconds}"),
220 seconds,
221 },
222 None,
223 );
224 tip = Some(commit);
225 out.push(commit);
226 }
227 if let Some(tip) = tip {
228 refs.set(name.as_ref(), tip);
229 }
230 out
231}