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;