git_policy_cli/rule_add_explicit.rs
1//! What:
2//! Pure decision of the add-explicit policy:
3//! `git add` names the paths it stages.
4//! Why:
5//! `git add .`,
6//! `git add -A` and `git add -u` sweep up every changed path,
7//! including
8//! ones the caller never looked at,
9//! so one commit stops describing one change.
10//! The
11//! policy rejects those forms.
12//! This file decides from the arguments alone;
13//! it reads
14//! no file and starts no process.
15//!
16//! In TS you'd write (pseudocode):
17//! ```ts
18//! // decideAddExplicit(['add', '.']) => 'cli-git: git add rejects bulk-staging patterns (.) ...'
19//! ```
20
21/// What:
22/// `use` brings names from sibling files into this file;
23/// `super::` means "the parent
24/// module",
25/// where every sibling file of this crate is declared.
26/// Why:
27/// Which tokens stage in bulk comes from Git's own option table of `git add`.
28///
29/// In TS you'd write (pseudocode):
30/// ```ts
31/// import { ADD_ESCAPE_HATCH, parseAddRegion } from './command_add.ts';
32/// ```
33use super::command_add::{ADD_ESCAPE_HATCH, AddRegion, parse_add_region};
34use super::command_options::OptionError;
35use super::global_arguments::command_tokens;
36/// What:
37/// `OsString` is owned operating-system text of raw bytes.
38/// Sibling the reader might
39/// expect:
40/// `String`,
41/// which must be valid UTF-8.
42/// Why:
43/// Arguments are compared as bytes;
44/// only the rejection text renders them.
45///
46/// In TS you'd write (pseudocode):
47/// ```ts
48/// // string, but byte-preserving.
49/// ```
50use std::ffi::OsString;
51
52/// What:
53/// The finding code of an add-explicit rejection.
54/// `&str` is borrowed text baked into
55/// the program.
56/// Why:
57/// Callers identify the finding by this stable code.
58///
59/// In TS you'd write (pseudocode):
60/// ```ts
61/// const BULK_ADD_CODE = 'bulk-add-rejected';
62/// ```
63pub const BULK_ADD_CODE: &str = "bulk-add-rejected";
64
65/// What:
66/// Decide from the argument list:
67/// the rejection text,
68/// or nothing to let the command
69/// through.
70/// `&[OsString]` borrows the arguments,
71/// already free of wrapper controls;
72/// `Option<String>` is "owned text or nothing".
73/// Why:
74/// Only `git add` is judged,
75/// and only when Git accepts its options.
76/// The text quotes
77/// the caller's own tokens,
78/// so `-vA` is named as written.
79///
80/// In TS you'd write (pseudocode):
81/// ```ts
82/// function decideAddExplicit(args: string[]): string | undefined;
83/// ```
84pub fn decide_add_explicit(arguments: &[OsString]) -> Option<String> {
85 // `let Some((a, b)) = ... else { ... };` unpacks the command word and the tokens after
86 // it, or returns when Git runs no subcommand.
87 let Some((word, region)) = command_tokens(arguments) else {
88 // `None` is the "absent" case of `Option`.
89 return None;
90 };
91 // `.as_encoded_bytes()` lends the raw bytes of the subcommand word.
92 if word.as_encoded_bytes() != b"add" {
93 return None;
94 }
95 // `Result<A, B>` is "either success `A` or failure `B`"; `&[]` is an empty flag list.
96 let parsed: Result<AddRegion, OptionError> = parse_add_region(region, &[]);
97 // `let Ok(x) = ... else { ... };` unwraps the facts or returns for a refused region.
98 let Ok(facts) = parsed else {
99 return None;
100 };
101 if facts.bulk_matches.is_empty() {
102 return None;
103 }
104 // `Vec<String>` is an owned list of owned text; `mut` allows pushing.
105 let mut tokens: Vec<String> = Vec::<String>::new();
106 // `Option<usize>` is "the index of the token named last, or nothing".
107 let mut named_last: Option<usize> = None;
108 // `for found in &facts.bulk_matches` borrows each match in argument order.
109 for found in &facts.bulk_matches {
110 // One cluster such as `-Au` matches twice; it is named once.
111 if named_last != Some(found.token) {
112 // `.to_string_lossy()` renders a token as text; `.into_owned()` makes it owned.
113 tokens.push(region[found.token].to_string_lossy().into_owned());
114 }
115 named_last = Some(found.token);
116 }
117 // `Some(x)` is the "present" case; `.join(", ")` builds one text with separators.
118 return Some(format!(
119 "cli-git: git add rejects bulk-staging patterns ({}) because they sweep up paths the \
120 caller did not intend to stage, leaving the index in a state that does not match a \
121 single logical change. Name the paths explicitly, or pass {ADD_ESCAPE_HATCH} to bypass \
122 for this invocation.",
123 tokens.join(", ")
124 ));
125}
126
127/// Bulk forms,
128/// explicit forms and the rejection text.
129#[cfg(test)]
130#[path = "rule_add_explicit_tests.rs"]
131mod tests;