Skip to main content

git_policy_cli/
config_loading.rs

1//! What:
2//!  Policy-configuration loading decisions before expensive policy initialization.
3//! Why:
4//!  Known inspection forms keep the fast path;
5//!  mutations,
6//!  aliases and ambiguous options require configuration.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! // Classify native argv without executing or weakening any configured policy.
11//! ```
12
13/// Import the exact current-Git global boundary and native arguments.
14use super::global_arguments::{GlobalOutcome, global_layout};
15use std::ffi::OsString;
16
17/// Whether this invocation needs the shipped policy configuration.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum ConfigLoading {
20    /// Known inspection/query/native-option-error form does not initialize the policy engines.
21    Skip,
22    /// A mutation or unresolved command shape requires configuration.
23    Required,
24}
25
26/// Commands whose existing wrapper contract does not load repository policy configuration.
27const INSPECTION_COMMANDS: &[&[u8]] = &[
28    b"annotate",
29    b"blame",
30    b"cat-file",
31    b"count-objects",
32    b"describe",
33    b"diff",
34    b"diff-files",
35    b"diff-index",
36    b"diff-tree",
37    b"for-each-ref",
38    b"grep",
39    b"help",
40    b"log",
41    b"ls-files",
42    b"ls-remote",
43    b"ls-tree",
44    b"merge-base",
45    b"name-rev",
46    b"rev-list",
47    b"rev-parse",
48    b"shortlog",
49    b"show",
50    b"show-branch",
51    b"show-ref",
52    b"status",
53    b"version",
54    b"whatchanged",
55];
56
57/// Mutation-bearing long forms from the selected native branch/tag implementations.
58fn mutating_long(name: &[u8], branch: bool) -> bool {
59    if branch {
60        return [
61            b"--copy".as_slice(),
62            b"--delete",
63            b"--delete-merged",
64            b"--edit-description",
65            b"--force",
66            b"--move",
67            b"--set-upstream",
68            b"--set-upstream-to",
69            b"--unset-upstream",
70            b"--create-reflog",
71        ]
72        .contains(&name);
73    }
74    return [
75        b"--annotate".as_slice(),
76        b"--delete",
77        b"--edit",
78        b"--force",
79        b"--sign",
80        b"--local-user",
81        b"--message",
82        b"--file",
83        b"--trailer",
84        b"--create-reflog",
85    ]
86    .contains(&name);
87}
88
89/// Classify branch/tag forms conservatively while distinguishing inline-optional and separated values.
90fn mixed_command(arguments: &[OsString], branch: bool) -> ConfigLoading {
91    let mut listing: bool = false;
92    let mut positional: bool = false;
93    let mut after_separator: bool = false;
94    // True while the token being visited is the separated value of the filter before it.
95    let mut is_value: bool = false;
96    // Visiting each argument once with its position cannot loop forever, unlike a hand-stepped index.
97    for (index, argument) in arguments.iter().enumerate() {
98        if is_value {
99            is_value = false;
100            continue;
101        }
102        let token: &[u8] = argument.as_encoded_bytes();
103        if after_separator {
104            positional = true;
105            continue;
106        }
107        if token == b"--" {
108            after_separator = true;
109            continue;
110        }
111        if token.starts_with(b"--") {
112            // Only split the option name; attached values remain opaque native bytes.
113            let assignment: Option<usize> = token.iter().position(is_equals);
114            let name: &[u8] = if let Some(at) = assignment {
115                &token[..at]
116            } else {
117                token
118            };
119            if mutating_long(name, branch) {
120                return ConfigLoading::Required;
121            }
122            if name == b"--list"
123                || (branch && name == b"--show-current")
124                || (!branch && name == b"--verify")
125            {
126                listing = true;
127                continue;
128            }
129            // These native optional arguments consume only an attached '=value', never the next positional.
130            if [b"--color".as_slice(), b"--column", b"--abbrev"].contains(&name) {
131                continue;
132            }
133            if [
134                b"--contains".as_slice(),
135                b"--no-contains",
136                b"--with",
137                b"--without",
138                b"--merged",
139                b"--no-merged",
140                b"--points-at",
141                b"--format",
142                b"--sort",
143            ]
144            .contains(&name)
145            {
146                if assignment.is_none() {
147                    if index + 1 == arguments.len() {
148                        // Last-argument defaults are inspection filters; missing required format/sort is left to Git.
149                        return if name == b"--format" || name == b"--sort" {
150                            ConfigLoading::Required
151                        } else {
152                            ConfigLoading::Skip
153                        };
154                    }
155                    is_value = true;
156                }
157                if ![b"--format".as_slice(), b"--sort"].contains(&name) {
158                    listing = true;
159                }
160                continue;
161            }
162            if [
163                b"--verbose".as_slice(),
164                b"--quiet",
165                b"--remotes",
166                b"--all",
167                b"--ignore-case",
168                b"--omit-empty",
169                b"--no-color",
170                b"--no-column",
171            ]
172            .contains(&name)
173            {
174                continue;
175            }
176            // Unknown and abbreviated forms can be valid native mutations; do not guess them into the fast path.
177            return ConfigLoading::Required;
178        }
179        if token.starts_with(b"-") && token.len() > 1 {
180            let mut numeric_lines: bool = false;
181            for letter in &token[1..] {
182                if numeric_lines {
183                    if letter.is_ascii_digit() {
184                        continue;
185                    }
186                    return ConfigLoading::Required;
187                }
188                let mutations: &[u8] = if branch { b"cCdDfmMut" } else { b"adefsumF" };
189                if mutations.contains(letter) {
190                    return ConfigLoading::Required;
191                }
192                if *letter == b'l' || (!branch && *letter == b'v') {
193                    listing = true;
194                } else if !branch && *letter == b'n' {
195                    listing = true;
196                    numeric_lines = true;
197                } else if (branch && b"vqrai".contains(letter)) || (!branch && *letter == b'i') {
198                    continue;
199                } else {
200                    return ConfigLoading::Required;
201                }
202            }
203            continue;
204        }
205        positional = true;
206    }
207    if !positional || listing {
208        return ConfigLoading::Skip;
209    }
210    return ConfigLoading::Required;
211}
212
213/// Named ASCII predicate avoids capturing or reinterpreting native option values.
214fn is_equals(byte: &u8) -> bool {
215    return *byte == b'=';
216}
217
218/// Decide configuration ownership without changing argv,
219///  resolving aliases,
220///  or reading executable configuration.
221pub fn classify_config_loading(arguments: &[OsString]) -> ConfigLoading {
222    let layout = global_layout(arguments);
223    if layout.outcome == GlobalOutcome::Query
224        || layout.outcome == GlobalOutcome::InvalidOption
225        || layout.outcome == GlobalOutcome::MissingValue
226    {
227        return ConfigLoading::Skip;
228    }
229    if layout.outcome == GlobalOutcome::NoCommand {
230        return ConfigLoading::Required;
231    }
232    let command: &[u8] = arguments[layout.prefix_len].as_encoded_bytes();
233    if INSPECTION_COMMANDS.contains(&command) {
234        return ConfigLoading::Skip;
235    }
236    if command == b"branch" || command == b"tag" {
237        return mixed_command(&arguments[layout.prefix_len + 1..], command == b"branch");
238    }
239    return ConfigLoading::Required;
240}
241
242/// Native-value and mixed-command controls remain outside release builds.
243#[cfg(test)]
244#[path = "config_loading_tests.rs"]
245mod tests;