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;
use crate::state::RepoType;
pub fn run(
open_browser: bool,
no_wait: bool,
no_cleanup: bool,
interval: u64,
verbose: bool,
) -> Result<()> {
if !git::is_git_repo() {
return Err(GwError::NotAGitRepository);
}
let repo_type = RepoType::detect()?;
let home_branch = repo_type.home_branch();
let current = git::current_branch()?;
println!();
if current == home_branch {
output::warn(&format!(
"On home branch '{}'. No PR to await.",
home_branch
));
output::hints(&["gw new feature/your-feature # Start a branch first"]);
return Ok(());
}
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(¤t)? {
Some(pr) => pr,
None => {
output::warn(&format!("No PR found for branch '{}'.", current));
output::hints(&["gh pr create -a \"@me\" -t \"...\" # Create a PR first"]);
return Ok(());
}
};
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(¤t, 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(¤t, 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(branch: &str, no_cleanup: bool, verbose: bool) -> Result<()> {
if no_cleanup {
output::ready("Merged", branch);
output::hints(&["gw cleanup # Delete the merged branch when ready"]);
return Ok(());
}
println!();
output::info("Cleaning up merged branch...");
cleanup::run(None, 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));
}
}