Skip to main content

git_policy_cli/
index_writer_commands.rs

1//! What:
2//!  Whether a resolved forwarded command writes the real index.
3//! Why:
4//!  Index writers take the landing lock so they never interleave with a landing commit.
5//!      The set follows `SPEC.md` "Index-writer coordination":
6//!  `add`,
7//!  `rm`,
8//!  `mv`,
9//!      `restore --staged`,
10//!  `reset` except `--soft`,
11//!  `stash`,
12//!  `checkout`,
13//!  `switch`,
14//!  `merge`,
15//!      `rebase`,
16//!  `cherry-pick`,
17//!  `revert`,
18//!  `apply --cached` and `apply --index`,
19//!  `update-index`,
20//!      `read-tree`,
21//!  `am`,
22//!  `pull` and `sparse-checkout`.
23//!  Options count only before Git's `--`;
24//!      long options match in full or as the abbreviations Git accepts
25//!      (`src/index-lock/index-writer-commands.ts`).
26//!
27//! In TS you'd write (pseudocode):
28//! ```ts
29//! isIndexWriter(await resolveForwardedCommand({ args, gitPath }))
30//! ```
31
32/// The resolved command.
33use super::forwarded_command::ResolvedCommand;
34
35/// Commands that write the index in every form.
36pub const ALWAYS_WRITING_COMMANDS: [&[u8]; 15] = [
37    b"add",
38    b"am",
39    b"checkout",
40    b"cherry-pick",
41    b"merge",
42    b"mv",
43    b"pull",
44    b"read-tree",
45    b"rebase",
46    b"revert",
47    b"rm",
48    b"sparse-checkout",
49    b"stash",
50    b"switch",
51    b"update-index",
52];
53
54/// What:
55///  A long option with the shortest abbreviation Git accepts for it in its command.
56/// Why:
57///   Git's `parse_options` accepts unambiguous prefixes.
58///
59/// In TS you'd write (pseudocode):
60/// ```ts
61/// type LongOption = { name: string; minimumLength: number };
62/// ```
63#[derive(Clone, Copy, Debug)]
64pub struct LongOption {
65    /// Full spelling.
66    pub name: &'static [u8],
67    /// Shortest accepted prefix length,
68    ///  `--` included.
69    pub minimum_length: usize,
70}
71
72/// `git restore --staged`;
73///  `--s` is ambiguous with `--source`.
74pub const RESTORE_STAGED: LongOption = LongOption { name: b"--staged", minimum_length: 4 };
75/// `git reset --soft`;
76///  `--s` is accepted.
77pub const RESET_SOFT: LongOption = LongOption { name: b"--soft", minimum_length: 3 };
78/// `git apply --cached`;
79///  `--c` is ambiguous with `--check`.
80pub const APPLY_CACHED: LongOption = LongOption { name: b"--cached", minimum_length: 4 };
81/// `git apply --index`;
82///  `--in` is ambiguous with `--intent-to-add` and `--inaccurate-eof`.
83pub const APPLY_INDEX: LongOption = LongOption { name: b"--index", minimum_length: 5 };
84
85/// What:
86///  The option tokens after the subcommand and before `--`.
87/// Why:
88///   `optionTokens`:
89///  a token is an option when it starts with `-` and is not `-` alone.
90///
91/// In TS you'd write (pseudocode):
92/// ```ts
93/// function optionTokens(command: ResolvedGitCommand): string[];
94/// ```
95pub fn option_tokens(command: &ResolvedCommand) -> Vec<&[u8]> {
96    // `mut` allows collecting tokens up to the separator.
97    let mut options: Vec<&[u8]> = Vec::new();
98    for token in command.region() {
99        let bytes: &[u8] = token.as_encoded_bytes();
100        if bytes == b"--" {
101            break;
102        }
103        if bytes.starts_with(b"-") && bytes != b"-" {
104            options.push(bytes);
105        }
106    }
107    return options;
108}
109
110/// What:
111///  Whether a token spells a long option or an accepted abbreviation of it.
112/// Why:
113///   `matchesLong`:
114///  an attached `=value` is ignored.
115///
116/// In TS you'd write (pseudocode):
117/// ```ts
118/// function matchesLong({ token, option }): boolean;
119/// ```
120pub fn matches_long(token: &[u8], option: LongOption) -> bool {
121    let name: &[u8] = match token.iter().position(is_equals) {
122        Some(end) => &token[..end],
123        None => token,
124    };
125    return name.len() >= option.minimum_length && option.name.starts_with(name);
126}
127
128/// What:
129///  Whether a byte is `=`.
130/// Why:
131///   A named predicate keeps the search free of closures.
132///
133/// In TS you'd write (pseudocode):
134/// ```ts
135/// (byte: number) => byte === 0x3d
136/// ```
137fn is_equals(byte: &u8) -> bool {
138    return *byte == b'=';
139}
140
141/// What:
142///  Whether a short-option cluster sets `letter` before a value-taking letter consumes the
143///       rest.
144/// Why:
145///   `clusterHas`:
146///  `-WS` sets `--staged`,
147///  `-sS` does not (`-s` takes the rest as a value).
148///
149/// In TS you'd write (pseudocode):
150/// ```ts
151/// function clusterHas({ token, letter, valueLetters }): boolean;
152/// ```
153pub fn cluster_has(token: &[u8], letter: u8, value_letters: &[u8]) -> bool {
154    if !token.starts_with(b"-") || token.starts_with(b"--") {
155        return false;
156    }
157    for byte in &token[1..] {
158        if *byte == letter {
159            return true;
160        }
161        if value_letters.contains(byte) {
162            return false;
163        }
164    }
165    return false;
166}
167
168/// What:
169///  Whether a resolved forwarded command writes the real index.
170/// Why:
171///   `isIndexWriter`.
172///
173/// In TS you'd write (pseudocode):
174/// ```ts
175/// function isIndexWriter(command: ResolvedGitCommand): boolean;
176/// ```
177pub fn is_index_writer(command: &ResolvedCommand) -> bool {
178    if !command.is_builtin() {
179        return false;
180    }
181    let word: &[u8] = command.word();
182    if ALWAYS_WRITING_COMMANDS.contains(&word) {
183        return true;
184    }
185    let options: Vec<&[u8]> = option_tokens(command);
186    if word == b"restore" {
187        for token in &options {
188            if matches_long(token, RESTORE_STAGED) || cluster_has(token, b'S', b"s") {
189                return true;
190            }
191        }
192        return false;
193    }
194    if word == b"reset" {
195        for token in &options {
196            if matches_long(token, RESET_SOFT) {
197                return false;
198            }
199        }
200        return true;
201    }
202    if word == b"apply" {
203        for token in &options {
204            if matches_long(token, APPLY_CACHED) || matches_long(token, APPLY_INDEX) {
205                return true;
206            }
207        }
208    }
209    return false;
210}
211
212/// Classification controls stay out of the release executable.
213#[cfg(test)]
214#[path = "index_writer_commands_tests.rs"]
215mod tests;