git_workflow/commands/
await_pr.rs1use std::process::Command;
24use std::thread;
25use std::time::Duration;
26
27use crate::commands::{cleanup, open};
28use crate::error::{GwError, Result};
29use crate::git;
30use crate::github::{self, PrState};
31use crate::output;
32
33pub fn run(
35 pr_number: u64,
36 open_browser: bool,
37 no_wait: bool,
38 no_cleanup: bool,
39 interval: u64,
40 verbose: bool,
41) -> Result<()> {
42 if !git::is_git_repo() {
43 return Err(GwError::NotAGitRepository);
44 }
45
46 println!();
47
48 if !github::is_gh_available() {
49 return Err(GwError::Other(
50 "GitHub CLI (gh) is not available. Install it from https://cli.github.com/".into(),
51 ));
52 }
53
54 let pr = match github::get_pr_for_branch(&pr_number.to_string())? {
56 Some(pr) => pr,
57 None => {
58 output::warn(&format!("No PR #{} found.", pr_number));
59 return Ok(());
60 }
61 };
62
63 let head_branch = pr.head_branch.clone();
66
67 output::info(&format!("PR: #{} {}", pr.number, pr.title));
68
69 if open_browser {
71 match open::open_url(&pr.url, verbose) {
72 Ok(()) => output::success(&format!("Opened {}", pr.url)),
73 Err(e) => output::warn(&format!("Could not open browser: {}", e)),
74 }
75 }
76
77 match &pr.state {
79 PrState::Merged { .. } => {
80 output::success(&format!("PR #{} is already merged", pr.number));
81 return finish_merged(&head_branch, no_cleanup, verbose);
82 }
83 PrState::Closed => {
84 output::warn(&format!(
85 "PR #{} is already closed without merging",
86 pr.number
87 ));
88 return Ok(());
89 }
90 PrState::Open => {}
91 }
92
93 if !no_wait {
95 output::info(&format!("Waiting for CI checks on PR #{}...", pr.number));
96 wait_for_ci(pr.number, verbose);
97 }
98
99 let poll_secs = interval.max(1);
101 output::info(&format!(
102 "Watching PR #{} for merge (every {}s)...",
103 pr.number, poll_secs
104 ));
105 let terminal = poll_until_terminal(pr.number, poll_secs);
106
107 match terminal {
109 PrState::Merged { .. } => {
110 output::success(&format!("PR #{} merged!", pr.number));
111 notify(&format!("PR #{} merged", pr.number), verbose);
112 finish_merged(&head_branch, no_cleanup, verbose)
113 }
114 PrState::Closed => {
115 output::warn(&format!("PR #{} was closed without merging", pr.number));
116 notify(
117 &format!("PR #{} closed without merging", pr.number),
118 verbose,
119 );
120 Ok(())
121 }
122 PrState::Open => unreachable!("poll_until_terminal only returns terminal states"),
123 }
124}
125
126fn finish_merged(head_branch: &str, no_cleanup: bool, verbose: bool) -> Result<()> {
128 if head_branch.is_empty() {
129 output::warn("Could not determine the PR's branch; skipping cleanup.");
132 output::hints(&["gw cleanup <branch> # Delete the merged branch manually"]);
133 return Ok(());
134 }
135 if no_cleanup {
136 output::ready("Merged", head_branch);
137 let hint = format!(
138 "gw cleanup {} # Delete the merged branch when ready",
139 head_branch
140 );
141 output::hints(&[&hint]);
142 return Ok(());
143 }
144 println!();
145 output::info(&format!("Cleaning up merged branch '{}'...", head_branch));
146 cleanup::run(Some(head_branch.to_string()), verbose)
147}
148
149fn wait_for_ci(pr_number: u64, verbose: bool) {
154 let num = pr_number.to_string();
155 let args = ["pr", "checks", &num, "--watch"];
156 if verbose {
157 output::action(&format!("gh {}", args.join(" ")));
158 }
159 match Command::new("gh").args(args).status() {
160 Ok(status) if status.success() => output::success("CI checks passed"),
161 Ok(_) => output::warn("CI checks did not all pass — continuing anyway"),
162 Err(e) => output::warn(&format!("Could not watch CI checks: {} — continuing", e)),
163 }
164}
165
166fn poll_until_terminal(pr_number: u64, interval_secs: u64) -> PrState {
171 let selector = pr_number.to_string();
172 let delay = Duration::from_secs(interval_secs);
173 loop {
174 match github::get_pr_for_branch(&selector) {
175 Ok(Some(pr)) => match pr.state {
176 PrState::Open => {}
177 terminal => return terminal,
178 },
179 Ok(None) => output::warn("PR not found while polling — retrying"),
180 Err(e) => output::warn(&format!("Could not fetch PR status: {} — retrying", e)),
181 }
182 thread::sleep(delay);
183 }
184}
185
186fn notify(message: &str, verbose: bool) {
192 let cmd = match std::env::var("GW_NOTIFY_CMD") {
193 Ok(cmd) if !cmd.is_empty() => cmd,
194 _ => return,
195 };
196 if verbose {
197 output::action(&format!("{} {}", cmd, message));
198 }
199 if let Err(e) = Command::new(&cmd).arg(message).status() {
200 output::warn(&format!("Notify command '{}' failed: {}", cmd, e));
201 }
202}