git_workflow/commands/
await_pr.rs1use std::process::Command;
19use std::thread;
20use std::time::Duration;
21
22use crate::commands::{cleanup, open};
23use crate::error::{GwError, Result};
24use crate::git;
25use crate::github::{self, PrState};
26use crate::output;
27use crate::state::RepoType;
28
29pub fn run(
31 open_browser: bool,
32 no_wait: bool,
33 no_cleanup: bool,
34 interval: u64,
35 verbose: bool,
36) -> Result<()> {
37 if !git::is_git_repo() {
38 return Err(GwError::NotAGitRepository);
39 }
40
41 let repo_type = RepoType::detect()?;
42 let home_branch = repo_type.home_branch();
43 let current = git::current_branch()?;
44
45 println!();
46
47 if current == home_branch {
48 output::warn(&format!(
49 "On home branch '{}'. No PR to await.",
50 home_branch
51 ));
52 output::hints(&["gw new feature/your-feature # Start a branch first"]);
53 return Ok(());
54 }
55
56 if !github::is_gh_available() {
57 return Err(GwError::Other(
58 "GitHub CLI (gh) is not available. Install it from https://cli.github.com/".into(),
59 ));
60 }
61
62 let pr = match github::get_pr_for_branch(¤t)? {
63 Some(pr) => pr,
64 None => {
65 output::warn(&format!("No PR found for branch '{}'.", current));
66 output::hints(&["gh pr create -a \"@me\" -t \"...\" # Create a PR first"]);
67 return Ok(());
68 }
69 };
70
71 output::info(&format!("PR: #{} {}", pr.number, pr.title));
72
73 if open_browser {
75 match open::open_url(&pr.url, verbose) {
76 Ok(()) => output::success(&format!("Opened {}", pr.url)),
77 Err(e) => output::warn(&format!("Could not open browser: {}", e)),
78 }
79 }
80
81 match &pr.state {
83 PrState::Merged { .. } => {
84 output::success(&format!("PR #{} is already merged", pr.number));
85 return finish_merged(¤t, no_cleanup, verbose);
86 }
87 PrState::Closed => {
88 output::warn(&format!(
89 "PR #{} is already closed without merging",
90 pr.number
91 ));
92 return Ok(());
93 }
94 PrState::Open => {}
95 }
96
97 if !no_wait {
99 output::info(&format!("Waiting for CI checks on PR #{}...", pr.number));
100 wait_for_ci(pr.number, verbose);
101 }
102
103 let poll_secs = interval.max(1);
105 output::info(&format!(
106 "Watching PR #{} for merge (every {}s)...",
107 pr.number, poll_secs
108 ));
109 let terminal = poll_until_terminal(pr.number, poll_secs);
110
111 match terminal {
113 PrState::Merged { .. } => {
114 output::success(&format!("PR #{} merged!", pr.number));
115 notify(&format!("PR #{} merged", pr.number), verbose);
116 finish_merged(¤t, no_cleanup, verbose)
117 }
118 PrState::Closed => {
119 output::warn(&format!("PR #{} was closed without merging", pr.number));
120 notify(
121 &format!("PR #{} closed without merging", pr.number),
122 verbose,
123 );
124 Ok(())
125 }
126 PrState::Open => unreachable!("poll_until_terminal only returns terminal states"),
127 }
128}
129
130fn finish_merged(branch: &str, no_cleanup: bool, verbose: bool) -> Result<()> {
132 if no_cleanup {
133 output::ready("Merged", branch);
134 output::hints(&["gw cleanup # Delete the merged branch when ready"]);
135 return Ok(());
136 }
137 println!();
138 output::info("Cleaning up merged branch...");
139 cleanup::run(None, verbose)
140}
141
142fn wait_for_ci(pr_number: u64, verbose: bool) {
147 let num = pr_number.to_string();
148 let args = ["pr", "checks", &num, "--watch"];
149 if verbose {
150 output::action(&format!("gh {}", args.join(" ")));
151 }
152 match Command::new("gh").args(args).status() {
153 Ok(status) if status.success() => output::success("CI checks passed"),
154 Ok(_) => output::warn("CI checks did not all pass — continuing anyway"),
155 Err(e) => output::warn(&format!("Could not watch CI checks: {} — continuing", e)),
156 }
157}
158
159fn poll_until_terminal(pr_number: u64, interval_secs: u64) -> PrState {
164 let selector = pr_number.to_string();
165 let delay = Duration::from_secs(interval_secs);
166 loop {
167 match github::get_pr_for_branch(&selector) {
168 Ok(Some(pr)) => match pr.state {
169 PrState::Open => {}
170 terminal => return terminal,
171 },
172 Ok(None) => output::warn("PR not found while polling — retrying"),
173 Err(e) => output::warn(&format!("Could not fetch PR status: {} — retrying", e)),
174 }
175 thread::sleep(delay);
176 }
177}
178
179fn notify(message: &str, verbose: bool) {
185 let cmd = match std::env::var("GW_NOTIFY_CMD") {
186 Ok(cmd) if !cmd.is_empty() => cmd,
187 _ => return,
188 };
189 if verbose {
190 output::action(&format!("{} {}", cmd, message));
191 }
192 if let Err(e) = Command::new(&cmd).arg(message).status() {
193 output::warn(&format!("Notify command '{}' failed: {}", cmd, e));
194 }
195}