Skip to main content

git_policy_cli/
command_commit.rs

1//! What:
2//!  Facts about the arguments after `git commit`,
3//!  read as Git 2.56.0 reads them.
4//! Why:
5//!  The commit-only transform,
6//!  the dry-run check and the commit transaction all decide
7//!      from these facts.
8//!  Every fact is the final state after Git applied each option in
9//!      order,
10//!  so `--all --no-all` reports `all` off,
11//!  exactly as `git commit` would act.
12//!
13//! In TS you'd write (pseudocode):
14//! ```ts
15//! // parseCommitRegion(postSubcommandArgs): CommitRegion  (throws OptionError)
16//! ```
17
18/// What:
19///  Bring the commit table,
20///  the tokenizer and its questions into this file.
21/// Why:
22///   This module only interprets what the shared tokenizer found.
23///
24/// In TS you'd write (pseudocode):
25/// ```ts
26/// import { COMMIT_TABLE, ALL, ONLY /* ... */ } from './command_commit_table.ts';
27/// import { parseOptions } from './command_options.ts';
28/// ```
29use super::command_commit_table::{
30    ALL, ALLOW_EMPTY, AMEND, COMMIT_TABLE, DRY_RUN, FIXUP, INCLUDE, INTERACTIVE, LONG, NULL, ONLY,
31    PATCH, PATHSPEC_FILE_NUL, PATHSPEC_FROM_FILE, PORCELAIN, SHORT,
32};
33use super::command_options::{
34    DEFAULT_MODE, Occurrence, OptionError, OptionValue, ParsedOptions, parse_options,
35};
36use super::command_options_query::{
37    WrapperFlags, is_enabled, last_occurrence, positional_tokens, split_wrapper_flags, value_bytes,
38};
39/// `OsString` is owned operating-system text of raw bytes (sibling `String` must be UTF-8).
40use std::ffi::OsString;
41
42/// Wrapper-only flag that suppresses `-o` injection for one commit invocation.
43pub const COMMIT_ESCAPE_HATCH: &str = "--no-enforce-only";
44
45/// What:
46///  The form of a `--fixup` value (builtin/commit.c:1378-1411).
47///  An `enum` is a closed
48///       set of named alternatives.
49/// Why:
50///   `amend:` and `reword:` turn on `--allow-empty`;
51///  `reword:` also turns on `--only`
52///       itself and refuses paths and an explicit `-o`.
53///
54/// In TS you'd write (pseudocode):
55/// ```ts
56/// type FixupKind = 'plain' | 'amend' | 'reword' | 'unknown-suboption';
57/// ```
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub enum FixupKind {
60    /// `--fixup=<commit>`.
61    Plain,
62    /// `--fixup=amend:<commit>`.
63    Amend,
64    /// `--fixup=reword:<commit>`.
65    Reword,
66    /// Letters then `:` that are neither `amend` nor `reword`;
67    ///  Git dies on these.
68    UnknownSuboption,
69}
70
71/// What:
72///  Final-state facts of one `git commit` region.
73///  `Vec<usize>` is an owned list of
74///       token indexes;
75///  `Option<bool>` is "true,
76///  false,
77///  or never stated".
78/// Why:
79///   Indexes point into the caller's unchanged arguments,
80///  so paths keep their bytes.
81///
82/// In TS you'd write (pseudocode):
83/// ```ts
84/// type CommitRegion = { all: boolean; only?: boolean; include: boolean; /* ... */ pathspecs: number[] };
85/// ```
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct CommitRegion {
88    /// `-a`/`--all` is on.
89    pub all: bool,
90    /// `Some(true)` after `-o`/`--only`,
91    ///  `Some(false)` after `--no-only`,
92    ///  `None` if unstated.
93    pub only: Option<bool>,
94    /// `-i`/`--include` is on.
95    pub include: bool,
96    /// `--interactive` is on.
97    pub interactive: bool,
98    /// `-p`/`--patch` is on.
99    pub patch: bool,
100    /// `--dry-run`,
101    ///  a status format,
102    ///  or `-z` makes Git record no commit (commit.c:1422-1423).
103    pub dry_run: bool,
104    /// `--amend` is on.
105    pub amend: bool,
106    /// `--allow-empty` is on.
107    pub allow_empty: bool,
108    /// Form of the final `--fixup` value,
109    ///  when one is set.
110    pub fixup: Option<FixupKind>,
111    /// Position of the final `--pathspec-from-file` value,
112    ///  when one is set.
113    pub pathspec_from_file: Option<OptionValue>,
114    /// `--pathspec-file-nul` is on.
115    pub pathspec_file_nul: bool,
116    /// Region token indexes of positional pathspecs,
117    ///  before and after `--`.
118    pub pathspecs: Vec<usize>,
119    /// Wrapper-only flags in option position;
120    ///  `escape` is `--no-enforce-only`.
121    pub wrapper: WrapperFlags,
122}
123
124/// What:
125///  Classify a `--fixup` value by its leading letters.
126///  `&[u8]` borrows the value bytes.
127/// Why:
128///   Git treats `<letters>:` as a suboption and anything else as a commit name.
129///
130/// In TS you'd write (pseudocode):
131/// ```ts
132/// const match = /^([A-Za-z]+):/.exec(value); // then compare match[1]
133/// ```
134fn fixup_kind(value: &[u8]) -> FixupKind {
135    let mut letters: usize = 0;
136    // `is_ascii_alphabetic` matches C `isalpha` in Git's locale-independent ctype table.
137    while letters < value.len() && value[letters].is_ascii_alphabetic() {
138        letters += 1;
139    }
140    if letters == 0 || letters == value.len() || value[letters] != b':' {
141        return FixupKind::Plain;
142    }
143    // `&value[..letters]` borrows the bytes before the colon.
144    let suboption: &[u8] = &value[..letters];
145    if suboption == b"amend" {
146        return FixupKind::Amend;
147    }
148    if suboption == b"reword" {
149        return FixupKind::Reword;
150    }
151    return FixupKind::UnknownSuboption;
152}
153
154/// What:
155///  Whether the options leave Git with a status format,
156///  which forces a dry run.
157/// Why:
158///   `--short`,
159///  `--porcelain` and `--long` write one shared variable,
160///  so only the last
161///       of them counts and a negated form clears it (commit.c:1752-1761).
162///
163/// In TS you'd write (pseudocode):
164/// ```ts
165/// let format = false; for (const o of occurrences) if (isFormat(o.id)) format = !o.negated;
166/// ```
167fn has_status_format(parsed: &ParsedOptions) -> bool {
168    let mut format: bool = false;
169    // `for occurrence in &parsed.occurrences` borrows each record in the order Git applied it.
170    for occurrence in &parsed.occurrences {
171        if occurrence.id == SHORT || occurrence.id == PORCELAIN || occurrence.id == LONG {
172            format = !occurrence.negated;
173        }
174    }
175    return format;
176}
177
178/// What:
179///  The value of an option's final use,
180///  unless that use negated it.
181/// Why:
182///   `--no-pathspec-from-file` after a value clears it,
183///  as Git's `OPTION_FILENAME` does.
184///
185/// In TS you'd write (pseudocode):
186/// ```ts
187/// const value = last === undefined || last.negated ? undefined : last.value;
188/// ```
189fn final_value(parsed: &ParsedOptions, id: u16) -> Option<OptionValue> {
190    let last: Option<Occurrence> = last_occurrence(parsed, id);
191    // `if let Some(found) = last` runs only when the option was written.
192    if let Some(found) = last {
193        if found.negated {
194            // `None` is the "absent" case of `Option`.
195            return None;
196        }
197        return found.value;
198    }
199    return None;
200}
201
202/// What:
203///  Parse the region after `commit`.
204///  `wrapper_flags` lists the caller's other
205///       wrapper-only spellings;
206///  `Result<A, B>` is "either success `A` or failure `B`".
207/// Why:
208///   A region Git itself would refuse yields the refusal,
209///  never a guessed fact set.
210///
211/// In TS you'd write (pseudocode):
212/// ```ts
213/// function parseCommitRegion(region: string[], wrapperFlags: string[]): CommitRegion; // throws
214/// ```
215pub fn parse_commit_region(
216    region: &[OsString],
217    wrapper_flags: &[&[u8]],
218) -> Result<CommitRegion, OptionError> {
219    // The command's own escape hatch is flag 0; the caller's flags follow.
220    let mut flags: Vec<&[u8]> = Vec::<&[u8]>::with_capacity(wrapper_flags.len() + 1);
221    flags.push(COMMIT_ESCAPE_HATCH.as_bytes());
222    for flag in wrapper_flags {
223        // `*flag` reads the `&[u8]` the loop variable points at.
224        flags.push(*flag);
225    }
226    // `?` returns Git's refusal to our caller, or unwraps the tokenized region.
227    let parsed: ParsedOptions =
228        parse_options(region, COMMIT_TABLE, DEFAULT_MODE, flags.as_slice())?;
229    // `mut` lets the default "never stated" be replaced when the option was written.
230    let mut only: Option<bool> = None;
231    if let Some(stated) = last_occurrence(&parsed, ONLY) {
232        // `Some(x)` is the "present" case of `Option`.
233        only = Some(!stated.negated);
234    }
235    let mut fixup: Option<FixupKind> = None;
236    if let Some(value) = final_value(&parsed, FIXUP) {
237        fixup = Some(fixup_kind(value_bytes(region, value)));
238    }
239    // `Ok(x)` is the success case of `Result`.
240    return Ok(CommitRegion {
241        all: is_enabled(&parsed, ALL),
242        only,
243        include: is_enabled(&parsed, INCLUDE),
244        interactive: is_enabled(&parsed, INTERACTIVE),
245        patch: is_enabled(&parsed, PATCH),
246        dry_run: is_enabled(&parsed, DRY_RUN)
247            || has_status_format(&parsed)
248            || is_enabled(&parsed, NULL),
249        amend: is_enabled(&parsed, AMEND),
250        allow_empty: is_enabled(&parsed, ALLOW_EMPTY),
251        fixup,
252        pathspec_from_file: final_value(&parsed, PATHSPEC_FROM_FILE),
253        pathspec_file_nul: is_enabled(&parsed, PATHSPEC_FILE_NUL),
254        pathspecs: positional_tokens(&parsed, region.len()),
255        wrapper: split_wrapper_flags(parsed.wrapper.as_slice()),
256    });
257}
258
259/// Ported incumbent cases and the readings where Git 2.56.0 differs from the incumbent.
260#[cfg(test)]
261#[path = "command_commit_tests.rs"]
262mod tests;
263
264/// Real Git 2.56.0 controls for the table and for each divergence.
265#[cfg(test)]
266#[path = "command_commit_git_tests.rs"]
267mod git_tests;