Skip to main content

git_policy_cli/
command_branch_create.rs

1//! What:
2//!  Branch-creation facts of `git branch`,
3//!  `git checkout` and `git switch` regions,
4//!       read as Git 2.56.0 reads them.
5//! Why:
6//!  The branch-worktree policy rejects creating a branch in the current worktree.
7//!  It
8//!      needs the explicit forms and the name Git may turn into a new local branch by
9//!      matching a remote branch,
10//!  for every spelling Git accepts.
11//! Gotcha:
12//!  The facts read option names and argument counts only.
13//!  Where Git later refuses
14//!         for an option value,
15//!  a combination of options,
16//!  configuration or repository
17//!         state,
18//!  the answer stays "creates":
19//!  it errs toward the policy seeing the command.
20//!
21//! In TS you'd write (pseudocode):
22//! ```ts
23//! // parseBranchCreationRegion({ subcommand: 'switch', postSubcommandArgs: ['-c', 'topic'] }).createsBranch
24//! ```
25
26/// What:
27///  Bring the tables,
28///  the mode and target decisions,
29///  the shared hatch spelling,
30///  the
31///       tokenizer and its questions into this file.
32/// Why:
33///   This module only selects the command's table and combines the decisions.
34///
35/// In TS you'd write (pseudocode):
36/// ```ts
37/// import { parseOptions } from './command_options.ts';
38/// ```
39use super::command_branch_mode::branch_creates;
40use super::command_branch_table::BRANCH_TABLE;
41use super::command_branch_target::{
42    checkout_target, creates_explicitly, guess_allowed, switch_target,
43};
44use super::command_checkout_table::{CHECKOUT_TABLE, SWITCH_TABLE};
45use super::command_options::{DEFAULT_MODE, OptionError, OptionSpec, ParsedOptions, parse_options};
46use super::command_options_query::{WrapperFlags, positional_tokens, split_wrapper_flags};
47use super::escape_hatch::BRANCH_WORKTREE_ESCAPE_HATCH;
48/// `OsString` is owned operating-system text of raw bytes (sibling `String` must be UTF-8).
49use std::ffi::OsString;
50
51/// What:
52///  The guarded subcommands.
53///  An `enum` is a closed set of named alternatives.
54/// Why:
55///   Each has its own option table and its own rule for what a positional name means.
56///
57/// In TS you'd write (pseudocode):
58/// ```ts
59/// type BranchCreationSubcommand = 'branch' | 'checkout' | 'switch';
60/// ```
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub enum BranchCreationCommand {
63    /// `git branch`.
64    Branch,
65    /// `git checkout`.
66    Checkout,
67    /// `git switch`.
68    Switch,
69}
70
71/// What:
72///  Facts of one guarded region.
73///  `Option<usize>` is "a token index or nothing".
74/// Why:
75///   `implicit_creation_target` names the argument a repository probe must check
76///       against remote branches;
77///  this module never runs that probe.
78///
79/// In TS you'd write (pseudocode):
80/// ```ts
81/// type BranchCreationRegion = { createsBranch: boolean; hasEscapeHatch: boolean;
82///   implicitCreationTarget: string | typeof NO_IMPLICIT_CREATION_TARGET };
83/// ```
84#[derive(Clone, Debug, Eq, PartialEq)]
85pub struct BranchCreationRegion {
86    /// The options ask Git to create,
87    ///  reset or copy a branch.
88    pub creates_branch: bool,
89    /// Region token index of the name Git would create by matching one remote branch.
90    pub implicit_creation_target: Option<usize>,
91    /// Wrapper-only flags in option position;
92    ///  `escape` is `--no-enforce-worktree-branch`.
93    pub wrapper: WrapperFlags,
94}
95
96/// What:
97///  The guarded command a subcommand word names,
98///  or nothing.
99///  `&[u8]` borrows the word.
100/// Why:
101///   Callers hold the subcommand token as raw bytes.
102///
103/// In TS you'd write (pseudocode):
104/// ```ts
105/// const guarded = ['branch', 'checkout', 'switch'].includes(word) ? word : undefined;
106/// ```
107pub fn branch_creation_command(word: &[u8]) -> Option<BranchCreationCommand> {
108    if word == b"branch" {
109        // `Some(x)` is the "present" case of `Option`.
110        return Some(BranchCreationCommand::Branch);
111    }
112    if word == b"checkout" {
113        return Some(BranchCreationCommand::Checkout);
114    }
115    if word == b"switch" {
116        return Some(BranchCreationCommand::Switch);
117    }
118    // `None` is the "absent" case of `Option`.
119    return None;
120}
121
122/// What:
123///  Parse the region after a guarded subcommand.
124///  `Result<A, B>` is "either success `A`
125///       or failure `B`".
126/// Why:
127///   A region Git itself would refuse yields the refusal;
128///  Git then creates nothing.
129///
130/// In TS you'd write (pseudocode):
131/// ```ts
132/// function parseBranchCreationRegion({ subcommand, postSubcommandArgs }): BranchCreationRegion; // throws
133/// ```
134pub fn parse_branch_creation_region(
135    command: BranchCreationCommand,
136    region: &[OsString],
137    wrapper_flags: &[&[u8]],
138) -> Result<BranchCreationRegion, OptionError> {
139    // The command's own escape hatch is flag 0; the caller's flags follow.
140    let mut flags: Vec<&[u8]> = Vec::<&[u8]>::with_capacity(wrapper_flags.len() + 1);
141    flags.push(BRANCH_WORKTREE_ESCAPE_HATCH.as_bytes());
142    for flag in wrapper_flags {
143        // `*flag` reads the `&[u8]` the loop variable points at.
144        flags.push(*flag);
145    }
146    // `match` picks one arm per variant; the compiler refuses a forgotten variant.
147    let table: &[OptionSpec] = match command {
148        BranchCreationCommand::Branch => BRANCH_TABLE,
149        BranchCreationCommand::Checkout => CHECKOUT_TABLE,
150        BranchCreationCommand::Switch => SWITCH_TABLE,
151    };
152    // `?` returns Git's refusal to our caller, or unwraps the tokenized region.
153    let parsed: ParsedOptions = parse_options(region, table, DEFAULT_MODE, flags.as_slice())?;
154    let wrapper: WrapperFlags = split_wrapper_flags(parsed.wrapper.as_slice());
155    if command == BranchCreationCommand::Branch {
156        let names: usize = positional_tokens(&parsed, region.len()).len();
157        // `Ok(x)` is the success case of `Result`.
158        return Ok(BranchCreationRegion {
159            creates_branch: branch_creates(&parsed, names),
160            implicit_creation_target: None,
161            wrapper,
162        });
163    }
164    let creates_branch: bool = creates_explicitly(&parsed);
165    let mut implicit_creation_target: Option<usize> = None;
166    if !creates_branch && guess_allowed(&parsed) {
167        implicit_creation_target = if command == BranchCreationCommand::Checkout {
168            checkout_target(&parsed, region)
169        } else {
170            switch_target(&parsed, region)
171        };
172    }
173    return Ok(BranchCreationRegion {
174        creates_branch,
175        implicit_creation_target,
176        wrapper,
177    });
178}
179
180/// `git branch` creation,
181///  escape hatch positions and refusals.
182#[cfg(test)]
183#[path = "command_branch_create_tests.rs"]
184mod tests;
185
186/// Explicit creation and the guess candidate of `git checkout` and `git switch`.
187#[cfg(test)]
188#[path = "command_branch_create_target_tests.rs"]
189mod target_tests;
190
191/// Real Git 2.56.0 controls:
192///  the three tables and a differential on `git branch`.
193#[cfg(test)]
194#[path = "command_branch_create_git_tests.rs"]
195mod git_tests;
196
197/// Real Git 2.56.0 controls:
198///  creation by `git checkout` and `git switch`.
199#[cfg(test)]
200#[path = "command_branch_create_guess_tests.rs"]
201mod guess_tests;