Skip to main content

git_policy_cli/
command_reset.rs

1//! What:
2//!  The complete `git reset` option table of Git 2.56.0 and the mode read from it.
3//! Why:
4//!  The linked-worktree policy guards resets that rewrite worktree files:
5//!  `--hard`,
6//!      `--merge` and `--keep`.
7//!  The five mode options write one variable,
8//!  so only the
9//!      last one counts,
10//!  in any abbreviation Git accepts.
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! // resetChangesWorktree(parseResetRegion(['--har', 'HEAD~1'])) === true
15//! ```
16
17use super::command_options::{
18    Arity, DEFAULT_MODE, OptionError, OptionSpec, ParsedOptions, UNREAD, parse_options, row,
19};
20use super::command_options_query::{WrapperFlags, split_wrapper_flags};
21/// What:
22///  Bring the shared hatch spelling,
23///  the tokenizer,
24///  its table builder and its
25///       questions into this file.
26/// Why:
27///   This module only declares Git's table and interprets what the tokenizer found.
28///
29/// In TS you'd write (pseudocode):
30/// ```ts
31/// import { parseOptions, row } from './command_options.ts';
32/// ```
33use super::escape_hatch::WORKTREE_ENFORCEMENT_ESCAPE_HATCH;
34/// `OsString` is owned operating-system text of raw bytes (sibling `String` must be UTF-8).
35use std::ffi::OsString;
36
37/// `--mixed`.
38pub const MIXED: u16 = 1;
39/// `--soft`.
40pub const SOFT: u16 = 2;
41/// `--hard`.
42pub const HARD: u16 = 3;
43/// `--merge`.
44pub const MERGE: u16 = 4;
45/// `--keep`.
46pub const KEEP: u16 = 5;
47
48/// What:
49///  Rows of `options[]` in `cmd_reset` (builtin/reset.c:350-382),
50///  in source order.
51///       `&[OptionSpec]` is a borrowed table baked into the program.
52/// Why:
53///   The five modes are `PARSE_OPT_NONEG`;
54///  `--recurse-submodules` takes an optional
55///       value,
56///  so a token after it is a revision or path.
57///
58/// In TS you'd write (pseudocode):
59/// ```ts
60/// export const RESET_TABLE = [row(UNREAD, 'q', 'quiet', 'none', true), /* ... */] as const;
61/// ```
62pub const RESET_TABLE: &[OptionSpec] = &[
63    row(UNREAD, Some(b'q'), Some("quiet"), Arity::None, true),
64    row(UNREAD, None, Some("no-refresh"), Arity::None, true),
65    row(MIXED, None, Some("mixed"), Arity::None, false),
66    row(SOFT, None, Some("soft"), Arity::None, false),
67    row(HARD, None, Some("hard"), Arity::None, false),
68    row(MERGE, None, Some("merge"), Arity::None, false),
69    row(KEEP, None, Some("keep"), Arity::None, false),
70    row(
71        UNREAD,
72        None,
73        Some("recurse-submodules"),
74        Arity::Optional,
75        true,
76    ),
77    row(UNREAD, Some(b'p'), Some("patch"), Arity::None, true),
78    row(UNREAD, None, Some("auto-advance"), Arity::None, true),
79    row(UNREAD, Some(b'U'), Some("unified"), Arity::Required, false),
80    row(
81        UNREAD,
82        None,
83        Some("inter-hunk-context"),
84        Arity::Required,
85        false,
86    ),
87    row(UNREAD, Some(b'N'), Some("intent-to-add"), Arity::None, true),
88    row(
89        UNREAD,
90        None,
91        Some("pathspec-from-file"),
92        Arity::Required,
93        true,
94    ),
95    row(UNREAD, None, Some("pathspec-file-nul"), Arity::None, true),
96];
97
98/// What:
99///  The reset mode Git ends up with.
100///  An `enum` is a closed set of named alternatives.
101/// Why:
102///   `Hard`,
103///  `Merge` and `Keep` rewrite worktree files;
104///  `Mixed` and `Soft` do not.
105///
106/// In TS you'd write (pseudocode):
107/// ```ts
108/// type ResetMode = 'mixed' | 'soft' | 'hard' | 'merge' | 'keep';
109/// ```
110#[derive(Clone, Copy, Debug, Eq, PartialEq)]
111pub enum ResetMode {
112    /// Reset `HEAD` and the index.
113    Mixed,
114    /// Reset only `HEAD`.
115    Soft,
116    /// Reset `HEAD`,
117    ///  the index and the working tree.
118    Hard,
119    /// Reset `HEAD`,
120    ///  the index and the working tree,
121    ///  keeping unmerged local changes.
122    Merge,
123    /// Reset `HEAD` but keep local changes.
124    Keep,
125}
126
127/// What:
128///  Facts of one `git reset` region.
129///  `Option<ResetMode>` is "a mode or none stated".
130/// Why:
131///   With no mode option Git uses mixed,
132///  which leaves worktree files alone.
133///
134/// In TS you'd write (pseudocode):
135/// ```ts
136/// type ResetRegion = { hasDestructiveMode: boolean; hasEscapeHatch: boolean };
137/// ```
138#[derive(Clone, Debug, Eq, PartialEq)]
139pub struct ResetRegion {
140    /// The last mode option written,
141    ///  when any was.
142    pub mode: Option<ResetMode>,
143    /// Wrapper-only flags in option position;
144    ///  `escape` is `--no-enforce-worktree`.
145    pub wrapper: WrapperFlags,
146}
147
148/// What:
149///  The mode a table identifier stands for,
150///  or nothing for other rows.
151/// Why:
152///   One loop over the occurrences can then keep the last mode.
153///
154/// In TS you'd write (pseudocode):
155/// ```ts
156/// const mode = { [MIXED]: 'mixed', [SOFT]: 'soft', /* ... */ }[id];
157/// ```
158fn mode_of(id: u16) -> Option<ResetMode> {
159    if id == MIXED {
160        // `Some(x)` is the "present" case of `Option`.
161        return Some(ResetMode::Mixed);
162    }
163    if id == SOFT {
164        return Some(ResetMode::Soft);
165    }
166    if id == HARD {
167        return Some(ResetMode::Hard);
168    }
169    if id == MERGE {
170        return Some(ResetMode::Merge);
171    }
172    if id == KEEP {
173        return Some(ResetMode::Keep);
174    }
175    // `None` is the "absent" case of `Option`.
176    return None;
177}
178
179/// What:
180///  Parse the region after `reset`.
181///  `Result<A, B>` is "either success `A` or failure `B`".
182/// Why:
183///   A region Git itself would refuse yields the refusal;
184///  Git then resets nothing.
185///
186/// In TS you'd write (pseudocode):
187/// ```ts
188/// function parseResetRegion(region: string[], wrapperFlags: string[]): ResetRegion; // throws
189/// ```
190pub fn parse_reset_region(
191    region: &[OsString],
192    wrapper_flags: &[&[u8]],
193) -> Result<ResetRegion, OptionError> {
194    // The command's own escape hatch is flag 0; the caller's flags follow.
195    let mut flags: Vec<&[u8]> = Vec::<&[u8]>::with_capacity(wrapper_flags.len() + 1);
196    flags.push(WORKTREE_ENFORCEMENT_ESCAPE_HATCH.as_bytes());
197    for flag in wrapper_flags {
198        // `*flag` reads the `&[u8]` the loop variable points at.
199        flags.push(*flag);
200    }
201    // `?` returns Git's refusal to our caller, or unwraps the tokenized region.
202    let parsed: ParsedOptions = parse_options(region, RESET_TABLE, DEFAULT_MODE, flags.as_slice())?;
203    let mut mode: Option<ResetMode> = None;
204    // `for occurrence in &parsed.occurrences` borrows each record in the order Git applied it.
205    for occurrence in &parsed.occurrences {
206        let stated: Option<ResetMode> = mode_of(occurrence.id);
207        if stated.is_some() {
208            mode = stated;
209        }
210    }
211    // `Ok(x)` is the success case of `Result`.
212    return Ok(ResetRegion {
213        mode,
214        wrapper: split_wrapper_flags(parsed.wrapper.as_slice()),
215    });
216}
217
218/// What:
219///  Whether the reset can rewrite worktree files.
220///  `&ResetRegion` borrows the facts.
221/// Why:
222///   Only these three modes set `opts.update`,
223///  which lets Git write worktree files
224///       (`reset_index`,
225///  builtin/reset.c:79-95).
226///
227/// In TS you'd write (pseudocode):
228/// ```ts
229/// function resetChangesWorktree(region: ResetRegion): boolean;
230/// ```
231pub fn reset_changes_worktree(region: &ResetRegion) -> bool {
232    return region.mode == Some(ResetMode::Hard)
233        || region.mode == Some(ResetMode::Merge)
234        || region.mode == Some(ResetMode::Keep);
235}
236
237/// Modes in every accepted spelling,
238///  and real-Git controls of the table and of last-wins.
239#[cfg(test)]
240#[path = "command_reset_tests.rs"]
241mod tests;