git_policy_cli/policy/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 crate::command_options::OptionError;
39use crate::diagnostics::EngineFailureCode;
40use crate::policy_events::PolicyEvent;
41use crate::repository_facts::RepositoryFacts;
42use crate::rule_argument_rewrite::ArgumentRewrite;
43use crate::rule_atomic_push::atomic_push;
44use crate::rule_commit_only::{
45 CommitOnlyDecision, decide_commit_only, resolve_index_state, resolve_sequencer_state,
46 violation_code_text,
47};
48use crate::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 hint: crate::policy_events::EngineFailureHint::OfCode,
125 message,
126 // `None` is the "absent" case of `Option`.
127 trigger: None,
128 policy: None,
129 path: None,
130 }],
131 complete: false,
132 };
133}
134
135/// What:
136/// Ask for the repository fact a commit-only decision still needs,
137/// and finish the
138/// decision.
139/// `&mut dyn RepositoryFacts` lends "any facts provider" for writing;
140/// `Result<A, B>` is "either success `A` or failure `B`".
141/// Why:
142/// Most commits are decided from their arguments;
143/// only a pathless commit asks
144/// whether an operation awaits its conclusion or whether the index is dirty.
145///
146/// In TS you'd write (pseudocode):
147/// ```ts
148/// async function settleCommitOnly(decision, args, facts): Promise<CommitOnlyDecision>; // throws
149/// ```
150fn settle_commit_only(
151 decision: CommitOnlyDecision,
152 arguments: &[OsString],
153 facts: &mut dyn RepositoryFacts,
154) -> Result<CommitOnlyDecision, String> {
155 // `match` picks one arm per variant; `other` binds every decision that needs no fact.
156 match decision {
157 CommitOnlyDecision::NeedsSequencerState => {
158 // A trailing `?` returns the failure to our caller, or unwraps the state.
159 return Ok(resolve_sequencer_state(facts.sequencer_state()?));
160 }
161 CommitOnlyDecision::NeedsIndexState(pending) => {
162 // `&pending` lends the pending insertion read-only.
163 return Ok(resolve_index_state(
164 arguments,
165 &pending,
166 facts.index_vs_head()?,
167 ));
168 }
169 other => return Ok(other),
170 }
171}
172
173/// What:
174/// Run the commit-only transform.
175/// `bool` says whether `--no-enforce-only` was given.
176/// Why:
177/// The hatch skips the transform entirely;
178/// its token is already gone from the
179/// arguments.
180/// A region Git itself refuses is left to Git.
181///
182/// In TS you'd write (pseudocode):
183/// ```ts
184/// async function commitOnlyStage(args, escaped, facts): Promise<TransformResult>;
185/// ```
186fn commit_only_stage(
187 arguments: &[OsString],
188 escaped: bool,
189 facts: &mut dyn RepositoryFacts,
190) -> TransformResult {
191 let unchanged: TransformResult = TransformResult {
192 arguments: arguments.to_vec(),
193 // `Vec::<PolicyEvent>::new()` is an empty owned list.
194 events: Vec::<PolicyEvent>::new(),
195 complete: true,
196 };
197 if escaped {
198 return unchanged;
199 }
200 // `&[]` is an empty flag list: wrapper controls were removed before this point.
201 let decided: Result<CommitOnlyDecision, OptionError> = decide_commit_only(arguments, &[]);
202 // `let Ok(x) = ... else { ... };` unwraps the decision or returns for a refused region.
203 let Ok(first) = decided else {
204 return unchanged;
205 };
206 let settled: CommitOnlyDecision = match settle_commit_only(first, arguments, facts) {
207 Ok(decision) => decision,
208 Err(message) => return incomplete(arguments, message),
209 };
210 match settled {
211 CommitOnlyDecision::Unchanged => return unchanged,
212 CommitOnlyDecision::Rewritten(rewritten) => {
213 return TransformResult {
214 arguments: rewritten,
215 events: Vec::<PolicyEvent>::new(),
216 complete: true,
217 };
218 }
219 CommitOnlyDecision::Rejected(violation) => {
220 return TransformResult {
221 arguments: arguments.to_vec(),
222 events: vec![PolicyEvent::CoreFinding {
223 core_id: COMMIT_ONLY_CORE,
224 code: violation_code_text(violation.code),
225 message: violation.message,
226 }],
227 complete: true,
228 };
229 }
230 // A finished decision never asks again; one that did is refused, not guessed.
231 CommitOnlyDecision::NeedsSequencerState | CommitOnlyDecision::NeedsIndexState(_) => {
232 return incomplete(
233 arguments,
234 String::from(
235 "cli-git: the commit-only transform asked for a second repository fact \
236 and cannot decide.",
237 ),
238 );
239 }
240 }
241}
242
243/// What:
244/// Apply the three transforms in order.
245/// `&[OsString]` borrows the arguments,
246/// already
247/// free of wrapper controls.
248/// Why:
249/// The order is the incumbent's:
250/// atomic push,
251/// commit-only,
252/// status hints.
253/// A rejection
254/// or failure of commit-only ends the stage with the stage's input unchanged.
255///
256/// In TS you'd write (pseudocode):
257/// ```ts
258/// async function applyFixedTransforms({ args, rawArgs, sequence }): Promise<FixedTransformResult>;
259/// ```
260pub fn apply_fixed_transforms(
261 arguments: &[OsString],
262 commit_only_escaped: bool,
263 facts: &mut dyn RepositoryFacts,
264) -> TransformResult {
265 // `match` unpacks the rewrite; a region Git refuses (`Err`) is left as it is.
266 let after_push: Vec<OsString> = match atomic_push(arguments, &[]) {
267 Ok(ArgumentRewrite::Rewritten(rewritten)) => rewritten,
268 Ok(ArgumentRewrite::Unchanged) | Err(_) => arguments.to_vec(),
269 };
270 // `.as_slice()` lends the owned list as a borrowed view.
271 let commit: TransformResult =
272 commit_only_stage(after_push.as_slice(), commit_only_escaped, facts);
273 if !commit.complete || !commit.events.is_empty() {
274 return TransformResult {
275 arguments: arguments.to_vec(),
276 events: commit.events,
277 complete: commit.complete,
278 };
279 }
280 let after_status: Vec<OsString> = match status_hints_off(commit.arguments.as_slice()) {
281 ArgumentRewrite::Rewritten(rewritten) => rewritten,
282 ArgumentRewrite::Unchanged => commit.arguments,
283 };
284 return TransformResult {
285 arguments: after_status,
286 events: Vec::<PolicyEvent>::new(),
287 complete: true,
288 };
289}
290
291/// Order,
292/// rejection and failure controls stay out of the release executable.
293#[cfg(test)]
294#[path = "policy_transforms_tests.rs"]
295mod tests;