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;