Skip to main content

git_policy_cli/
wrapper_controls.rs

1//! What:
2//!  The wrapper-only control tokens,
3//!  what each one means,
4//!  and their removal from the
5//!       arguments that come before the Git subcommand.
6//! Why:
7//!  Git refuses options it does not know,
8//!  so every control must be gone before Git runs,
9//!      and every policy decision must read the arguments without them.
10//!  A control is
11//!      recognized only where Git itself would look for an option,
12//!  and it is removed by its
13//!      position,
14//!  never by its spelling,
15//!  so an equal-looking value or path is kept.
16//!
17//! In TS you'd write (pseudocode):
18//! ```ts
19//! // const { args, keepGoing, escapedPolicyIds } = parsePolicyControls({ args: rawArgs });
20//! ```
21
22/// What:
23///  `use` brings names from sibling files into this file;
24///  `super::` means "the parent
25///       module",
26///  where every sibling file of this crate is declared.
27/// Why:
28///   The scan asks the Git 2.56.0 global-option reader where an unknown option sits,
29///       and removes tokens through the one position-based removal function.
30///
31/// In TS you'd write (pseudocode):
32/// ```ts
33/// import { withoutTokens } from './command_options_query.ts';
34/// import { globalLayout } from './global_arguments.ts';
35/// ```
36use super::command_add::ADD_ESCAPE_HATCH;
37use super::command_options_query::without_tokens;
38use super::escape_hatch::{
39    BRANCH_WORKTREE_ESCAPE_HATCH, WORKTREE_COPY_ESCAPE_HATCH, WORKTREE_ENFORCEMENT_ESCAPE_HATCH,
40};
41use super::global_arguments::{GlobalLayout, GlobalOutcome, global_layout};
42use super::policy_registry::PolicyId;
43/// What:
44///  `OsString` is owned operating-system text of raw bytes.
45///  Sibling the reader might
46///       expect:
47///  `String`,
48///  which must be valid UTF-8.
49/// Why:
50///   Arguments may hold bytes that are not UTF-8 and are forwarded unchanged.
51///
52/// In TS you'd write (pseudocode):
53/// ```ts
54/// // string, but byte-preserving.
55/// ```
56use std::ffi::OsString;
57
58/// What:
59///  The control that lets later policies run after an error finding.
60///  `&str` is borrowed
61///       text;
62///  here it is baked into the program.
63/// Why:
64///   A caller who wants every finding of one command in a single run passes it.
65///
66/// In TS you'd write (pseudocode):
67/// ```ts
68/// const KEEP_GOING_FLAG = '--cli-git-keep-going';
69/// ```
70pub const KEEP_GOING_FLAG: &str = "--cli-git-keep-going";
71
72/// What:
73///  What one control asks for.
74///  An `enum` is a closed set of named alternatives;
75///       `Escape` carries the policy it switches off.
76///  `#[derive(...)]` asks the compiler to
77///       generate copying (`Clone`,
78///  `Copy`),
79///  debug printing (`Debug`) and `==`.
80/// Why:
81///   Recognition (a spelling) and effect (a meaning) are kept apart,
82///  so two spellings
83///       of one escape cannot drift.
84///
85/// In TS you'd write (pseudocode):
86/// ```ts
87/// type ControlMeaning = { kind: 'keep-going' } | { kind: 'escape'; policy: PolicyId } | { kind: 'skip-worktree-copy' };
88/// ```
89#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub enum ControlMeaning {
91    /// Continue with later policies after an error finding.
92    KeepGoing,
93    /// Skip one policy for this invocation.
94    Escape(PolicyId),
95    /// Skip ignored-state synchronization for this invocation.
96    SkipWorktreeCopy,
97}
98
99/// What:
100///  One accepted control:
101///  its exact spelling and its meaning.
102///  A `struct` is a record
103///       with named fields;
104///  `&'static str` is text baked into the program for its whole run.
105/// Why:
106///   The table below is the single list of spellings the wrapper recognizes.
107///
108/// In TS you'd write (pseudocode):
109/// ```ts
110/// type ControlSpelling = { flag: string; meaning: ControlMeaning };
111/// ```
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub struct ControlSpelling {
114    /// The exact token;
115    ///  controls are never abbreviated and never carry `=value`.
116    pub flag: &'static str,
117    /// What the token asks for.
118    pub meaning: ControlMeaning,
119}
120
121/// What:
122///  Every control spelling.
123///  `&[ControlSpelling]` is a borrowed,
124///  read-only list (siblings:
125///       `Vec<T>`,
126///  owned and growable;
127///  `[T; N]`,
128///  fixed length in the type).
129/// Why:
130///   One `--no-enforce-<policy id>` per shipped policy,
131///  the three spellings that predate
132///       policy ids,
133///  the keep-going flag and the worktree-copy opt-out.
134///  A compiled-in list
135///       cannot be reordered or extended at run time.
136/// Gotcha:
137///  `wrapper_controls_tests.rs` pins that every shipped policy has exactly the
138///         spelling `--no-enforce-` followed by its registry name.
139///
140/// In TS you'd write (pseudocode):
141/// ```ts
142/// const CONTROL_SPELLINGS: readonly ControlSpelling[] = [ ... ];
143/// ```
144pub const CONTROL_SPELLINGS: &[ControlSpelling] = &[
145    ControlSpelling {
146        flag: KEEP_GOING_FLAG,
147        meaning: ControlMeaning::KeepGoing,
148    },
149    ControlSpelling {
150        flag: "--no-enforce-require-root",
151        meaning: ControlMeaning::Escape(PolicyId::RequireRoot),
152    },
153    ControlSpelling {
154        flag: "--no-enforce-linked-worktree-only",
155        meaning: ControlMeaning::Escape(PolicyId::LinkedWorktreeOnly),
156    },
157    ControlSpelling {
158        flag: "--no-enforce-branch-worktree-only",
159        meaning: ControlMeaning::Escape(PolicyId::BranchWorktreeOnly),
160    },
161    ControlSpelling {
162        flag: "--no-enforce-add-explicit",
163        meaning: ControlMeaning::Escape(PolicyId::AddExplicit),
164    },
165    ControlSpelling {
166        flag: "--no-enforce-final-newline",
167        meaning: ControlMeaning::Escape(PolicyId::FinalNewline),
168    },
169    ControlSpelling {
170        flag: "--no-enforce-markdown/autofix",
171        meaning: ControlMeaning::Escape(PolicyId::MarkdownAutofix),
172    },
173    ControlSpelling {
174        flag: "--no-enforce-mono/forbidden-root-context",
175        meaning: ControlMeaning::Escape(PolicyId::ForbiddenRootContext),
176    },
177    ControlSpelling {
178        flag: "--no-enforce-mono/dependent-version-bump",
179        meaning: ControlMeaning::Escape(PolicyId::DependentVersionBump),
180    },
181    ControlSpelling {
182        flag: "--no-enforce-security/forbidden-strings",
183        meaning: ControlMeaning::Escape(PolicyId::ForbiddenStrings),
184    },
185    ControlSpelling {
186        flag: WORKTREE_ENFORCEMENT_ESCAPE_HATCH,
187        meaning: ControlMeaning::Escape(PolicyId::LinkedWorktreeOnly),
188    },
189    ControlSpelling {
190        flag: BRANCH_WORKTREE_ESCAPE_HATCH,
191        meaning: ControlMeaning::Escape(PolicyId::BranchWorktreeOnly),
192    },
193    ControlSpelling {
194        flag: ADD_ESCAPE_HATCH,
195        meaning: ControlMeaning::Escape(PolicyId::AddExplicit),
196    },
197    ControlSpelling {
198        flag: WORKTREE_COPY_ESCAPE_HATCH,
199        meaning: ControlMeaning::SkipWorktreeCopy,
200    },
201];
202
203/// What:
204///  Everything the controls of one invocation asked for.
205///  `bool` is true or false;
206///       `Vec<PolicyId>` is an owned,
207///  growable list of policy identities.
208/// Why:
209///   The engine reads the effect of the removed tokens from this record,
210///  because the
211///       tokens themselves are gone from the arguments it sees.
212///
213/// In TS you'd write (pseudocode):
214/// ```ts
215/// type Controls = { keepGoing: boolean; escaped: PolicyId[]; commitOnlyEscaped: boolean; skipWorktreeCopy: boolean };
216/// ```
217#[derive(Clone, Debug, Eq, PartialEq)]
218pub struct Controls {
219    /// Later policies run after an error finding.
220    pub keep_going: bool,
221    /// Policies skipped for this invocation,
222    ///  each listed once,
223    ///  in first-use order.
224    pub escaped: Vec<PolicyId>,
225    /// `--no-enforce-only` was written in option position of `git commit`.
226    pub commit_only_escaped: bool,
227    /// Ignored-state synchronization is skipped.
228    pub skip_worktree_copy: bool,
229}
230
231/// What:
232///  The record of an invocation that used no control.
233///  `Vec::<PolicyId>::new()` is an
234///       empty owned list.
235/// Why:
236///   Every scan starts from "nothing was asked" and only adds.
237///
238/// In TS you'd write (pseudocode):
239/// ```ts
240/// const noControls = (): Controls => ({ keepGoing: false, escaped: [], commitOnlyEscaped: false, skipWorktreeCopy: false });
241/// ```
242pub fn no_controls() -> Controls {
243    return Controls {
244        keep_going: false,
245        escaped: Vec::<PolicyId>::new(),
246        commit_only_escaped: false,
247        skip_worktree_copy: false,
248    };
249}
250
251/// What:
252///  The meaning of a token that is exactly a control spelling,
253///  or nothing.
254///  `&[u8]`
255///       borrows raw bytes;
256///  `Option<T>` is "a value or nothing".
257/// Why:
258///   Arguments are compared as bytes,
259///  so a non-UTF-8 argument is never decoded.
260///
261/// In TS you'd write (pseudocode):
262/// ```ts
263/// const meaning = CONTROL_SPELLINGS.find(s => s.flag === token)?.meaning;
264/// ```
265pub fn control_meaning(token: &[u8]) -> Option<ControlMeaning> {
266    // `for spelling in CONTROL_SPELLINGS` borrows each row in table order.
267    for spelling in CONTROL_SPELLINGS {
268        // `.as_bytes()` lends the spelling's bytes for the comparison.
269        if spelling.flag.as_bytes() == token {
270            // `Some(x)` is the "present" case of `Option`.
271            return Some(spelling.meaning);
272        }
273    }
274    // `None` is the "absent" case of `Option`.
275    return None;
276}
277
278/// What:
279///  The control spellings as byte strings,
280///  in table order.
281///  `Vec<&'static [u8]>` is an
282///       owned list of borrowed byte strings baked into the program.
283/// Why:
284///   Every Git option-table reader takes this list,
285///  so it reports a control found in
286///       option position instead of refusing it as an unknown option.
287///  The position in this
288///       list is the position in `CONTROL_SPELLINGS`.
289///
290/// In TS you'd write (pseudocode):
291/// ```ts
292/// const flags = CONTROL_SPELLINGS.map(s => s.flag);
293/// ```
294pub fn control_flags() -> Vec<&'static [u8]> {
295    // `with_capacity(n)` reserves room for `n` items; `mut` allows pushing.
296    let mut flags: Vec<&'static [u8]> =
297        Vec::<&'static [u8]>::with_capacity(CONTROL_SPELLINGS.len());
298    for spelling in CONTROL_SPELLINGS {
299        flags.push(spelling.flag.as_bytes());
300    }
301    return flags;
302}
303
304/// What:
305///  Add one control's effect to the record.
306///  `&mut Controls` lends the record for
307///       writing (sibling `&Controls` lends it read-only).
308/// Why:
309///   A repeated escape must not list its policy twice:
310///  the list is compared in tests
311///       and printed in diagnostics.
312///
313/// In TS you'd write (pseudocode):
314/// ```ts
315/// function recordControl(controls: Controls, meaning: ControlMeaning): void;
316/// ```
317pub fn record_control(controls: &mut Controls, meaning: ControlMeaning) {
318    // `match` picks one arm per variant and unpacks the policy an `Escape` carries; the
319    // compiler refuses the code if a variant is forgotten.
320    match meaning {
321        ControlMeaning::KeepGoing => controls.keep_going = true,
322        ControlMeaning::SkipWorktreeCopy => controls.skip_worktree_copy = true,
323        ControlMeaning::Escape(policy) => {
324            // `.contains(&policy)` borrows the identity for the lookup.
325            if !controls.escaped.contains(&policy) {
326                controls.escaped.push(policy);
327            }
328        }
329    }
330}
331
332/// What:
333///  Whether a policy is skipped for this invocation.
334/// Why:
335///   The engine asks by typed identity instead of scanning the list itself.
336///
337/// In TS you'd write (pseudocode):
338/// ```ts
339/// const escaped = controls.escaped.includes(policy);
340/// ```
341pub fn is_escaped(controls: &Controls, policy: PolicyId) -> bool {
342    return controls.escaped.contains(&policy);
343}
344
345/// What:
346///  Copy the arguments without the controls written before the subcommand,
347///  adding
348///       their effect to `controls`.
349///  `&[OsString]` borrows the argument list;
350///  `Vec<OsString>`
351///       is the owned result.
352/// Why:
353///   Git 2.56.0's global-option reader stops at the first option it does not know.
354///       When that option is exactly a control,
355///  it sits where Git would have read an
356///       option,
357///  so it is removed by its position and the reader is asked again.
358///  A value
359///       of `-C`,
360///  `-c` or another value-taking option is skipped by that reader and is
361///       therefore never removed,
362///  even when it spells a control.
363/// Gotcha:
364///  Each round removes one token,
365///  so the loop runs at most once per argument.
366///
367/// In TS you'd write (pseudocode):
368/// ```ts
369/// function stripGlobalControls(args: string[], controls: Controls): string[];
370/// ```
371pub fn strip_global_controls(arguments: &[OsString], controls: &mut Controls) -> Vec<OsString> {
372    // `.to_vec()` copies the borrowed list into an owned one that later rounds replace.
373    let mut current: Vec<OsString> = arguments.to_vec();
374    // `0..arguments.len()` counts the rounds; `_round` is unused because only the count matters.
375    for _round in 0..arguments.len() {
376        // `.as_slice()` lends the owned list as a borrowed view.
377        let layout: GlobalLayout = global_layout(current.as_slice());
378        if layout.outcome != GlobalOutcome::InvalidOption {
379            break;
380        }
381        // `.as_encoded_bytes()` lends the raw bytes of the unknown option.
382        let unknown: &[u8] = current[layout.prefix_len].as_encoded_bytes();
383        // `let Some(x) = ... else { ... };` unwraps the meaning or leaves the loop.
384        let Some(meaning) = control_meaning(unknown) else {
385            break;
386        };
387        record_control(controls, meaning);
388        // `&[layout.prefix_len]` is a one-item borrowed list of positions to drop.
389        current = without_tokens(current.as_slice(), 0, &[layout.prefix_len]);
390    }
391    return current;
392}
393
394/// Spelling,
395///  meaning and global-prefix controls stay out of the release executable.
396#[cfg(test)]
397#[path = "wrapper_controls_tests.rs"]
398mod tests;