Skip to main content

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;