git_policy_cli/invocation/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 crate::command_add::ADD_ESCAPE_HATCH;
37use crate::command_options_query::without_tokens;
38use crate::escape_hatch::{
39 BRANCH_WORKTREE_ESCAPE_HATCH, WORKTREE_COPY_ESCAPE_HATCH, WORKTREE_ENFORCEMENT_ESCAPE_HATCH,
40};
41use crate::global_arguments::{GlobalLayout, GlobalOutcome, global_layout};
42use crate::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-mono/catalog-specifiers",
183 meaning: ControlMeaning::Escape(PolicyId::CatalogSpecifiers),
184 },
185 ControlSpelling {
186 flag: "--no-enforce-security/forbidden-strings",
187 meaning: ControlMeaning::Escape(PolicyId::ForbiddenStrings),
188 },
189 ControlSpelling {
190 flag: WORKTREE_ENFORCEMENT_ESCAPE_HATCH,
191 meaning: ControlMeaning::Escape(PolicyId::LinkedWorktreeOnly),
192 },
193 ControlSpelling {
194 flag: BRANCH_WORKTREE_ESCAPE_HATCH,
195 meaning: ControlMeaning::Escape(PolicyId::BranchWorktreeOnly),
196 },
197 ControlSpelling {
198 flag: ADD_ESCAPE_HATCH,
199 meaning: ControlMeaning::Escape(PolicyId::AddExplicit),
200 },
201 ControlSpelling {
202 flag: WORKTREE_COPY_ESCAPE_HATCH,
203 meaning: ControlMeaning::SkipWorktreeCopy,
204 },
205];
206
207/// What:
208/// Everything the controls of one invocation asked for.
209/// `bool` is true or false;
210/// `Vec<PolicyId>` is an owned,
211/// growable list of policy identities.
212/// Why:
213/// The engine reads the effect of the removed tokens from this record,
214/// because the
215/// tokens themselves are gone from the arguments it sees.
216///
217/// In TS you'd write (pseudocode):
218/// ```ts
219/// type Controls = { keepGoing: boolean; escaped: PolicyId[]; commitOnlyEscaped: boolean; skipWorktreeCopy: boolean };
220/// ```
221#[derive(Clone, Debug, Eq, PartialEq)]
222pub struct Controls {
223 /// Later policies run after an error finding.
224 pub keep_going: bool,
225 /// Policies skipped for this invocation,
226 /// each listed once,
227 /// in first-use order.
228 pub escaped: Vec<PolicyId>,
229 /// `--no-enforce-only` was written in option position of `git commit`.
230 pub commit_only_escaped: bool,
231 /// Ignored-state synchronization is skipped.
232 pub skip_worktree_copy: bool,
233}
234
235/// What:
236/// The record of an invocation that used no control.
237/// `Vec::<PolicyId>::new()` is an
238/// empty owned list.
239/// Why:
240/// Every scan starts from "nothing was asked" and only adds.
241///
242/// In TS you'd write (pseudocode):
243/// ```ts
244/// const noControls = (): Controls => ({ keepGoing: false, escaped: [], commitOnlyEscaped: false, skipWorktreeCopy: false });
245/// ```
246pub fn no_controls() -> Controls {
247 return Controls {
248 keep_going: false,
249 escaped: Vec::<PolicyId>::new(),
250 commit_only_escaped: false,
251 skip_worktree_copy: false,
252 };
253}
254
255/// What:
256/// The meaning of a token that is exactly a control spelling,
257/// or nothing.
258/// `&[u8]`
259/// borrows raw bytes;
260/// `Option<T>` is "a value or nothing".
261/// Why:
262/// Arguments are compared as bytes,
263/// so a non-UTF-8 argument is never decoded.
264///
265/// In TS you'd write (pseudocode):
266/// ```ts
267/// const meaning = CONTROL_SPELLINGS.find(s => s.flag === token)?.meaning;
268/// ```
269pub fn control_meaning(token: &[u8]) -> Option<ControlMeaning> {
270 // `for spelling in CONTROL_SPELLINGS` borrows each row in table order.
271 for spelling in CONTROL_SPELLINGS {
272 // `.as_bytes()` lends the spelling's bytes for the comparison.
273 if spelling.flag.as_bytes() == token {
274 // `Some(x)` is the "present" case of `Option`.
275 return Some(spelling.meaning);
276 }
277 }
278 // `None` is the "absent" case of `Option`.
279 return None;
280}
281
282/// What:
283/// The control spellings as byte strings,
284/// in table order.
285/// `Vec<&'static [u8]>` is an
286/// owned list of borrowed byte strings baked into the program.
287/// Why:
288/// Every Git option-table reader takes this list,
289/// so it reports a control found in
290/// option position instead of refusing it as an unknown option.
291/// The position in this
292/// list is the position in `CONTROL_SPELLINGS`.
293///
294/// In TS you'd write (pseudocode):
295/// ```ts
296/// const flags = CONTROL_SPELLINGS.map(s => s.flag);
297/// ```
298pub fn control_flags() -> Vec<&'static [u8]> {
299 // `with_capacity(n)` reserves room for `n` items; `mut` allows pushing.
300 let mut flags: Vec<&'static [u8]> =
301 Vec::<&'static [u8]>::with_capacity(CONTROL_SPELLINGS.len());
302 for spelling in CONTROL_SPELLINGS {
303 flags.push(spelling.flag.as_bytes());
304 }
305 return flags;
306}
307
308/// What:
309/// Add one control's effect to the record.
310/// `&mut Controls` lends the record for
311/// writing (sibling `&Controls` lends it read-only).
312/// Why:
313/// A repeated escape must not list its policy twice:
314/// the list is compared in tests
315/// and printed in diagnostics.
316///
317/// In TS you'd write (pseudocode):
318/// ```ts
319/// function recordControl(controls: Controls, meaning: ControlMeaning): void;
320/// ```
321pub fn record_control(controls: &mut Controls, meaning: ControlMeaning) {
322 // `match` picks one arm per variant and unpacks the policy an `Escape` carries; the
323 // compiler refuses the code if a variant is forgotten.
324 match meaning {
325 ControlMeaning::KeepGoing => controls.keep_going = true,
326 ControlMeaning::SkipWorktreeCopy => controls.skip_worktree_copy = true,
327 ControlMeaning::Escape(policy) => {
328 // `.contains(&policy)` borrows the identity for the lookup.
329 if !controls.escaped.contains(&policy) {
330 controls.escaped.push(policy);
331 }
332 }
333 }
334}
335
336/// What:
337/// Whether a policy is skipped for this invocation.
338/// Why:
339/// The engine asks by typed identity instead of scanning the list itself.
340///
341/// In TS you'd write (pseudocode):
342/// ```ts
343/// const escaped = controls.escaped.includes(policy);
344/// ```
345pub fn is_escaped(controls: &Controls, policy: PolicyId) -> bool {
346 return controls.escaped.contains(&policy);
347}
348
349/// What:
350/// Copy the arguments without the controls written before the subcommand,
351/// adding
352/// their effect to `controls`.
353/// `&[OsString]` borrows the argument list;
354/// `Vec<OsString>`
355/// is the owned result.
356/// Why:
357/// Git 2.56.0's global-option reader stops at the first option it does not know.
358/// When that option is exactly a control,
359/// it sits where Git would have read an
360/// option,
361/// so it is removed by its position and the reader is asked again.
362/// A value
363/// of `-C`,
364/// `-c` or another value-taking option is skipped by that reader and is
365/// therefore never removed,
366/// even when it spells a control.
367/// Gotcha:
368/// Each round removes one token,
369/// so the loop runs at most once per argument.
370///
371/// In TS you'd write (pseudocode):
372/// ```ts
373/// function stripGlobalControls(args: string[], controls: Controls): string[];
374/// ```
375pub fn strip_global_controls(arguments: &[OsString], controls: &mut Controls) -> Vec<OsString> {
376 // `.to_vec()` copies the borrowed list into an owned one that later rounds replace.
377 let mut current: Vec<OsString> = arguments.to_vec();
378 // `0..arguments.len()` counts the rounds; `_round` is unused because only the count matters.
379 for _round in 0..arguments.len() {
380 // `.as_slice()` lends the owned list as a borrowed view.
381 let layout: GlobalLayout = global_layout(current.as_slice());
382 if layout.outcome != GlobalOutcome::InvalidOption {
383 break;
384 }
385 // `.as_encoded_bytes()` lends the raw bytes of the unknown option.
386 let unknown: &[u8] = current[layout.prefix_len].as_encoded_bytes();
387 // `let Some(x) = ... else { ... };` unwraps the meaning or leaves the loop.
388 let Some(meaning) = control_meaning(unknown) else {
389 break;
390 };
391 record_control(controls, meaning);
392 // `&[layout.prefix_len]` is a one-item borrowed list of positions to drop.
393 current = without_tokens(current.as_slice(), 0, &[layout.prefix_len]);
394 }
395 return current;
396}
397
398/// Spelling,
399/// meaning and global-prefix controls stay out of the release executable.
400#[cfg(test)]
401#[path = "wrapper_controls_tests.rs"]
402mod tests;