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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
//! What:
//! The Git command a forwarded invocation actually runs,
//! after ordinary alias expansion.
//! Why:
//! Git expands an alias only when no built-in has that name,
//! reads it from `alias.<name>`
//! or `alias.<name>.command`,
//! splits it with `split_cmdline` quoting,
//! lets its leading words
//! be global options,
//! and expands again when the result is an alias.
//! Index-writer
//! coordination and the commit rules decide from the expanded command,
//! so
//! `git -c alias.c=commit c` is a commit (`src/forwarded-command.ts`).
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const command = await resolveForwardedCommand({ args, gitPath });
//! ```
/// Debug diagnostics.
use super::diagnostic_log::debug;
/// The built-in command names.
use super::git_builtins::is_git_builtin;
/// The global-option layout.
use super::global_arguments::{GlobalLayout, GlobalOutcome, global_layout};
/// Trimming with the incumbent's `.trim()` semantics.
use super::js_text::trim_javascript;
/// Running real Git.
use super::transaction_git::{GitContext, GitRequest, run_git};
/// `OsString` is owned operating-system text of raw bytes.
use std::ffi::OsString;
/// `Path` is a borrowed filesystem path.
use std::path::Path;
/// Alias expansions followed before giving up,
/// well above any realistic chain.
pub const MAX_ALIAS_EXPANSIONS: usize = 16;
/// What:
/// How the command was reached.
/// Why:
/// A shell alias or an unresolved name is never classified as a built-in.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ForwardedCommandRoute = 'direct' | 'alias' | 'shell-alias' | 'unresolved';
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Route {
/// A built-in named directly.
Direct,
/// A built-in reached through one or more aliases.
Alias,
/// An alias running a shell command (`!...`).
ShellAlias,
/// An external command,
/// an unknown name,
/// an alias loop or a malformed alias value.
Unresolved,
}
/// What:
/// The resolved command:
/// the arguments after expansion,
/// where the subcommand is,
/// its
/// name,
/// and the route.
/// Why:
/// `ResolvedGitCommand`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ResolvedGitCommand = { args: string[]; subcommandIndex: number; subcommand?: string; route: ForwardedCommandRoute };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedCommand {
/// Arguments after expansion,
/// global options included.
pub arguments: Vec<OsString>,
/// Index of the subcommand in `arguments`.
pub subcommand_index: usize,
/// The subcommand,
/// or nothing for a bare `git` or a help,
/// version or error form.
pub subcommand: Option<OsString>,
/// How it was reached.
pub route: Route,
}
/// Methods over a resolved command.
impl ResolvedCommand {
/// What:
/// The subcommand as bytes,
/// empty when there is none.
/// Why:
/// Classification compares names.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// command.subcommand ?? ''
/// ```
pub fn word(&self) -> &[u8] {
match &self.subcommand {
Some(name) => return name.as_encoded_bytes(),
None => return b"",
}
}
/// What:
/// The arguments after the subcommand.
/// Why:
/// Option classification reads them.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// command.args.slice(command.subcommandIndex + 1)
/// ```
pub fn region(&self) -> &[OsString] {
return self.arguments.get(self.subcommand_index + 1..).unwrap_or(&[]);
}
/// What:
/// Whether this is a built-in reached directly or through aliases.
/// Why:
/// Shell aliases and unresolved names are never index writers or commits.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// command.route !== 'shell-alias' && command.route !== 'unresolved' && command.subcommand !== undefined
/// ```
pub fn is_builtin(&self) -> bool {
return matches!(self.route, Route::Direct | Route::Alias) && self.subcommand.is_some();
}
}
/// What:
/// Whether a byte is whitespace as C `isspace` sees it in the C locale.
/// Why:
/// `split_cmdline` separates words on exactly these.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// [' ', '\t', '\n', '\r', '\v', '\f'].includes(character)
/// ```
fn is_space(byte: u8) -> bool {
return matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c);
}
/// What:
/// Split an alias value as Git's `split_cmdline` does:
/// whitespace separates words outside
/// quotes,
/// single and double quotes group,
/// and a backslash escapes the next byte except
/// inside single quotes.
/// `None` is an unclosed quote or a trailing backslash.
/// Why:
/// `splitAliasCommand`;
/// bytes are kept as they are.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function splitAliasCommand(value: string): string[] | typeof ALIAS_SPLIT_FAILED;
/// ```
pub fn split_alias_command(value: &[u8]) -> Option<Vec<Vec<u8>>> {
let mut words: Vec<Vec<u8>> = Vec::new();
let mut current: Vec<u8> = Vec::new();
// Starts true as the incumbent's `splitAliasCommand` does, so an empty value is one empty word.
let mut started: bool = true;
let mut quote: u8 = 0;
let mut escaped: bool = false;
for &byte in value {
if escaped {
current.push(byte);
escaped = false;
} else if quote == 0 && is_space(byte) {
if started {
words.push(std::mem::take(&mut current));
}
current.clear();
started = false;
} else if quote == 0 && (byte == b'\'' || byte == b'"') {
quote = byte;
started = true;
} else if quote != 0 && byte == quote {
quote = 0;
} else if byte == b'\\' && quote != b'\'' {
escaped = true;
started = true;
} else {
current.push(byte);
started = true;
}
}
if quote != 0 || escaped {
return None;
}
if started {
words.push(current);
}
return Some(words);
}
/// What:
/// An alias value with surrounding whitespace removed,
/// as JavaScript's `.trim()` does for
/// text;
/// a value that is not UTF-8 loses only ASCII whitespace.
/// Why:
/// The incumbent trims `git config --get` output,
/// newline included,
/// before splitting.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// value.trim()
/// ```
pub fn trim_alias_value(value: &[u8]) -> Vec<u8> {
if let Ok(text) = std::str::from_utf8(value) {
return trim_javascript(text).as_bytes().to_vec();
}
let start: usize = value.iter().position(is_not_ascii_space).unwrap_or(value.len());
let end: usize = value.iter().rposition(is_not_ascii_space).map_or(start, next_index);
return value[start..end].to_vec();
}
/// What:
/// Whether a byte is not ASCII whitespace.
/// Why:
/// A named predicate keeps the trimming free of closures.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// (byte: number) => !isspace(byte)
/// ```
fn is_not_ascii_space(byte: &u8) -> bool {
return !is_space(*byte);
}
/// What:
/// The index after `index`.
/// Why:
/// A named function keeps `map_or` free of closures.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// (index: number) => index + 1
/// ```
fn next_index(index: usize) -> usize {
return index + 1;
}
/// What:
/// The value of one config key through `git config --get`,
/// or nothing when Git reports
/// none or cannot be asked.
/// Why:
/// `readAliasKey`:
/// every failure of the lookup is "no alias".
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function readAliasKey({ gitPath, globalArgs, key }): Promise<string | typeof ALIAS_UNSET>;
/// ```
fn read_config_value(context: &GitContext, global: &[OsString], key: &OsString) -> Option<Vec<u8>> {
let mut arguments: Vec<OsString> = global.to_vec();
arguments.push(OsString::from("config"));
arguments.push(OsString::from("--get"));
arguments.push(key.clone());
let mut request: GitRequest = GitRequest::new(Path::new("."), arguments.as_slice());
request.without_prefix = true;
match run_git(context, &request) {
Ok(output) if output.succeeded() => return Some(output.stdout),
Ok(_) | Err(_) => {
debug(
"readAliasKey",
format!("no {}", key.to_string_lossy()).as_str(),
);
return None;
}
}
}
/// What:
/// An alias value in either of Git's spellings,
/// `alias.<name>` first.
/// Why:
/// `lookupAlias`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function lookupAlias({ gitPath, globalArgs, name }): Promise<string | typeof ALIAS_UNSET>;
/// ```
pub fn lookup_alias(context: &GitContext, global: &[OsString], name: &OsString) -> Option<Vec<u8>> {
let mut plain: OsString = OsString::from("alias.");
plain.push(name);
if let Some(value) = read_config_value(context, global, &plain) {
return Some(value);
}
let mut spelled: OsString = plain;
spelled.push(".command");
return read_config_value(context, global, &spelled);
}
/// What:
/// The resolved command for the current arguments with a route.
/// Why:
/// Every ending of the expansion builds one.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// ({ args, subcommandIndex, subcommand, route })
/// ```
fn resolved(arguments: &[OsString], layout: &GlobalLayout, route: Route) -> ResolvedCommand {
let subcommand: Option<OsString> = if layout.outcome == GlobalOutcome::Command {
arguments.get(layout.prefix_len).cloned()
} else {
None
};
return ResolvedCommand {
arguments: arguments.to_vec(),
subcommand_index: layout.prefix_len,
subcommand,
route,
};
}
/// What:
/// Words of an alias value as arguments.
/// Why:
/// The expansion replaces the alias name with them.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// words
/// ```
fn os_words(words: Vec<Vec<u8>>) -> Vec<OsString> {
let mut owned: Vec<OsString> = Vec::with_capacity(words.len());
for word in words {
#[cfg(unix)]
{
/// The Unix byte-to-string conversion for command words.
use std::os::unix::ffi::OsStringExt;
owned.push(OsString::from_vec(word));
}
#[cfg(not(unix))]
{
owned.push(OsString::from(String::from_utf8_lossy(word.as_slice()).into_owned()));
}
}
return owned;
}
/// What:
/// Resolve the command a forwarded invocation runs,
/// expanding aliases up to
/// `MAX_ALIAS_EXPANSIONS` times;
/// a built-in name starts no Git process.
/// Why:
/// `resolveForwardedCommand`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function resolveForwardedCommand({ args, gitPath }): Promise<ResolvedGitCommand>;
/// ```
pub fn resolve_forwarded_command(context: &GitContext, arguments: &[OsString]) -> ResolvedCommand {
// `mut` allows replacing the arguments with each expansion.
let mut current: Vec<OsString> = arguments.to_vec();
let mut seen: Vec<OsString> = Vec::new();
for _ in 0..=MAX_ALIAS_EXPANSIONS {
let layout: GlobalLayout = global_layout(current.as_slice());
let route: Route = if seen.is_empty() { Route::Direct } else { Route::Alias };
if layout.outcome != GlobalOutcome::Command {
return resolved(current.as_slice(), &layout, route);
}
let name: OsString = current[layout.prefix_len].clone();
if is_git_builtin(name.as_encoded_bytes()) {
return resolved(current.as_slice(), &layout, route);
}
if seen.contains(&name) {
debug(
"resolveForwardedCommand",
format!("alias loop at {}", name.to_string_lossy()).as_str(),
);
return resolved(current.as_slice(), &layout, Route::Unresolved);
}
seen.push(name.clone());
let global: &[OsString] = ¤t[..layout.prefix_len];
let Some(value) = lookup_alias(context, global, &name) else {
return resolved(current.as_slice(), &layout, Route::Unresolved);
};
let trimmed: Vec<u8> = trim_alias_value(value.as_slice());
if trimmed.first() == Some(&b'!') {
return resolved(current.as_slice(), &layout, Route::ShellAlias);
}
let Some(words) = split_alias_command(trimmed.as_slice()) else {
return resolved(current.as_slice(), &layout, Route::Unresolved);
};
if words.is_empty() {
return resolved(current.as_slice(), &layout, Route::Unresolved);
}
let mut expanded: Vec<OsString> = global.to_vec();
expanded.extend(os_words(words));
expanded.extend(current[layout.prefix_len + 1..].iter().cloned());
debug(
"resolveForwardedCommand",
format!("expanded alias {}", name.to_string_lossy()).as_str(),
);
current = expanded;
}
let layout: GlobalLayout = global_layout(current.as_slice());
return resolved(current.as_slice(), &layout, Route::Unresolved);
}
/// What:
/// Whether a resolved command creates or moves linked worktrees.
/// Why:
/// `createsOrMovesWorktrees`:
/// `git worktree add` and `git worktree move`,
/// decided by the
/// first positional token after `worktree`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function createsOrMovesWorktrees(command: ResolvedGitCommand): boolean;
/// ```
pub fn creates_or_moves_worktrees(command: &ResolvedCommand) -> bool {
if !command.is_builtin() || command.word() != b"worktree" {
return false;
}
for token in command.region() {
let bytes: &[u8] = token.as_encoded_bytes();
if !bytes.starts_with(b"-") {
return bytes == b"add" || bytes == b"move";
}
}
return false;
}
/// Resolution controls stay out of the release executable.
#[cfg(test)]
#[path = "forwarded_command_tests.rs"]
mod tests;