git-ents.gitmain
⌘K
foforge
effect.rs237 lines · 7.9 KB · rusthistorycomment on this file
1//! `git ents effect`: define, list, show, run, and log effects
2//! (`model.effect-definition`, `effect.local-run`).
3
4use ents_effect::run::{run_effect, short_oid};
5use ents_model::{Effect, ResultRecord, Status, namespace};
6use ents_receive::{Identity, propose_entity};
7use gix_ref_store::RefStoreRead;
8
9use super::{actor, signer};
10use crate::error::{Error, Result};
11use crate::mutate::outcome_to_result;
12use crate::root::LocalRoot;
13
14/// `git ents effect list`: every effect currently defined.
15///
16/// # Errors
17///
18/// Propagates a ref-store or object read failure.
19pub fn list(root: &LocalRoot) -> Result<Vec<(String, Effect)>> {
20 let mut out = Vec::new();
21 for entry in root.refs.iter_prefix("refs/meta/effects/")? {
22 let (name, tip) = entry?;
23 let path = name.as_bstr().to_string();
24 let Some(short) = path.strip_prefix("refs/meta/effects/") else {
25 continue;
26 };
27 if short.is_empty() || short.contains('/') {
28 continue;
29 }
30 let tree = super::commit_tree(&root.objects, tip)?;
31 if let Ok(effect) = facet_git_tree::deserialize::<Effect>(&tree, &root.objects) {
32 out.push((short.to_owned(), effect));
33 }
34 }
35 Ok(out)
36}
37
38/// `git ents effect add`: define (or replace) `name`.
39///
40/// # Errors
41///
42/// See [`crate::mutate::outcome_to_result`].
43pub fn add(
44 root: &LocalRoot,
45 name: &str,
46 on: String,
47 run: String,
48 toolchains: Vec<String>,
49 key: Option<std::path::PathBuf>,
50) -> Result<()> {
51 // Validate the trigger parses before it is ever written — a malformed
52 // trigger would otherwise be silently skipped by every future
53 // reconciliation scan (`ents_receive::reconcile`'s own tolerance rule).
54 let _: ents_query::Query = on
55 .parse()
56 .map_err(|_source| Error::InvalidArgument(format!("unparsable trigger: {on}")))?;
57
58 let signer = signer(root, key)?;
59 let effect = Effect {
60 name: name.to_owned(),
61 trigger: on,
62 toolchains,
63 run,
64 };
65 let ref_name = namespace::effect_ref(name)?;
66 let identity = Identity {
67 actor: actor(&signer),
68 author: None,
69 sign: &|payload| signer.sign(payload),
70 };
71 let outcome = propose_entity(
72 &root.refs,
73 &root.objects,
74 &root.events,
75 ref_name,
76 &effect,
77 &identity,
78 &format!("Define effect {name}"),
79 root.mode(),
80 )?;
81 outcome_to_result(outcome, None)?;
82 Ok(())
83}
84
85/// `git ents effect show`: the definition, plus its result at `at` when
86/// given.
87///
88/// # Errors
89///
90/// [`Error::NotFound`] if `name` has no effect definition.
91pub fn show(root: &LocalRoot, name: &str, at: Option<String>) -> Result<(Effect, Option<Status>)> {
92 let ref_name = namespace::effect_ref(name)?;
93 let Some(tip) = root.refs.get(ref_name.as_ref())? else {
94 return Err(Error::NotFound {
95 what: format!("effect {name}"),
96 });
97 };
98 let tree = super::commit_tree(&root.objects, tip)?;
99 let effect = facet_git_tree::deserialize::<Effect>(&tree, &root.objects)?;
100
101 let status = match at {
102 None => None,
103 Some(commit) => {
104 let oid = resolve_commit(root, &commit)?;
105 let results_ref = namespace::result_ref(name, &short_oid(oid))?;
106 match root.refs.get(results_ref.as_ref())? {
107 None => None,
108 Some(result_tip) => {
109 let tree = super::commit_tree(&root.objects, result_tip)?;
110 facet_git_tree::deserialize::<ResultRecord>(&tree, &root.objects)
111 .ok()
112 .map(|record| record.status)
113 }
114 }
115 }
116 };
117 Ok((effect, status))
118}
119
120/// `git ents effect run`: run `name` locally against every outstanding
121/// commit, or a single `at` — no queue, identical materialization and
122/// sandbox path to a hosted worker (`effect.local-run`).
123///
124/// # Errors
125///
126/// Propagates any failure `ents_effect::run::run_effect` reports.
127#[expect(
128 clippy::result_large_err,
129 reason = "the closure passed to run_effect below is typed against ents_effect::Error, that \
130 crate's own Result shape, not this crate's to box"
131)]
132pub fn run(
133 root: &LocalRoot,
134 name: &str,
135 at: Option<String>,
136 key: Option<std::path::PathBuf>,
137 executor: &dyn ents_effect::Executor,
138) -> Result<Vec<(gix_hash::ObjectId, ents_receive::Outcome)>> {
139 let ref_name = namespace::effect_ref(name)?;
140 let Some(tip) = root.refs.get(ref_name.as_ref())? else {
141 return Err(Error::NotFound {
142 what: format!("effect {name}"),
143 });
144 };
145 let tree = super::commit_tree(&root.objects, tip)?;
146 let effect = facet_git_tree::deserialize::<Effect>(&tree, &root.objects)?;
147
148 let signer = signer(root, key)?;
149 let at_oid = at.map(|rev| resolve_commit(root, &rev)).transpose()?;
150
151 let scratch = tempfile::tempdir().map_err(|source| Error::Io {
152 path: root.path.clone(),
153 source,
154 })?;
155 let cache = tempfile::tempdir().map_err(|source| Error::Io {
156 path: root.path.clone(),
157 source,
158 })?;
159
160 // `run_effect` no longer resolves toolchain names itself: resolve and
161 // materialize each of the effect's declared toolchains here, before
162 // handing the run loop an already-materialized slice.
163 let mut toolchains = Vec::with_capacity(effect.toolchains.len());
164 for toolchain_name in &effect.toolchains {
165 let (_, recipe) = ents_kiln::toolchain::resolve(&root.refs, &root.objects, toolchain_name)?;
166 let bin = ents_kiln::toolchain::materialize(&recipe, &root.objects, cache.path())?;
167 toolchains.push((toolchain_name.clone(), bin));
168 }
169
170 let author = actor(&signer);
171 let outcomes = run_effect(
172 &root.refs,
173 &root.objects,
174 &root.events,
175 executor,
176 scratch.path(),
177 &toolchains,
178 name,
179 &effect,
180 at_oid,
181 |short| canonical_result_ref(name, short),
182 &author,
183 &|payload| signer.sign(payload),
184 root.mode(),
185 )?;
186 Ok(outcomes)
187}
188
189/// Build the canonical results refname for one run, in the shape
190/// `run_effect`'s own `results_ref` parameter expects.
191///
192/// # Errors
193///
194/// Never in practice: `name` is an already-defined effect and `short` is
195/// always a hex oid slice ([`ents_effect::run::short_oid`]'s own shape), so
196/// both always compose into a well-formed refname; kept fallible only
197/// because `ents_effect::run::run_effect`'s own signature requires it.
198#[expect(
199 clippy::result_large_err,
200 reason = "the Result shape is ents_effect::run_effect's own signature, not this crate's to box"
201)]
202fn canonical_result_ref(name: &str, short: &str) -> ents_effect::Result<gix::refs::FullName> {
203 #[expect(
204 clippy::expect_used,
205 clippy::unwrap_in_result,
206 reason = "see this function's own doc: always well-formed in practice"
207 )]
208 Ok(namespace::result_ref(name, short).expect("well-formed refname segments"))
209}
210
211/// `git ents effect log`: every recorded result for `name`, keyed by the
212/// full oid of the judged commit (the identity `model.result-identity`
213/// binds, and what `results(...)` queries match on).
214///
215/// # Errors
216///
217/// Propagates a ref-store or object read failure.
218pub fn log(root: &LocalRoot, name: &str) -> Result<Vec<(gix_hash::ObjectId, ResultRecord)>> {
219 let prefix = format!("refs/meta/results/{name}/");
220 let mut out = Vec::new();
221 for entry in root.refs.iter_prefix(&prefix)? {
222 let (_, tip) = entry?;
223 let tree = super::commit_tree(&root.objects, tip)?;
224 if let Ok(record) = facet_git_tree::deserialize::<ResultRecord>(&tree, &root.objects) {
225 out.push((record.target(), record));
226 }
227 }
228 Ok(out)
229}
230
231fn resolve_commit(root: &LocalRoot, rev: &str) -> Result<gix_hash::ObjectId> {
232 let repo = gix::open(&root.path)?;
233 let id = repo
234 .rev_parse_single(rev)
235 .map_err(|source| Error::InvalidArgument(format!("cannot resolve {rev}: {source}")))?;
236 Ok(id.detach())
237}