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
//! What:
//! Pure decision of the add-explicit policy:
//! `git add` names the paths it stages.
//! Why:
//! `git add .`,
//! `git add -A` and `git add -u` sweep up every changed path,
//! including
//! ones the caller never looked at,
//! so one commit stops describing one change.
//! The
//! policy rejects those forms.
//! This file decides from the arguments alone;
//! it reads
//! no file and starts no process.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // decideAddExplicit(['add', '.']) => 'cli-git: git add rejects bulk-staging patterns (.) ...'
//! ```
/// What:
/// `use` brings names from sibling files into this file;
/// `super::` means "the parent
/// module",
/// where every sibling file of this crate is declared.
/// Why:
/// Which tokens stage in bulk comes from Git's own option table of `git add`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// import { ADD_ESCAPE_HATCH, parseAddRegion } from './command_add.ts';
/// ```
use super::command_add::{ADD_ESCAPE_HATCH, AddRegion, parse_add_region};
use super::command_options::OptionError;
use super::global_arguments::command_tokens;
/// What:
/// `OsString` is owned operating-system text of raw bytes.
/// Sibling the reader might
/// expect:
/// `String`,
/// which must be valid UTF-8.
/// Why:
/// Arguments are compared as bytes;
/// only the rejection text renders them.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// // string, but byte-preserving.
/// ```
use std::ffi::OsString;
/// What:
/// The finding code of an add-explicit rejection.
/// `&str` is borrowed text baked into
/// the program.
/// Why:
/// Callers identify the finding by this stable code.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const BULK_ADD_CODE = 'bulk-add-rejected';
/// ```
pub const BULK_ADD_CODE: &str = "bulk-add-rejected";
/// What:
/// Decide from the argument list:
/// the rejection text,
/// or nothing to let the command
/// through.
/// `&[OsString]` borrows the arguments,
/// already free of wrapper controls;
/// `Option<String>` is "owned text or nothing".
/// Why:
/// Only `git add` is judged,
/// and only when Git accepts its options.
/// The text quotes
/// the caller's own tokens,
/// so `-vA` is named as written.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function decideAddExplicit(args: string[]): string | undefined;
/// ```
pub fn decide_add_explicit(arguments: &[OsString]) -> Option<String> {
// `let Some((a, b)) = ... else { ... };` unpacks the command word and the tokens after
// it, or returns when Git runs no subcommand.
let Some((word, region)) = command_tokens(arguments) else {
// `None` is the "absent" case of `Option`.
return None;
};
// `.as_encoded_bytes()` lends the raw bytes of the subcommand word.
if word.as_encoded_bytes() != b"add" {
return None;
}
// `Result<A, B>` is "either success `A` or failure `B`"; `&[]` is an empty flag list.
let parsed: Result<AddRegion, OptionError> = parse_add_region(region, &[]);
// `let Ok(x) = ... else { ... };` unwraps the facts or returns for a refused region.
let Ok(facts) = parsed else {
return None;
};
if facts.bulk_matches.is_empty() {
return None;
}
// `Vec<String>` is an owned list of owned text; `mut` allows pushing.
let mut tokens: Vec<String> = Vec::<String>::new();
// `Option<usize>` is "the index of the token named last, or nothing".
let mut named_last: Option<usize> = None;
// `for found in &facts.bulk_matches` borrows each match in argument order.
for found in &facts.bulk_matches {
// One cluster such as `-Au` matches twice; it is named once.
if named_last != Some(found.token) {
// `.to_string_lossy()` renders a token as text; `.into_owned()` makes it owned.
tokens.push(region[found.token].to_string_lossy().into_owned());
}
named_last = Some(found.token);
}
// `Some(x)` is the "present" case; `.join(", ")` builds one text with separators.
return Some(format!(
"cli-git: git add rejects bulk-staging patterns ({}) because they sweep up paths the \
caller did not intend to stage, leaving the index in a state that does not match a \
single logical change. Name the paths explicitly, or pass {ADD_ESCAPE_HATCH} to bypass \
for this invocation.",
tokens.join(", ")
));
}
/// Bulk forms,
/// explicit forms and the rejection text.
#[cfg(test)]
#[path = "rule_add_explicit_tests.rs"]
mod tests;