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;