Skip to main content

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;