git_policy_cli/policy_transforms.rs
1//! What:
2//! The fixed argument transforms of a forwarded command,
3//! in their fixed order:
4//! atomic push,
5//! commit-only,
6//! status hints off.
7//! Why:
8//! These are not policies:
9//! they have no severity and cannot be configured.
10//! They add
11//! `--atomic` to `git push`,
12//! `-o` to `git commit` and `-c advice.statusHints=false`
13//! before `git status`,
14//! and the commit-only transform can reject a commit.
15//! The
16//! decisions belong to the pure rule cores;
17//! this module runs them in order and
18//! fetches the two repository facts commit-only may ask for.
19//!
20//! In TS you'd write (pseudocode):
21//! ```ts
22//! // const { args, events, complete } = await applyFixedTransforms({ args, rawArgs, sequence });
23//! ```
24
25/// What:
26/// `use` brings names from sibling files into this file;
27/// `super::` means "the parent
28/// module",
29/// where every sibling file of this crate is declared.
30/// Why:
31/// Each transform is one rule core;
32/// the facts interface answers what a core asks.
33///
34/// In TS you'd write (pseudocode):
35/// ```ts
36/// import { atomicPush } from '../rule/atomic-push.ts';
37/// ```
38use super::command_options::OptionError;
39use super::diagnostics::EngineFailureCode;
40use super::policy_events::PolicyEvent;
41use super::repository_facts::RepositoryFacts;
42use super::rule_argument_rewrite::ArgumentRewrite;
43use super::rule_atomic_push::atomic_push;
44use super::rule_commit_only::{
45 CommitOnlyDecision, decide_commit_only, resolve_index_state, resolve_sequencer_state,
46 violation_code_text,
47};
48use super::rule_status_hints::status_hints_off;
49/// What:
50/// `OsString` is owned operating-system text of raw bytes.
51/// Sibling the reader might
52/// expect:
53/// `String`,
54/// which must be valid UTF-8.
55/// Why:
56/// Arguments are forwarded with their exact bytes.
57///
58/// In TS you'd write (pseudocode):
59/// ```ts
60/// // string, but byte-preserving.
61/// ```
62use std::ffi::OsString;
63
64/// What:
65/// The identifier of the commit-only transform in its events.
66/// `&str` is borrowed
67/// text baked into the program.
68/// Why:
69/// A fixed-transform rejection names its transform where a finding names its policy.
70///
71/// In TS you'd write (pseudocode):
72/// ```ts
73/// const COMMIT_ONLY_CORE = 'commit-only';
74/// ```
75pub const COMMIT_ONLY_CORE: &str = "commit-only";
76
77/// What:
78/// What the transform stage produced.
79/// A `struct` is a record with named fields;
80/// `Vec<T>` is an owned list;
81/// `bool` is true or false.
82/// `#[derive(...)]` asks the
83/// compiler to generate cloning,
84/// debug printing and `==`.
85/// Why:
86/// The caller forwards `arguments` only when no event blocks and `complete` is true.
87///
88/// In TS you'd write (pseudocode):
89/// ```ts
90/// type TransformResult = { args: string[]; events: PolicyEvent[]; complete: boolean };
91/// ```
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub struct TransformResult {
94 /// The arguments to forward;
95 /// the stage's input when a transform rejected or failed.
96 pub arguments: Vec<OsString>,
97 /// Nothing,
98 /// one rejection,
99 /// or one engine failure.
100 pub events: Vec<PolicyEvent>,
101 /// False when a transform could not decide.
102 pub complete: bool,
103}
104
105/// What:
106/// The result of a transform that could not read a fact.
107/// `&[OsString]` borrows the
108/// stage's input;
109/// `String` is the owned explanation.
110/// Why:
111/// A transform that cannot decide stops the command with an engine failure.
112///
113/// In TS you'd write (pseudocode):
114/// ```ts
115/// return { args, events: [createEngineFailureEvent({ code: 'core-incomplete', message })], complete: false };
116/// ```
117fn incomplete(arguments: &[OsString], message: String) -> TransformResult {
118 return TransformResult {
119 // `.to_vec()` copies the borrowed list into an owned one.
120 arguments: arguments.to_vec(),
121 // `vec![...]` is a macro building an owned list from literal items.
122 events: vec![PolicyEvent::EngineFailure {
123 code: EngineFailureCode::CoreIncomplete,
124 message,
125 // `None` is the "absent" case of `Option`.
126 trigger: None,
127 policy: None,
128 path: None,
129 }],
130 complete: false,
131 };
132}
133
134/// What:
135/// Ask for the repository fact a commit-only decision still needs,
136/// and finish the
137/// decision.
138/// `&mut dyn RepositoryFacts` lends "any facts provider" for writing;
139/// `Result<A, B>` is "either success `A` or failure `B`".
140/// Why:
141/// Most commits are decided from their arguments;
142/// only a pathless commit asks
143/// whether an operation awaits its conclusion or whether the index is dirty.
144///
145/// In TS you'd write (pseudocode):
146/// ```ts
147/// async function settleCommitOnly(decision, args, facts): Promise<CommitOnlyDecision>; // throws
148/// ```
149fn settle_commit_only(
150 decision: CommitOnlyDecision,
151 arguments: &[OsString],
152 facts: &mut dyn RepositoryFacts,
153) -> Result<CommitOnlyDecision, String> {
154 // `match` picks one arm per variant; `other` binds every decision that needs no fact.
155 match decision {
156 CommitOnlyDecision::NeedsSequencerState => {
157 // A trailing `?` returns the failure to our caller, or unwraps the state.
158 return Ok(resolve_sequencer_state(facts.sequencer_state()?));
159 }
160 CommitOnlyDecision::NeedsIndexState(pending) => {
161 // `&pending` lends the pending insertion read-only.
162 return Ok(resolve_index_state(
163 arguments,
164 &pending,
165 facts.index_vs_head()?,
166 ));
167 }
168 other => return Ok(other),
169 }
170}
171
172/// What:
173/// Run the commit-only transform.
174/// `bool` says whether `--no-enforce-only` was given.
175/// Why:
176/// The hatch skips the transform entirely;
177/// its token is already gone from the
178/// arguments.
179/// A region Git itself refuses is left to Git.
180///
181/// In TS you'd write (pseudocode):
182/// ```ts
183/// async function commitOnlyStage(args, escaped, facts): Promise<TransformResult>;
184/// ```
185fn commit_only_stage(
186 arguments: &[OsString],
187 escaped: bool,
188 facts: &mut dyn RepositoryFacts,
189) -> TransformResult {
190 let unchanged: TransformResult = TransformResult {
191 arguments: arguments.to_vec(),
192 // `Vec::<PolicyEvent>::new()` is an empty owned list.
193 events: Vec::<PolicyEvent>::new(),
194 complete: true,
195 };
196 if escaped {
197 return unchanged;
198 }
199 // `&[]` is an empty flag list: wrapper controls were removed before this point.
200 let decided: Result<CommitOnlyDecision, OptionError> = decide_commit_only(arguments, &[]);
201 // `let Ok(x) = ... else { ... };` unwraps the decision or returns for a refused region.
202 let Ok(first) = decided else {
203 return unchanged;
204 };
205 let settled: CommitOnlyDecision = match settle_commit_only(first, arguments, facts) {
206 Ok(decision) => decision,
207 Err(message) => return incomplete(arguments, message),
208 };
209 match settled {
210 CommitOnlyDecision::Unchanged => return unchanged,
211 CommitOnlyDecision::Rewritten(rewritten) => {
212 return TransformResult {
213 arguments: rewritten,
214 events: Vec::<PolicyEvent>::new(),
215 complete: true,
216 };
217 }
218 CommitOnlyDecision::Rejected(violation) => {
219 return TransformResult {
220 arguments: arguments.to_vec(),
221 events: vec![PolicyEvent::CoreFinding {
222 core_id: COMMIT_ONLY_CORE,
223 code: violation_code_text(violation.code),
224 message: violation.message,
225 }],
226 complete: true,
227 };
228 }
229 // A finished decision never asks again; one that did is refused, not guessed.
230 CommitOnlyDecision::NeedsSequencerState | CommitOnlyDecision::NeedsIndexState(_) => {
231 return incomplete(
232 arguments,
233 String::from(
234 "cli-git: the commit-only transform asked for a second repository fact \
235 and cannot decide.",
236 ),
237 );
238 }
239 }
240}
241
242/// What:
243/// Apply the three transforms in order.
244/// `&[OsString]` borrows the arguments,
245/// already
246/// free of wrapper controls.
247/// Why:
248/// The order is the incumbent's:
249/// atomic push,
250/// commit-only,
251/// status hints.
252/// A rejection
253/// or failure of commit-only ends the stage with the stage's input unchanged.
254///
255/// In TS you'd write (pseudocode):
256/// ```ts
257/// async function applyFixedTransforms({ args, rawArgs, sequence }): Promise<FixedTransformResult>;
258/// ```
259pub fn apply_fixed_transforms(
260 arguments: &[OsString],
261 commit_only_escaped: bool,
262 facts: &mut dyn RepositoryFacts,
263) -> TransformResult {
264 // `match` unpacks the rewrite; a region Git refuses (`Err`) is left as it is.
265 let after_push: Vec<OsString> = match atomic_push(arguments, &[]) {
266 Ok(ArgumentRewrite::Rewritten(rewritten)) => rewritten,
267 Ok(ArgumentRewrite::Unchanged) | Err(_) => arguments.to_vec(),
268 };
269 // `.as_slice()` lends the owned list as a borrowed view.
270 let commit: TransformResult =
271 commit_only_stage(after_push.as_slice(), commit_only_escaped, facts);
272 if !commit.complete || !commit.events.is_empty() {
273 return TransformResult {
274 arguments: arguments.to_vec(),
275 events: commit.events,
276 complete: commit.complete,
277 };
278 }
279 let after_status: Vec<OsString> = match status_hints_off(commit.arguments.as_slice()) {
280 ArgumentRewrite::Rewritten(rewritten) => rewritten,
281 ArgumentRewrite::Unchanged => commit.arguments,
282 };
283 return TransformResult {
284 arguments: after_status,
285 events: Vec::<PolicyEvent>::new(),
286 complete: true,
287 };
288}
289
290/// Order,
291/// rejection and failure controls stay out of the release executable.
292#[cfg(test)]
293#[path = "policy_transforms_tests.rs"]
294mod tests;