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;