git_policy_cli/commit/
commit_args.rs1const PATHSPEC_SEPARATOR: &str = "--";
25use crate::transaction_journal::TransactionMode;
27
28const REDIRECTING_VALUE_OPTIONS: [&str; 3] = ["-C", "--git-dir", "--work-tree"];
30
31const VALUE_TAKING_GLOBAL_OPTIONS: [&str; 7] = [
33 "-c",
34 "-C",
35 "--git-dir",
36 "--work-tree",
37 "--namespace",
38 "--super-prefix",
39 "--attr-source",
40];
41
42const COMMIT_VALUE_OPTIONS: [&str; 20] = [
44 "-m",
45 "-F",
46 "-C",
47 "-c",
48 "-t",
49 "-U",
50 "--message",
51 "--file",
52 "--reuse-message",
53 "--reedit-message",
54 "--squash",
55 "--fixup",
56 "--author",
57 "--date",
58 "--cleanup",
59 "--trailer",
60 "--template",
61 "--unified",
62 "--inter-hunk-context",
63 "--pathspec-from-file",
64];
65
66const SHORT_VALUE_OPTIONS: [&str; 6] = ["-m", "-F", "-C", "-c", "-t", "-U"];
68
69const OPTIONAL_VALUE_LETTERS: [char; 2] = ['S', 'u'];
71
72fn find_value_option_index(letters: &[char]) -> Option<usize> {
82 for (index, letter) in letters.iter().enumerate().skip(1) {
83 let option: String = format!("-{letter}");
84 if SHORT_VALUE_OPTIONS.contains(&option.as_str()) {
85 return Some(index);
86 }
87 }
88 return None;
89}
90
91fn normalise_inline_short(token: &str) -> Vec<String> {
101 if !token.starts_with('-') || token.starts_with("--") || token.len() <= 2 {
102 return vec![String::from(token)];
103 }
104 let letters: Vec<char> = token.chars().collect();
105 let Some(value_index) = find_value_option_index(letters.as_slice()) else {
106 return vec![String::from(token)];
107 };
108 let mut tokens: Vec<String> = letters[1..value_index]
109 .iter()
110 .map(|letter| format!("-{letter}"))
111 .collect();
112 tokens.push(format!("-{}", letters[value_index]));
113 let inline: String = letters[value_index + 1..].iter().collect();
114 if !inline.is_empty() {
115 tokens.push(inline);
116 }
117 return tokens;
118}
119
120fn takes_next_token(token: &str) -> bool {
133 if token.starts_with("--") {
134 return COMMIT_VALUE_OPTIONS.contains(&token);
135 }
136 if !token.starts_with('-') || token.len() < 2 {
137 return false;
138 }
139 let letters: Vec<char> = token.chars().skip(1).collect();
140 for (index, letter) in letters.iter().enumerate() {
141 if OPTIONAL_VALUE_LETTERS.contains(letter) {
142 return false;
143 }
144 if SHORT_VALUE_OPTIONS.contains(&format!("-{letter}").as_str()) {
145 return index + 1 == letters.len();
146 }
147 }
148 return false;
149}
150
151pub fn separator_index(region: &[String]) -> Option<usize> {
162 let mut index: usize = 0;
163 while index < region.len() {
164 let token: &str = region[index].as_str();
165 if token == PATHSPEC_SEPARATOR {
166 return Some(index);
167 }
168 if takes_next_token(token) {
169 index += 1;
170 }
171 index += 1;
172 }
173 return None;
174}
175
176pub fn normalise_commit_args(args: &[String]) -> Vec<String> {
186 let separator: Option<usize> = separator_index(args);
187 let (region, tail): (&[String], &[String]) = match separator {
188 Some(index) => (&args[..index], &args[index..]),
189 None => (args, &[]),
190 };
191 let mut normalised: Vec<String> = Vec::new();
192 let mut value_next: bool = false;
193 for token in region {
194 if value_next {
196 normalised.push(token.clone());
197 value_next = false;
198 continue;
199 }
200 normalised.extend(normalise_inline_short(token));
201 value_next = takes_next_token(token);
202 }
203 normalised.extend(tail.iter().cloned());
204 return normalised;
205}
206
207pub fn split_commit_invocation(args: &[String], subcommand_index: usize) -> (Vec<String>, Vec<String>) {
218 let globals: &[String] = &args[..subcommand_index];
219 let mut kept: Vec<String> = Vec::new();
220 let mut index: usize = 0;
221 while index < globals.len() {
222 let token: &str = globals[index].as_str();
223 let name: &str = if token.starts_with("--") && token.contains('=') {
224 &token[..token.find('=').unwrap_or(token.len())]
225 } else {
226 token
227 };
228 let separated: bool = VALUE_TAKING_GLOBAL_OPTIONS.contains(&token);
229 if REDIRECTING_VALUE_OPTIONS.contains(&name) || token == "--bare" {
230 if separated {
231 index += 1;
232 }
233 index += 1;
234 continue;
235 }
236 kept.push(String::from(token));
237 if separated {
238 if let Some(value) = globals.get(index + 1) {
239 kept.push(value.clone());
240 }
241 index += 1;
242 }
243 index += 1;
244 }
245 return (kept, args[subcommand_index + 1..].to_vec());
246}
247
248pub fn without_all_flag(commit_args: &[String]) -> Vec<String> {
258 let normalised: Vec<String> = normalise_commit_args(commit_args);
259 let mut kept: Vec<String> = Vec::new();
260 let mut value_next: bool = false;
261 let mut after_separator: bool = false;
262 for token in normalised {
263 if after_separator || value_next {
264 kept.push(token);
265 value_next = false;
266 continue;
267 }
268 if token == PATHSPEC_SEPARATOR {
269 after_separator = true;
270 kept.push(token);
271 continue;
272 }
273 if token == "--all" {
274 continue;
275 }
276 if token.starts_with('-') && !token.starts_with("--") && token.len() > 1 {
277 let letters: Vec<char> = token[1..].chars().collect();
278 let value_start: Option<usize> = letters
279 .iter()
280 .position(|letter| { return OPTIONAL_VALUE_LETTERS.contains(letter); });
281 let flags: &[char] = match value_start {
282 Some(index) => &letters[..index],
283 None => &letters[..],
284 };
285 let mut cluster: String = String::from("-");
286 for letter in flags.iter().filter(|letter| { return **letter != 'a'; }) {
287 cluster.push(*letter);
288 }
289 if let Some(index) = value_start {
290 for letter in &letters[index..] {
291 cluster.push(*letter);
292 }
293 }
294 if cluster == "-" {
295 continue;
296 }
297 value_next = COMMIT_VALUE_OPTIONS.contains(&cluster.as_str());
298 kept.push(cluster);
299 continue;
300 }
301 value_next = COMMIT_VALUE_OPTIONS.contains(&token.as_str());
302 kept.push(token);
303 }
304 return kept;
305}
306
307pub fn private_explicit_commit_args(args: &[String], pathspecs: &[String]) -> Vec<String> {
319 let mut without_file: Vec<String> = Vec::new();
320 let mut index: usize = 0;
321 while index < args.len() {
322 let token: &str = args[index].as_str();
323 if token == "--pathspec-from-file" {
324 index += 2;
325 continue;
326 }
327 if token.starts_with("--pathspec-from-file=") || token == "--pathspec-file-nul" {
328 index += 1;
329 continue;
330 }
331 without_file.push(String::from(token));
332 index += 1;
333 }
334 for pathspec in pathspecs.iter().rev() {
335 if let Some(position) = without_file.iter().rposition(|token| { return token == pathspec; }) {
336 without_file.remove(position);
337 }
338 }
339 let separator: Option<usize> = separator_index(without_file.as_slice());
342 let mut kept: Vec<String> = Vec::new();
343 let mut value_next: bool = false;
344 for (position, token) in without_file.into_iter().enumerate() {
345 if separator.is_some_and(|at| { return position > at; }) {
346 kept.push(token);
347 continue;
348 }
349 if separator == Some(position) {
350 continue;
351 }
352 if value_next {
353 kept.push(token);
354 value_next = false;
355 continue;
356 }
357 value_next = takes_next_token(token.as_str());
358 if token == "-o" || token == "--only" {
359 continue;
360 }
361 kept.push(token);
362 }
363 return kept;
364}
365
366pub fn private_selection_commit_args(args: &[String], pathspecs: &[String]) -> Vec<String> {
376 return private_explicit_commit_args(args, pathspecs)
377 .into_iter()
378 .filter(|token| { return !["-i", "-p", "--include", "--interactive", "--patch"].contains(&token.as_str()); })
379 .collect();
380}
381
382pub fn resolve_private_commit_args(
393 args: &[String],
394 pathspecs: &[String],
395 mode: TransactionMode,
396 selected_privately: bool,
397) -> Vec<String> {
398 if mode == TransactionMode::ExplicitPath {
399 return private_explicit_commit_args(args, pathspecs);
400 }
401 if selected_privately {
402 return private_selection_commit_args(args, pathspecs);
403 }
404 return args.to_vec();
405}
406
407#[cfg(test)]
409#[path = "commit_args_tests.rs"]
410mod tests;