pub mod types;
use std::path::Path;
use std::process::Command;
use crate::error::{Error, Result};
pub use types::{Author, OpenPr, PrSummary, PrView, pr_state};
pub trait GhClient {
fn list_open_prs(&self, dir: &Path) -> Result<Vec<PrSummary>>;
fn view_pr(&self, dir: &Path, target: &str) -> Result<PrView>;
fn default_branch(&self, dir: &Path) -> Result<Option<String>>;
fn find_pr_for_branch(&self, dir: &Path, branch: &str) -> Result<Option<OpenPr>>;
fn create_pr(&self, dir: &Path, args: &[String]) -> Result<String>;
fn edit_pr(&self, dir: &Path, args: &[String]) -> Result<String>;
fn open_pr_numbers(&self, dir: &Path) -> Result<Vec<u64>> {
Ok(self
.list_open_prs(dir)?
.into_iter()
.map(|p| p.number)
.collect())
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RealGh;
impl GhClient for RealGh {
fn list_open_prs(&self, dir: &Path) -> Result<Vec<PrSummary>> {
let output = run_gh(
dir,
&[
"pr",
"list",
"--state",
"open",
"--json",
"number,title,author,state,isDraft,headRefName,createdAt",
],
)?;
serde_json::from_str(&output).map_err(Error::from)
}
fn view_pr(&self, dir: &Path, target: &str) -> Result<PrView> {
let output = run_gh(
dir,
&[
"pr",
"view",
target,
"--json",
"number,title,state,isDraft,headRefName,baseRefName,url",
],
)?;
serde_json::from_str(&output).map_err(Error::from)
}
fn default_branch(&self, dir: &Path) -> Result<Option<String>> {
match run_gh(dir, &["repo", "view", "--json", "defaultBranchRef"]) {
Ok(output) => Ok(types::parse_default_branch(&output)),
Err(_) => Ok(None),
}
}
fn find_pr_for_branch(&self, dir: &Path, branch: &str) -> Result<Option<OpenPr>> {
let output = run_gh(
dir,
&[
"pr",
"list",
"--head",
branch,
"--state",
"open",
"--json",
"number,url,state,isDraft",
],
)?;
let prs: Vec<OpenPr> = serde_json::from_str(&output).map_err(Error::from)?;
Ok(prs.into_iter().next())
}
fn create_pr(&self, dir: &Path, args: &[String]) -> Result<String> {
let argv: Vec<&str> = args.iter().map(String::as_str).collect();
run_gh(dir, &argv)
}
fn edit_pr(&self, dir: &Path, args: &[String]) -> Result<String> {
let argv: Vec<&str> = args.iter().map(String::as_str).collect();
run_gh(dir, &argv)
}
}
fn run_gh(dir: &Path, args: &[&str]) -> Result<String> {
let result = Command::new("gh").current_dir(dir).args(args).output();
let output = match result {
Ok(output) => output,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::GhUnavailable(
"gh is not installed; install it and run `gh auth login`".into(),
));
}
Err(e) => return Err(Error::GhUnavailable(format!("failed to run gh: {e}"))),
};
if output.status.success() {
return Ok(String::from_utf8_lossy(&output.stdout).into_owned());
}
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let lowered = stderr.to_ascii_lowercase();
if lowered.contains("auth")
|| lowered.contains("logged in")
|| lowered.contains("gh auth login")
{
Err(Error::GhUnavailable(format!(
"{stderr}\nrun `gh auth login`"
)))
} else {
Err(Error::Subprocess {
program: "gh".into(),
stderr,
})
}
}