git_policy_cli/command_status.rs
1//! What:
2//! The complete `git status` option table of Git 2.56.0,
3//! the machine-readable fact
4//! read from it,
5//! and the reading of `advice.statusHints` set before the subcommand.
6//! Why:
7//! The status-hints transform must not override an explicit caller choice,
8//! and the
9//! wrapper adds no note to output a program will parse.
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! // parseStatusPreRegion(pre).hasStatusHintsOverride; parseStatusPostRegion(post).isMachineReadable
14//! ```
15
16/// What:
17/// Bring the tokenizer,
18/// its table builder,
19/// its questions and the list of global
20/// options that take a separate value into this file.
21/// Why:
22/// The post-subcommand region uses Git's status table;
23/// the pre-subcommand region is
24/// walked with Git's global option arity.
25///
26/// In TS you'd write (pseudocode):
27/// ```ts
28/// import { parseOptions, row } from './command_options.ts';
29/// ```
30use super::command_options::{
31 Arity, DEFAULT_MODE, OptionError, OptionSpec, ParsedOptions, UNREAD, WrapperOccurrence,
32 parse_options, row,
33};
34use super::command_options_query::is_enabled;
35use super::global_arguments::VALUE_OPTIONS;
36/// `OsString` is owned operating-system text of raw bytes (sibling `String` must be UTF-8).
37use std::ffi::OsString;
38
39/// `-s`,
40/// `--short`.
41pub const SHORT: u16 = 1;
42/// `--porcelain[=<version>]`.
43pub const PORCELAIN: u16 = 2;
44/// `--long`.
45pub const LONG: u16 = 3;
46/// `-z`,
47/// `--null`.
48pub const NULL: u16 = 4;
49
50/// What:
51/// Rows of `builtin_status_options` (builtin/commit.c:1548-1599),
52/// in source order.
53/// `&[OptionSpec]` is a borrowed table baked into the program.
54/// Why:
55/// Four rows take an optional value (`PARSE_OPT_OPTARG`),
56/// so a token after them is a
57/// pathspec,
58/// not their value.
59///
60/// In TS you'd write (pseudocode):
61/// ```ts
62/// export const STATUS_TABLE = [row(UNREAD, 'v', 'verbose', 'none', true), /* ... */] as const;
63/// ```
64pub const STATUS_TABLE: &[OptionSpec] = &[
65 row(UNREAD, Some(b'v'), Some("verbose"), Arity::None, true),
66 row(SHORT, Some(b's'), Some("short"), Arity::None, true),
67 row(UNREAD, Some(b'b'), Some("branch"), Arity::None, true),
68 row(UNREAD, None, Some("show-stash"), Arity::None, true),
69 row(UNREAD, None, Some("ahead-behind"), Arity::None, true),
70 row(PORCELAIN, None, Some("porcelain"), Arity::Optional, true),
71 row(LONG, None, Some("long"), Arity::None, true),
72 row(NULL, Some(b'z'), Some("null"), Arity::None, true),
73 row(
74 UNREAD,
75 Some(b'u'),
76 Some("untracked-files"),
77 Arity::Optional,
78 true,
79 ),
80 row(UNREAD, None, Some("ignored"), Arity::Optional, true),
81 row(
82 UNREAD,
83 None,
84 Some("ignore-submodules"),
85 Arity::Optional,
86 true,
87 ),
88 row(UNREAD, None, Some("column"), Arity::Optional, true),
89 row(UNREAD, None, Some("no-renames"), Arity::None, true),
90 // `PARSE_OPT_OPTARG | PARSE_OPT_NONEG` (commit.c:1596-1598).
91 row(
92 UNREAD,
93 Some(b'M'),
94 Some("find-renames"),
95 Arity::Optional,
96 false,
97 ),
98];
99
100/// Config key,
101/// lower-cased,
102/// whose explicit setting the transform respects.
103const ADVICE_KEY: &[u8] = b"advice.statushints";
104
105/// What:
106/// Facts of the region after `status`.
107/// `bool` is true or false.
108/// Why:
109/// Short,
110/// porcelain and NUL-terminated output is parsed by programs.
111///
112/// In TS you'd write (pseudocode):
113/// ```ts
114/// type StatusPostRegion = { isMachineReadable: boolean };
115/// ```
116#[derive(Clone, Debug, Eq, PartialEq)]
117pub struct StatusRegion {
118 /// The final format is short or porcelain,
119 /// or `-z` is on.
120 pub machine_readable: bool,
121 /// Wrapper-only flags in option position;
122 /// `flag` indexes the caller's list.
123 pub wrapper: Vec<WrapperOccurrence>,
124}
125
126/// What:
127/// Whether the final status format is short or porcelain.
128/// Why:
129/// `--short`,
130/// `--porcelain` and `--long` write one shared variable,
131/// so only the last
132/// counts;
133/// `--long` and every negated form select human-readable output.
134///
135/// In TS you'd write (pseudocode):
136/// ```ts
137/// let machine = false; for (const o of occurrences) if (isFormat(o.id)) machine = o.id !== LONG && !o.negated;
138/// ```
139fn has_machine_format(parsed: &ParsedOptions) -> bool {
140 let mut machine: bool = false;
141 // `for occurrence in &parsed.occurrences` borrows each record in the order Git applied it.
142 for occurrence in &parsed.occurrences {
143 if occurrence.id == SHORT || occurrence.id == PORCELAIN {
144 machine = !occurrence.negated;
145 }
146 if occurrence.id == LONG {
147 machine = false;
148 }
149 }
150 return machine;
151}
152
153/// What:
154/// Parse the region after `status`.
155/// `Result<A, B>` is "either success `A` or failure `B`".
156/// Why:
157/// A region Git itself would refuse yields the refusal,
158/// never a guessed fact.
159///
160/// In TS you'd write (pseudocode):
161/// ```ts
162/// function parseStatusPostRegion(region: string[]): StatusPostRegion; // throws
163/// ```
164pub fn parse_status_region(
165 region: &[OsString],
166 wrapper_flags: &[&[u8]],
167) -> Result<StatusRegion, OptionError> {
168 // `?` returns Git's refusal to our caller, or unwraps the tokenized region.
169 let parsed: ParsedOptions = parse_options(region, STATUS_TABLE, DEFAULT_MODE, wrapper_flags)?;
170 // `Ok(x)` is the success case of `Result`.
171 return Ok(StatusRegion {
172 machine_readable: has_machine_format(&parsed) || is_enabled(&parsed, NULL),
173 wrapper: parsed.wrapper,
174 });
175}
176
177/// What:
178/// Whether `key` names `advice.statusHints`.
179/// `&[u8]` borrows the key bytes.
180/// Why:
181/// Git compares section and variable names without regard to ASCII case,
182/// and the
183/// comparison must not decode bytes that may not be UTF-8.
184///
185/// In TS you'd write (pseudocode):
186/// ```ts
187/// const isAdviceKey = key.toLowerCase() === 'advice.statushints';
188/// ```
189fn is_advice_key(key: &[u8]) -> bool {
190 return key.eq_ignore_ascii_case(ADVICE_KEY);
191}
192
193/// What:
194/// Byte index of the first (`last == false`) or last (`last == true`) `=` in `bytes`.
195/// `Option<usize>` is "an index or nothing".
196/// Why:
197/// `-c name=value` splits at the first `=` (config.c:495);
198/// `--config-env=name=var`
199/// splits at the last (config.c:511).
200///
201/// In TS you'd write (pseudocode):
202/// ```ts
203/// const at = last ? text.lastIndexOf('=') : text.indexOf('=');
204/// ```
205fn equals_index(bytes: &[u8], last: bool) -> Option<usize> {
206 let mut found: Option<usize> = None;
207 let mut index: usize = 0;
208 while index < bytes.len() {
209 if bytes[index] == b'=' {
210 // `Some(x)` is the "present" case of `Option`.
211 found = Some(index);
212 if !last {
213 return found;
214 }
215 }
216 index += 1;
217 }
218 return found;
219}
220
221/// What:
222/// Whether one configuration parameter sets `advice.statusHints`.
223/// `parameter` is the
224/// text after `-c` or after `--config-env`.
225/// Why:
226/// A bare `-c advice.statusHints` is Git's boolean-true spelling and is as explicit
227/// as the valued form;
228/// `--config-env` always needs `=<variable>`.
229///
230/// In TS you'd write (pseudocode):
231/// ```ts
232/// const key = parameter.split('=')[0]; return key.toLowerCase() === 'advice.statushints';
233/// ```
234fn sets_advice_key(parameter: &[u8], from_environment: bool) -> bool {
235 // `if let Some(at) = ...` runs only when the parameter contains `=`.
236 if let Some(at) = equals_index(parameter, from_environment) {
237 // `¶meter[..at]` borrows the bytes before the `=`.
238 return is_advice_key(¶meter[..at]);
239 }
240 return !from_environment && is_advice_key(parameter);
241}
242
243/// What:
244/// Whether the caller configured `advice.statusHints` in the global options before
245/// the subcommand.
246/// `global_prefix` is exactly those tokens.
247/// Why:
248/// `git -c advice.statusHints=true status` keeps Git's hints;
249/// the wrapper neither
250/// overrides the choice nor adds its own note.
251/// Gotcha:
252/// Divergence from the incumbent:
253/// `--config-env` sets the same key and counts too.
254///
255/// In TS you'd write (pseudocode):
256/// ```ts
257/// function parseStatusPreRegion(pre: readonly string[]): { hasStatusHintsOverride: boolean };
258/// ```
259pub fn has_status_hints_override(global_prefix: &[OsString]) -> bool {
260 // The value-taking option whose value is the token being visited, when there is one.
261 // `Option<&[u8]>` is "borrowed bytes or nothing"; `mut` allows setting and clearing it.
262 let mut awaiting_value: Option<&[u8]> = None;
263 // `for argument in global_prefix` borrows each token once, in order, so the scan
264 // always ends: an option only marks the next token as its value.
265 for argument in global_prefix {
266 // `.as_encoded_bytes()` lends the raw bytes; nothing is decoded or copied.
267 let token: &[u8] = argument.as_encoded_bytes();
268 // `if let Some(option) = ...` runs only while a value is awaited.
269 if let Some(option) = awaiting_value {
270 // `None` is the "absent" case: this token settles the option.
271 awaiting_value = None;
272 if option == b"-c" && sets_advice_key(token, false) {
273 return true;
274 }
275 if option == b"--config-env" && sets_advice_key(token, true) {
276 return true;
277 }
278 continue;
279 }
280 // `.strip_prefix(...)` returns the remainder when the bytes start with the prefix.
281 if let Some(joined) = token.strip_prefix(b"--config-env=") {
282 if sets_advice_key(joined, true) {
283 return true;
284 }
285 continue;
286 }
287 if VALUE_OPTIONS.contains(&token) {
288 // `Some(x)` is the "present" case: the next token is this option's value.
289 awaiting_value = Some(token);
290 }
291 }
292 return false;
293}
294
295/// Facts in every accepted spelling,
296/// and real-Git controls of the table.
297#[cfg(test)]
298#[path = "command_status_tests.rs"]
299mod tests;