1use std::io::Write;
7
8use owo_colors::OwoColorize;
9use rootcause::prelude::ResultExt;
10use rootcause::report;
11use url::Url;
12use ytil_git::CmdError;
13use ytil_sys::cli::Args;
14
15#[ytil_sys::main]
17fn main() -> rootcause::Result<()> {
18 let args = ytil_sys::cli::get();
19 if args.has_help() {
20 println!("{}", include_str!("../help.txt"));
21 return Ok(());
22 }
23 let args: Vec<_> = args.iter().map(String::as_str).collect();
24
25 match args.split_first() {
26 None => autocomplete_git_branches_and_switch(),
27 Some((hd, _)) if *hd == "-" => ytil_git::branch::switch(hd)
29 .inspect(|()| report_branch_switch(hd))
30 .map_err(From::from),
31 Some((hd, tail)) if *hd == "-b" => create_branch_and_switch(&build_branch_name(tail)?),
32 Some((hd, &[])) => handle_single_input_argument(hd),
33 _ => create_branch_and_switch(&build_branch_name(&args)?),
34 }?;
35
36 Ok(())
37}
38
39fn autocomplete_git_branches_and_switch() -> rootcause::Result<()> {
51 let Some(branch) = ytil_tui::git_branch::select()? else {
52 return Ok(());
53 };
54
55 let branch_name_no_origin = branch.name_no_origin();
56 ytil_git::branch::switch(branch_name_no_origin).inspect(|()| report_branch_switch(branch_name_no_origin))?;
57
58 Ok(())
59}
60
61fn handle_single_input_argument(arg: &str) -> rootcause::Result<()> {
72 let branch_name = if let Ok(url) = Url::parse(arg) {
73 ytil_gh::log_into_github()?;
74 ytil_gh::get_branch_name_from_url(&url)?
75 } else {
76 arg.to_string()
77 };
78
79 match ytil_git::branch::switch(&branch_name).map_err(|e| *e) {
80 Err(CmdError::CmdFailure { stderr, .. }) if stderr.contains("invalid reference: ") => {
81 create_branch_and_switch(&branch_name)
82 }
83 other => Ok(other?),
84 }
85}
86
87fn create_branch_and_switch(branch_name: &str) -> rootcause::Result<()> {
99 if !should_create_new_branch(branch_name)? {
100 return Ok(());
101 }
102 if let Err(err) = ytil_git::branch::create_from_default_branch(branch_name, None) {
103 if err.to_string().contains("already exists") {
104 ytil_git::branch::switch(branch_name).inspect(|()| report_branch_exists(branch_name))?;
105 return Ok(());
106 }
107 return Err(err);
108 }
109 ytil_git::branch::switch(branch_name).inspect(|()| report_branch_new(branch_name))?;
110 Ok(())
111}
112
113fn should_create_new_branch(branch_name: &str) -> rootcause::Result<bool> {
124 let default_branch = ytil_git::branch::get_default()?;
125 if default_branch == branch_name {
126 return Ok(true);
127 }
128 let curr_branch = ytil_git::branch::get_current()?;
129 if default_branch == curr_branch {
130 return Ok(true);
131 }
132 ask_branching_from_not_default(branch_name, &curr_branch);
133 std::io::stdout().flush()?;
134 let mut input = String::new();
135 std::io::stdin().read_line(&mut input)?;
136 if !input.trim().is_empty() {
137 report_branch_not_created(branch_name);
138 return Ok(false);
139 }
140 Ok(true)
141}
142
143fn build_branch_name(args: &[&str]) -> rootcause::Result<String> {
155 fn is_permitted(c: char) -> bool {
156 const PERMITTED_CHARS: [char; 3] = ['.', '/', '_'];
157 c.is_alphanumeric() || PERMITTED_CHARS.contains(&c)
158 }
159
160 let mut branch_name = String::new();
164 let mut need_separator = false;
165
166 for arg in args {
167 for token in arg.split_whitespace() {
168 for c in token.chars() {
169 if is_permitted(c) {
170 if need_separator && !branch_name.is_empty() {
171 branch_name.push('-');
172 }
173 need_separator = false;
174 for lc in c.to_lowercase() {
175 branch_name.push(lc);
176 }
177 } else {
178 need_separator = true;
180 }
181 }
182 need_separator = true;
184 }
185 }
186
187 if branch_name.is_empty() {
188 Err(report!("branch name construction produced empty string")).attach_with(|| format!("args={args:#?}"))?;
189 }
190
191 Ok(branch_name)
192}
193
194fn report_branch_switch(branch_name: &str) {
196 println!("{} {}", ">".magenta().bold(), branch_name.bold());
197}
198
199fn report_branch_new(branch_name: &str) {
201 println!("{} {}", "+".green().bold(), branch_name.bold());
202}
203
204fn report_branch_exists(branch_name: &str) {
206 println!("{}{} {}", "!".blue().bold(), ">".magenta().bold(), branch_name.bold());
207}
208
209fn report_branch_not_created(branch_name: &str) {
211 print!("{} {} not created", "x".red().bold(), branch_name.bold());
212}
213
214fn ask_branching_from_not_default(branch_name: &str, default_branch_name: &str) {
216 print!(
217 "{} {} from {}",
218 "*".cyan().bold(),
219 branch_name.bold(),
220 default_branch_name.bold()
221 );
222}
223
224#[cfg(test)]
225mod tests {
226 use rstest::rstest;
227
228 use super::*;
229
230 #[rstest]
231 #[case::empty_input("", "branch name construction produced empty string")]
232 #[case::invalid_characters_only("❌", "branch name construction produced empty string")]
233 fn test_build_branch_name_fails_as_expected(#[case] input: &str, #[case] expected_ctx: &str) {
234 assert2::assert!(let Err(err) = build_branch_name(&[input]));
235 assert_eq!(err.format_current_context().to_string(), expected_ctx);
236 }
237
238 #[rstest]
239 #[case::single_word(&["HelloWorld"], "helloworld")]
240 #[case::space_separated(&["Hello World"], "hello-world")]
241 #[case::special_characters(&["Feature: Implement User Login!"], "feature-implement-user-login")]
242 #[case::version_number(&["Version 2.0"], "version-2.0")]
243 #[case::multiple_separators(&["This---is...a_test"], "this-is...a_test")]
244 #[case::leading_trailing_spaces(&[" Leading and trailing "], "leading-and-trailing")]
245 #[case::emoji(&["Hello 🌎 World"], "hello-world")]
246 #[case::emoji_at_start_end(&["🚀Launch🚀Day"], "launch-day")]
247 #[case::multiple_emojis(&["Smile 😊 and 🤖 code"], "smile-and-code")]
248 #[case::multiple_args(&["Hello", "World"], "hello-world")]
249 #[case::args_with_spaces(&["Hello World", "World"], "hello-world-world")]
250 #[case::mixed_args(&["Hello World", "🌎", "42"], "hello-world-42")]
251 #[case::special_chars_in_args(&["This", "---is.", "..a_test"], "this-is.-..a_test")]
252 #[case::dependabot_path(&["dependabot/cargo/opentelemetry-0.27.1"], "dependabot/cargo/opentelemetry-0.27.1")]
253 fn test_build_branch_name_succeeds_as_expected(#[case] input: &[&str], #[case] expected_output: &str) {
254 assert2::assert!(let Ok(actual_output) = build_branch_name(input));
255 assert_eq!(actual_output, expected_output);
256 }
257}