1use crate::context::GitContext;
2
3const PROTECTED_BRANCHES: &[&str] = &["main", "master", "develop"];
4
5pub fn try_simple_pr_create(task: &str, ctx: &GitContext) -> Option<Result<String, String>> {
7 if !ctx.is_repo {
8 return None;
9 }
10
11 let lower = task.trim().to_lowercase();
12 if !is_simple_create_intent(&lower) {
13 return None;
14 }
15
16 let head = match ctx.branch.clone() {
17 Some(b) => b,
18 None => {
19 return Some(Err(
20 "Could not determine current branch. Run this inside a git repository."
21 .to_string(),
22 ));
23 }
24 };
25
26 if PROTECTED_BRANCHES.contains(&head.as_str()) {
27 return Some(Err(format!(
28 "You are on `{head}`. PRs need a feature branch.\n\n\
29 Example:\n \
30 git checkout -b feature/my-change\n \
31 git push origin feature/my-change\n \
32 git-cli \"create a PR to main\" --execute"
33 )));
34 }
35
36 let base = parse_target_base(&lower).unwrap_or_else(|| default_base_branch(ctx));
37 let title = title_from_branch(&head);
38 let body = format!("PR for branch {head}");
39
40 let mut lines = vec![
41 format!("# Push branch `{head}` to remote"),
42 format!("git push origin {head}"),
43 format!("# Create PR: {head} → {base}"),
44 format!(
45 "gh pr create --base {base} --head {head} --title \"{title}\" --body \"{body}\""
46 ),
47 ];
48
49 if let Some(ref prs) = ctx.open_prs {
50 let pattern = format!("{head} → {base}");
51 if prs.contains(&pattern) {
52 lines.insert(
53 0,
54 format!("# PR already exists for {head} → {base} (see Open PRs in context)"),
55 );
56 return Some(Ok(lines.join("\n")));
57 }
58 }
59
60 Some(Ok(lines.join("\n")))
61}
62
63fn is_simple_create_intent(lower: &str) -> bool {
64 const EXACT: &[&str] = &[
65 "create a pr",
66 "create pr",
67 "create a pull request",
68 "create pull request",
69 "open a pr",
70 "open pr",
71 ];
72 const PREFIX: &[&str] = &[
73 "create a pr to ",
74 "create pr to ",
75 "create a pull request to ",
76 "create pull request to ",
77 "open a pr to ",
78 "open pr to ",
79 ];
80
81 if EXACT.contains(&lower) {
82 return true;
83 }
84 if PREFIX.iter().any(|p| lower.starts_with(p)) {
85 return !lower.contains(" and ")
87 && !lower.contains("merge")
88 && !lower.contains("all ");
89 }
90 false
91}
92
93fn parse_target_base(lower: &str) -> Option<String> {
94 const PREFIXES: &[&str] = &[
95 "create a pr to ",
96 "create pr to ",
97 "create a pull request to ",
98 "create pull request to ",
99 "open a pr to ",
100 "open pr to ",
101 ];
102 for prefix in PREFIXES {
103 if let Some(rest) = lower.strip_prefix(prefix) {
104 let base = rest.trim();
105 if !base.is_empty() {
106 return Some(base.to_string());
107 }
108 }
109 }
110 None
111}
112
113fn default_base_branch(ctx: &GitContext) -> String {
114 if let Some(ref branches) = ctx.branches {
115 if branches.lines().any(|l| l.trim().trim_start_matches('*').trim() == "main") {
116 return "main".to_string();
117 }
118 if branches.lines().any(|l| l.trim().trim_start_matches('*').trim() == "master") {
119 return "master".to_string();
120 }
121 }
122 "main".to_string()
123}
124
125fn title_from_branch(head: &str) -> String {
126 if let Some(name) = head.strip_prefix("feature/") {
127 format!("feat: {name}")
128 } else if let Some(name) = head.strip_prefix("fix/") {
129 format!("fix: {name}")
130 } else {
131 format!("feat: {head}")
132 }
133}