git_policy_cli/command_add.rs
1//! What:
2//! The complete `git add` option table of Git 2.56.0 and the bulk-staging facts read
3//! from it.
4//! Why:
5//! The add-explicit policy rejects staging that names no explicit path.
6//! Git accepts
7//! `-A` inside any cluster (`-vA`),
8//! by abbreviation (`--al`) and through
9//! `--no-ignore-removal`;
10//! a list of exact tokens misses those.
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! // parseAddRegion(postSubcommandArgs): AddRegion (throws OptionError)
15//! ```
16
17/// What:
18/// Bring the tokenizer,
19/// its table builder and its questions into this file.
20/// Why:
21/// This module only declares Git's table and interprets what the tokenizer found.
22///
23/// In TS you'd write (pseudocode):
24/// ```ts
25/// import { parseOptions, row } from './command_options.ts';
26/// ```
27use super::command_options::{
28 Arity, DEFAULT_MODE, Occurrence, OptionError, OptionSpec, ParsedOptions, UNREAD, parse_options,
29 row,
30};
31use super::command_options_query::{
32 WrapperFlags, is_enabled, last_occurrence, positional_tokens, split_wrapper_flags,
33};
34/// `OsString` is owned operating-system text of raw bytes (sibling `String` must be UTF-8).
35use std::ffi::OsString;
36
37/// Wrapper-only flag that suppresses bulk-add enforcement for one invocation.
38pub const ADD_ESCAPE_HATCH: &str = "--no-enforce-bulk-add";
39
40/// `-A`,
41/// `--all`.
42pub const ALL: u16 = 1;
43/// `--ignore-removal`,
44/// the same variable as `--no-all` (builtin/add.c:252-259).
45pub const IGNORE_REMOVAL: u16 = 2;
46/// `-u`,
47/// `--update`.
48pub const UPDATE: u16 = 3;
49/// `--resolved`:
50/// add every conflict-resolved tracked file.
51pub const RESOLVED: u16 = 4;
52
53/// What:
54/// Rows of `builtin_add_options` (builtin/add.c:261-292),
55/// in source order.
56/// `&[OptionSpec]` is a borrowed table baked into the program.
57/// Why:
58/// Only `--chmod`,
59/// `--pathspec-from-file`,
60/// `-U` and `--inter-hunk-context` take a
61/// value;
62/// every other token after an option is a pathspec.
63///
64/// In TS you'd write (pseudocode):
65/// ```ts
66/// export const ADD_TABLE = [row(UNREAD, 'n', 'dry-run', 'none', true), /* ... */] as const;
67/// ```
68pub const ADD_TABLE: &[OptionSpec] = &[
69 row(UNREAD, Some(b'n'), Some("dry-run"), Arity::None, true),
70 row(UNREAD, Some(b'v'), Some("verbose"), Arity::None, true),
71 row(UNREAD, Some(b'i'), Some("interactive"), Arity::None, true),
72 row(UNREAD, Some(b'p'), Some("patch"), Arity::None, true),
73 row(UNREAD, None, Some("auto-advance"), Arity::None, true),
74 row(UNREAD, Some(b'U'), Some("unified"), Arity::Required, false),
75 row(
76 UNREAD,
77 None,
78 Some("inter-hunk-context"),
79 Arity::Required,
80 false,
81 ),
82 row(UNREAD, Some(b'e'), Some("edit"), Arity::None, true),
83 row(UNREAD, Some(b'f'), Some("force"), Arity::None, true),
84 row(UPDATE, Some(b'u'), Some("update"), Arity::None, true),
85 row(UNREAD, None, Some("renormalize"), Arity::None, true),
86 row(RESOLVED, None, Some("resolved"), Arity::None, true),
87 row(UNREAD, Some(b'N'), Some("intent-to-add"), Arity::None, true),
88 row(ALL, Some(b'A'), Some("all"), Arity::None, true),
89 row(
90 IGNORE_REMOVAL,
91 None,
92 Some("ignore-removal"),
93 Arity::None,
94 true,
95 ),
96 row(UNREAD, None, Some("refresh"), Arity::None, true),
97 row(UNREAD, None, Some("ignore-errors"), Arity::None, true),
98 row(UNREAD, None, Some("ignore-missing"), Arity::None, true),
99 row(UNREAD, None, Some("sparse"), Arity::None, true),
100 row(UNREAD, None, Some("chmod"), Arity::Required, true),
101 row(UNREAD, None, Some("warn-embedded-repo"), Arity::None, true),
102 row(
103 UNREAD,
104 None,
105 Some("pathspec-from-file"),
106 Arity::Required,
107 true,
108 ),
109 row(UNREAD, None, Some("pathspec-file-nul"), Arity::None, true),
110];
111
112/// Pathspecs that match every changed path under the working directory or repository.
113const BULK_PATHSPECS: &[&[u8]] = &[b".", b"./", b"*", b":/"];
114
115/// What:
116/// Why a token stages in bulk.
117/// An `enum` is a closed set of named alternatives.
118/// Why:
119/// The policy names the matched tokens;
120/// the kind says which Git meaning matched.
121///
122/// In TS you'd write (pseudocode):
123/// ```ts
124/// type BulkKind = 'all-flag' | 'update-flag' | 'pathspec';
125/// ```
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub enum BulkKind {
128 /// `-A`/`--all` is on after every option was applied.
129 AllFlag,
130 /// `-u`/`--update` is on after every option was applied.
131 UpdateFlag,
132 /// A positional pathspec equal to `.`,
133 /// `./`,
134 /// `*` or `:/`.
135 Pathspec,
136}
137
138/// What:
139/// One bulk-staging match.
140/// `usize` is the list index type.
141/// Why:
142/// The token index lets the diagnostic quote the caller's exact spelling.
143///
144/// In TS you'd write (pseudocode):
145/// ```ts
146/// type BulkMatch = { kind: BulkKind; token: number };
147/// ```
148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
149pub struct BulkMatch {
150 /// Which Git meaning matched.
151 pub kind: BulkKind,
152 /// Region token index of the token that turned the meaning on.
153 pub token: usize,
154}
155
156/// What:
157/// Facts of one `git add` region.
158/// `Vec<BulkMatch>` is an owned list in argument order.
159/// Why:
160/// The add-explicit policy rejects when the list is non-empty and no hatch is given.
161///
162/// In TS you'd write (pseudocode):
163/// ```ts
164/// type AddRegion = { bulkMatches: string[]; hasEscapeHatch: boolean };
165/// ```
166#[derive(Clone, Debug, Eq, PartialEq)]
167pub struct AddRegion {
168 /// Bulk-staging matches in argument order.
169 pub bulk_matches: Vec<BulkMatch>,
170 /// `--resolved` is on.
171 /// Not a bulk match in the shipped policy;
172 /// reported for its owner.
173 pub resolved: bool,
174 /// Wrapper-only flags in option position;
175 /// `escape` is `--no-enforce-bulk-add`.
176 pub wrapper: WrapperFlags,
177}
178
179/// What:
180/// The token that left `-A` on,
181/// or nothing.
182/// `Option<usize>` is "an index or nothing".
183/// Why:
184/// `--all` and `--ignore-removal` write one variable in opposite senses,
185/// so they are
186/// applied together in argument order (`ignore_removal_cb`,
187/// add.c:252-259).
188///
189/// In TS you'd write (pseudocode):
190/// ```ts
191/// let on; for (const o of occurrences) { if (o.id === ALL) on = o.negated ? undefined : o.token; /* ... */ }
192/// ```
193fn all_token(parsed: &ParsedOptions) -> Option<usize> {
194 let mut on: Option<usize> = None;
195 // `for occurrence in &parsed.occurrences` borrows each record in the order Git applied it.
196 for occurrence in &parsed.occurrences {
197 // `--all` turns it on; `--no-all` turns it off.
198 let all_sets: bool = occurrence.id == ALL && !occurrence.negated;
199 // `--no-ignore-removal` turns it on; `--ignore-removal` turns it off.
200 let removal_sets: bool = occurrence.id == IGNORE_REMOVAL && occurrence.negated;
201 if all_sets || removal_sets {
202 // `Some(x)` is the "present" case of `Option`.
203 on = Some(occurrence.token);
204 } else if occurrence.id == ALL || occurrence.id == IGNORE_REMOVAL {
205 // `None` is the "absent" case of `Option`.
206 on = None;
207 }
208 }
209 return on;
210}
211
212/// What:
213/// The token that left `-u` on,
214/// or nothing.
215/// Why:
216/// `--update --no-update` leaves it off,
217/// as Git's `OPT_BOOL` does.
218///
219/// In TS you'd write (pseudocode):
220/// ```ts
221/// const token = last !== undefined && !last.negated ? last.token : undefined;
222/// ```
223fn update_token(parsed: &ParsedOptions) -> Option<usize> {
224 let last: Option<Occurrence> = last_occurrence(parsed, UPDATE);
225 // `if let Some(found) = last` runs only when the option was written.
226 if let Some(found) = last {
227 if found.negated {
228 return None;
229 }
230 return Some(found.token);
231 }
232 return None;
233}
234
235/// What:
236/// The sort key of a match:
237/// its token index.
238/// `&BulkMatch` borrows one match.
239/// Why:
240/// Matches are reported in the order the caller wrote them.
241///
242/// In TS you'd write (pseudocode):
243/// ```ts
244/// matches.sort((a, b) => a.token - b.token);
245/// ```
246fn match_token(found: &BulkMatch) -> usize {
247 return found.token;
248}
249
250/// What:
251/// Parse the region after `add`.
252/// `Result<A, B>` is "either success `A` or failure `B`".
253/// Why:
254/// A region Git itself would refuse yields the refusal,
255/// never a guessed fact set.
256///
257/// In TS you'd write (pseudocode):
258/// ```ts
259/// function parseAddRegion(region: string[], wrapperFlags: string[]): AddRegion; // throws
260/// ```
261pub fn parse_add_region(
262 region: &[OsString],
263 wrapper_flags: &[&[u8]],
264) -> Result<AddRegion, OptionError> {
265 // The command's own escape hatch is flag 0; the caller's flags follow.
266 let mut flags: Vec<&[u8]> = Vec::<&[u8]>::with_capacity(wrapper_flags.len() + 1);
267 flags.push(ADD_ESCAPE_HATCH.as_bytes());
268 for flag in wrapper_flags {
269 // `*flag` reads the `&[u8]` the loop variable points at.
270 flags.push(*flag);
271 }
272 // `?` returns Git's refusal to our caller, or unwraps the tokenized region.
273 let parsed: ParsedOptions = parse_options(region, ADD_TABLE, DEFAULT_MODE, flags.as_slice())?;
274 let mut bulk_matches: Vec<BulkMatch> = Vec::<BulkMatch>::new();
275 // `if let Some(token) = ...` runs only when `-A` is on.
276 if let Some(token) = all_token(&parsed) {
277 bulk_matches.push(BulkMatch {
278 kind: BulkKind::AllFlag,
279 token,
280 });
281 }
282 if let Some(token) = update_token(&parsed) {
283 bulk_matches.push(BulkMatch {
284 kind: BulkKind::UpdateFlag,
285 token,
286 });
287 }
288 for token in positional_tokens(&parsed, region.len()) {
289 // `.as_encoded_bytes()` lends the raw bytes; nothing is decoded or copied.
290 if BULK_PATHSPECS.contains(®ion[token].as_encoded_bytes()) {
291 bulk_matches.push(BulkMatch {
292 kind: BulkKind::Pathspec,
293 token,
294 });
295 }
296 }
297 // `sort_by_key` orders the list by the value `match_token` returns for each item.
298 bulk_matches.sort_by_key(match_token);
299 // `Ok(x)` is the success case of `Result`.
300 return Ok(AddRegion {
301 bulk_matches,
302 resolved: is_enabled(&parsed, RESOLVED),
303 wrapper: split_wrapper_flags(parsed.wrapper.as_slice()),
304 });
305}
306
307/// Bulk matches in every accepted spelling,
308/// and real-Git controls of the table.
309#[cfg(test)]
310#[path = "command_add_tests.rs"]
311mod tests;