1#![feature(exit_status_error)]
29
30use std::io::Write;
31
32use color_eyre::eyre::bail;
33use color_eyre::owo_colors::OwoColorize as _;
34use url::Url;
35use ytil_git::CmdError;
36use ytil_sys::cli::Args;
37
38fn autocomplete_git_branches_and_switch() -> color_eyre::Result<()> {
50 let Some(branch) = ytil_tui::git_branch::select()? else {
51 return Ok(());
52 };
53
54 let branch_name_no_origin = branch.name_no_origin();
55 ytil_git::branch::switch(branch_name_no_origin).inspect(|()| report_branch_switch(branch_name_no_origin))?;
56
57 Ok(())
58}
59
60fn handle_single_input_argument(arg: &str) -> color_eyre::Result<()> {
71 let branch_name = if let Ok(url) = Url::parse(arg) {
72 ytil_gh::log_into_github()?;
73 ytil_gh::get_branch_name_from_url(&url)?
74 } else {
75 arg.to_string()
76 };
77
78 match ytil_git::branch::switch(&branch_name).map_err(|e| *e) {
79 Err(CmdError::CmdFailure { stderr, .. }) if stderr.contains("invalid reference: ") => {
80 create_branch_and_switch(&branch_name)
81 }
82 other => Ok(other?),
83 }
84}
85
86fn create_branch_and_switch(branch_name: &str) -> color_eyre::Result<()> {
98 if !should_create_new_branch(branch_name)? {
99 return Ok(());
100 }
101 if let Err(err) = ytil_git::branch::create_from_default_branch(branch_name, None) {
102 if err.to_string().contains("already exists") {
103 ytil_git::branch::switch(branch_name).inspect(|()| report_branch_exists(branch_name))?;
104 return Ok(());
105 }
106 return Err(err);
107 }
108 ytil_git::branch::switch(branch_name).inspect(|()| report_branch_new(branch_name))?;
109 Ok(())
110}
111
112fn should_create_new_branch(branch_name: &str) -> color_eyre::Result<bool> {
123 let default_branch = ytil_git::branch::get_default()?;
124 if default_branch == branch_name {
125 return Ok(true);
126 }
127 let curr_branch = ytil_git::branch::get_current()?;
128 if default_branch == curr_branch {
129 return Ok(true);
130 }
131 ask_branching_from_not_default(branch_name, &curr_branch);
132 std::io::stdout().flush()?;
133 let mut input = String::new();
134 std::io::stdin().read_line(&mut input)?;
135 if !input.trim().is_empty() {
136 report_branch_not_created(branch_name);
137 return Ok(false);
138 }
139 Ok(true)
140}
141
142fn build_branch_name(args: &[&str]) -> color_eyre::Result<String> {
154 fn is_permitted(c: char) -> bool {
155 const PERMITTED_CHARS: [char; 3] = ['.', '/', '_'];
156 c.is_alphanumeric() || PERMITTED_CHARS.contains(&c)
157 }
158
159 let branch_name = args
160 .iter()
161 .flat_map(|x| {
162 x.split_whitespace().filter_map(|y| {
163 let z = y
164 .chars()
165 .map(|c| if is_permitted(c) { c } else { ' ' })
166 .collect::<String>()
167 .split_whitespace()
168 .collect::<Vec<_>>()
169 .join("-")
170 .to_lowercase();
171 if z.is_empty() {
172 return None;
173 }
174 Some(z)
175 })
176 })
177 .collect::<Vec<_>>()
178 .join("-");
179
180 if branch_name.is_empty() {
181 bail!("branch name construction produced empty string | args={args:#?}")
182 }
183
184 Ok(branch_name)
185}
186
187fn report_branch_switch(branch_name: &str) {
189 println!("{} {}", ">".magenta().bold(), branch_name.bold());
190}
191
192fn report_branch_new(branch_name: &str) {
194 println!("{} {}", "+".green().bold(), branch_name.bold());
195}
196
197fn report_branch_exists(branch_name: &str) {
199 println!("{}{} {}", "!".blue().bold(), ">".magenta().bold(), branch_name.bold());
200}
201
202fn report_branch_not_created(branch_name: &str) {
204 print!("{} {} not created", "x".red().bold(), branch_name.bold());
205}
206
207fn ask_branching_from_not_default(branch_name: &str, default_branch_name: &str) {
209 print!(
210 "{} {} from {}",
211 "*".cyan().bold(),
212 branch_name.bold(),
213 default_branch_name.bold()
214 );
215}
216
217fn main() -> color_eyre::Result<()> {
219 color_eyre::install()?;
220
221 let args = ytil_sys::cli::get();
222 if args.has_help() {
223 println!("{}", include_str!("../help.txt"));
224 return Ok(());
225 }
226 let args: Vec<_> = args.iter().map(String::as_str).collect();
227
228 match args.split_first() {
229 None => autocomplete_git_branches_and_switch(),
230 Some((hd, _)) if *hd == "-" => ytil_git::branch::switch(hd)
232 .inspect(|()| report_branch_switch(hd))
233 .map_err(From::from),
234 Some((hd, tail)) if *hd == "-b" => create_branch_and_switch(&build_branch_name(tail)?),
235 Some((hd, &[])) => handle_single_input_argument(hd),
236 _ => create_branch_and_switch(&build_branch_name(&args)?),
237 }?;
238
239 Ok(())
240}
241
242#[cfg(test)]
243mod tests {
244 use rstest::rstest;
245
246 use super::*;
247
248 #[rstest]
249 #[case::empty_input("", "branch name construction produced empty string | args=[\n \"\",\n]")]
250 #[case::invalid_characters_only("❌", "branch name construction produced empty string | args=[\n \"❌\",\n]")]
251 fn build_branch_name_fails_as_expected(#[case] input: &str, #[case] expected_output: &str) {
252 assert2::let_assert!(Err(actual_error) = build_branch_name(&[input]));
253 assert_eq!(format!("{actual_error}"), expected_output);
254 }
255
256 #[rstest]
257 #[case::single_word(&["HelloWorld"], "helloworld")]
258 #[case::space_separated(&["Hello World"], "hello-world")]
259 #[case::special_characters(&["Feature: Implement User Login!"], "feature-implement-user-login")]
260 #[case::version_number(&["Version 2.0"], "version-2.0")]
261 #[case::multiple_separators(&["This---is...a_test"], "this-is...a_test")]
262 #[case::leading_trailing_spaces(&[" Leading and trailing "], "leading-and-trailing")]
263 #[case::emoji(&["Hello 🌎 World"], "hello-world")]
264 #[case::emoji_at_start_end(&["🚀Launch🚀Day"], "launch-day")]
265 #[case::multiple_emojis(&["Smile 😊 and 🤖 code"], "smile-and-code")]
266 #[case::multiple_args(&["Hello", "World"], "hello-world")]
267 #[case::args_with_spaces(&["Hello World", "World"], "hello-world-world")]
268 #[case::mixed_args(&["Hello World", "🌎", "42"], "hello-world-42")]
269 #[case::special_chars_in_args(&["This", "---is.", "..a_test"], "this-is.-..a_test")]
270 #[case::dependabot_path(&["dependabot/cargo/opentelemetry-0.27.1"], "dependabot/cargo/opentelemetry-0.27.1")]
271 fn build_branch_name_succeeds_as_expected(#[case] input: &[&str], #[case] expected_output: &str) {
272 assert2::let_assert!(Ok(actual_output) = build_branch_name(input));
273 assert_eq!(actual_output, expected_output);
274 }
275}