Skip to main content

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;