use std::process::Command;
use std::thread;
use std::time::Duration;
use crate::commands::{cleanup, open};
use crate::error::{GwError, Result};
use crate::git;
use crate::github::{self, PrState};
use crate::output;
pub fn run(
pr_number: u64,
open_browser: bool,
no_wait: bool,
no_cleanup: bool,
interval: u64,
verbose: bool,
) -> Result<()> {
if !git::is_git_repo() {
return Err(GwError::NotAGitRepository);
}
println!();
if !github::is_gh_available() {
return Err(GwError::Other(
"GitHub CLI (gh) is not available. Install it from https://cli.github.com/".into(),
));
}
let pr = match github::get_pr_for_branch(&pr_number.to_string())? {
Some(pr) => pr,
None => {
output::warn(&format!("No PR #{} found.", pr_number));
return Ok(());
}
};
let head_branch = pr.head_branch.clone();
output::info(&format!("PR: #{} {}", pr.number, pr.title));
if open_browser {
match open::open_url(&pr.url, verbose) {
Ok(()) => output::success(&format!("Opened {}", pr.url)),
Err(e) => output::warn(&format!("Could not open browser: {}", e)),
}
}
match &pr.state {
PrState::Merged { .. } => {
output::success(&format!("PR #{} is already merged", pr.number));
return finish_merged(&head_branch, no_cleanup, verbose);
}
PrState::Closed => {
output::warn(&format!(
"PR #{} is already closed without merging",
pr.number
));
return Ok(());
}
PrState::Open => {}
}
if !no_wait {
output::info(&format!("Waiting for CI checks on PR #{}...", pr.number));
wait_for_ci(pr.number, verbose);
}
let poll_secs = interval.max(1);
output::info(&format!(
"Watching PR #{} for merge (every {}s)...",
pr.number, poll_secs
));
let terminal = poll_until_terminal(pr.number, poll_secs);
match terminal {
PrState::Merged { .. } => {
output::success(&format!("PR #{} merged!", pr.number));
notify(&format!("PR #{} merged", pr.number), verbose);
finish_merged(&head_branch, no_cleanup, verbose)
}
PrState::Closed => {
output::warn(&format!("PR #{} was closed without merging", pr.number));
notify(
&format!("PR #{} closed without merging", pr.number),
verbose,
);
Ok(())
}
PrState::Open => unreachable!("poll_until_terminal only returns terminal states"),
}
}
fn finish_merged(head_branch: &str, no_cleanup: bool, verbose: bool) -> Result<()> {
if head_branch.is_empty() {
output::warn("Could not determine the PR's branch; skipping cleanup.");
output::hints(&["gw cleanup <branch> # Delete the merged branch manually"]);
return Ok(());
}
if no_cleanup {
output::ready("Merged", head_branch);
let hint = format!(
"gw cleanup {} # Delete the merged branch when ready",
head_branch
);
output::hints(&[&hint]);
return Ok(());
}
println!();
output::info(&format!("Cleaning up merged branch '{}'...", head_branch));
cleanup::run(Some(head_branch.to_string()), verbose)
}
fn wait_for_ci(pr_number: u64, verbose: bool) {
let num = pr_number.to_string();
let args = ["pr", "checks", &num, "--watch"];
if verbose {
output::action(&format!("gh {}", args.join(" ")));
}
match Command::new("gh").args(args).status() {
Ok(status) if status.success() => output::success("CI checks passed"),
Ok(_) => output::warn("CI checks did not all pass — continuing anyway"),
Err(e) => output::warn(&format!("Could not watch CI checks: {} — continuing", e)),
}
}
fn poll_until_terminal(pr_number: u64, interval_secs: u64) -> PrState {
let selector = pr_number.to_string();
let delay = Duration::from_secs(interval_secs);
loop {
match github::get_pr_for_branch(&selector) {
Ok(Some(pr)) => match pr.state {
PrState::Open => {}
terminal => return terminal,
},
Ok(None) => output::warn("PR not found while polling — retrying"),
Err(e) => output::warn(&format!("Could not fetch PR status: {} — retrying", e)),
}
thread::sleep(delay);
}
}
fn notify(message: &str, verbose: bool) {
let cmd = match std::env::var("GW_NOTIFY_CMD") {
Ok(cmd) if !cmd.is_empty() => cmd,
_ => return,
};
if verbose {
output::action(&format!("{} {}", cmd, message));
}
if let Err(e) = Command::new(&cmd).arg(message).status() {
output::warn(&format!("Notify command '{}' failed: {}", cmd, e));
}
}