Skip to main content

git_policy_cli/
command_clean.rs

1//! What:
2//!  The complete `git clean` option table of Git 2.56.0 and the deletion facts read
3//!       from it.
4//! Why:
5//!  The linked-worktree policy guards a clean that can delete files.
6//!  Git's last
7//!      `--dry-run`/`--no-dry-run` wins,
8//!  in clusters (`-ndX`) and abbreviations (`--dry`).
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! // cleanChangesWorktree(parseCleanRegion(['-ndX'])) === false
13//! ```
14
15use super::command_options::{
16    Arity, DEFAULT_MODE, OptionError, OptionSpec, ParsedOptions, UNREAD, parse_options, row,
17};
18use super::command_options_query::{WrapperFlags, is_enabled, split_wrapper_flags};
19/// What:
20///  Bring the shared hatch spelling,
21///  the tokenizer,
22///  its table builder and its
23///       questions into this file.
24/// Why:
25///   This module only declares Git's table and interprets what the tokenizer found.
26///
27/// In TS you'd write (pseudocode):
28/// ```ts
29/// import { parseOptions, row } from './command_options.ts';
30/// ```
31use super::escape_hatch::WORKTREE_ENFORCEMENT_ESCAPE_HATCH;
32/// `OsString` is owned operating-system text of raw bytes (sibling `String` must be UTF-8).
33use std::ffi::OsString;
34
35/// `-n`,
36///  `--dry-run`.
37pub const DRY_RUN: u16 = 1;
38/// `-i`,
39///  `--interactive`.
40pub const INTERACTIVE: u16 = 2;
41
42/// What:
43///  Rows of `options[]` in `cmd_clean` (builtin/clean.c:936-948),
44///  in source order.
45///       `&[OptionSpec]` is a borrowed table baked into the program;
46///  `None` in the long
47///       position means the option has only a letter.
48/// Why:
49///   `-e`/`--exclude` takes a value and is `PARSE_OPT_NONEG`;
50///  `-d`,
51///  `-x` and `-X` have
52///       no long spelling.
53///
54/// In TS you'd write (pseudocode):
55/// ```ts
56/// export const CLEAN_TABLE = [row(UNREAD, 'q', 'quiet', 'none', true), /* ... */] as const;
57/// ```
58pub const CLEAN_TABLE: &[OptionSpec] = &[
59    row(UNREAD, Some(b'q'), Some("quiet"), Arity::None, true),
60    row(DRY_RUN, Some(b'n'), Some("dry-run"), Arity::None, true),
61    row(UNREAD, Some(b'f'), Some("force"), Arity::None, true),
62    row(
63        INTERACTIVE,
64        Some(b'i'),
65        Some("interactive"),
66        Arity::None,
67        true,
68    ),
69    row(UNREAD, Some(b'd'), None, Arity::None, true),
70    row(UNREAD, Some(b'e'), Some("exclude"), Arity::Required, false),
71    row(UNREAD, Some(b'x'), None, Arity::None, true),
72    row(UNREAD, Some(b'X'), None, Arity::None, true),
73];
74
75/// What:
76///  Final-state facts of one `git clean` region.
77///  `bool` is true or false.
78/// Why:
79///   The policy decides from the final dry-run state;
80///  `interactive` is reported for
81///       its diagnostics.
82///
83/// In TS you'd write (pseudocode):
84/// ```ts
85/// type CleanRegion = { dryRunActive: boolean; interactiveActive: boolean; hasEscapeHatch: boolean };
86/// ```
87#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct CleanRegion {
89    /// `-n`/`--dry-run` is on after every option was applied.
90    pub dry_run: bool,
91    /// `-i`/`--interactive` is on after every option was applied.
92    pub interactive: bool,
93    /// Wrapper-only flags in option position;
94    ///  `escape` is `--no-enforce-worktree`.
95    pub wrapper: WrapperFlags,
96}
97
98/// What:
99///  Parse the region after `clean`.
100///  `Result<A, B>` is "either success `A` or failure `B`".
101/// Why:
102///   A region Git itself would refuse yields the refusal;
103///  Git then deletes nothing.
104///
105/// In TS you'd write (pseudocode):
106/// ```ts
107/// function parseCleanRegion(region: string[], wrapperFlags: string[]): CleanRegion; // throws
108/// ```
109pub fn parse_clean_region(
110    region: &[OsString],
111    wrapper_flags: &[&[u8]],
112) -> Result<CleanRegion, OptionError> {
113    // The command's own escape hatch is flag 0; the caller's flags follow.
114    let mut flags: Vec<&[u8]> = Vec::<&[u8]>::with_capacity(wrapper_flags.len() + 1);
115    flags.push(WORKTREE_ENFORCEMENT_ESCAPE_HATCH.as_bytes());
116    for flag in wrapper_flags {
117        // `*flag` reads the `&[u8]` the loop variable points at.
118        flags.push(*flag);
119    }
120    // `?` returns Git's refusal to our caller, or unwraps the tokenized region.
121    let parsed: ParsedOptions = parse_options(region, CLEAN_TABLE, DEFAULT_MODE, flags.as_slice())?;
122    // `Ok(x)` is the success case of `Result`.
123    return Ok(CleanRegion {
124        dry_run: is_enabled(&parsed, DRY_RUN),
125        interactive: is_enabled(&parsed, INTERACTIVE),
126        wrapper: split_wrapper_flags(parsed.wrapper.as_slice()),
127    });
128}
129
130/// What:
131///  Whether the clean can delete files.
132///  `&CleanRegion` borrows the facts.
133/// Why:
134///   Every `unlink` and `rmdir` in Git's clean is skipped under dry run,
135///  also after
136///       the interactive menu chose what to delete (builtin/clean.c:191,
137///  223,
138///  267,
139///       1050-1078).
140/// Gotcha:
141///  Divergence from the incumbent,
142///  which treated every interactive clean as
143///         deleting:
144///  `git clean -i -n` deletes nothing in Git 2.56.0.
145///
146/// In TS you'd write (pseudocode):
147/// ```ts
148/// function cleanChangesWorktree(region: CleanRegion): boolean;
149/// ```
150pub fn clean_changes_worktree(region: &CleanRegion) -> bool {
151    return !region.dry_run;
152}
153
154/// Facts in every accepted spelling,
155///  and real-Git controls of the table and of dry runs.
156#[cfg(test)]
157#[path = "command_clean_tests.rs"]
158mod tests;