Skip to main content

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        // `&parameter[..at]` borrows the bytes before the `=`.
238        return is_advice_key(&parameter[..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;