Skip to main content

ghl/cmds/
pr.rs

1//! Pull request creation command.
2
3use owo_colors::OwoColorize;
4use rootcause::prelude::ResultExt;
5use rootcause::report;
6
7/// Prompt for a branch and create a pull request for it.
8pub fn run() -> rootcause::Result<()> {
9    let Some(branch) = ytil_tui::git_branch::select()? else {
10        return Ok(());
11    };
12
13    let title = pr_title_from_branch_name(branch.name_no_origin())?;
14    let pr_url = ytil_gh::pr::create(&title)?;
15    println!("{} title={title:?} pr_url={pr_url:?}", "PR created".green().bold());
16
17    Ok(())
18}
19
20/// Parses a branch name to generate a pull request title.
21fn pr_title_from_branch_name(branch_name: &str) -> rootcause::Result<String> {
22    let mut parts = branch_name.split('-');
23
24    let x = parts
25        .next()
26        .ok_or_else(|| report!("error malformed branch_name"))
27        .attach_with(|| format!("branch_name={branch_name:?}"))?;
28    let issue_number: usize = x
29        .parse()
30        .context("error parsing issue number")
31        .attach_with(|| format!("branch_name={branch_name:?} issue_number={x:?}"))?;
32
33    let mut title = String::with_capacity(branch_name.len());
34    for (i, word) in parts.enumerate() {
35        if i > 0 {
36            title.push(' ');
37        }
38        if i == 0 {
39            let mut chars = word.chars();
40            if let Some(first) = chars.next() {
41                for c in first.to_uppercase() {
42                    title.push(c);
43                }
44                title.push_str(chars.as_str());
45            }
46        } else {
47            title.push_str(word);
48        }
49    }
50
51    if title.is_empty() {
52        Err(report!("error empty title")).attach_with(|| format!("branch_name={branch_name:?}"))?;
53    }
54
55    Ok(format!("[{issue_number}]: {title}"))
56}
57
58#[cfg(test)]
59mod tests {
60    use rstest::rstest;
61    use test_that::prelude::*;
62
63    use super::*;
64
65    #[rstest]
66    #[case("43-foo-bar-baz", "[43]: Foo bar baz")]
67    #[case("1-hello", "[1]: Hello")]
68    #[case("123-long-branch-name-here", "[123]: Long branch name here")]
69    fn test_pr_title_from_branch_name_when_valid_input_formats_correctly(#[case] input: &str, #[case] expected: &str) {
70        assert_that!(pr_title_from_branch_name(input).unwrap(), eq(expected));
71    }
72
73    #[rstest]
74    #[case("abc-foo", "error parsing issue number")]
75    #[case("42", "error empty title")]
76    #[case("", "error parsing issue number")]
77    fn test_pr_title_from_branch_name_when_invalid_input_returns_error(
78        #[case] input: &str,
79        #[case] expected_ctx: &str,
80    ) {
81        assert_that!(
82            pr_title_from_branch_name(input),
83            err(result_of!(
84                |err: &rootcause::Report| err.format_current_context().to_string(),
85                eq(expected_ctx)
86            ))
87        );
88    }
89}