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