use std::error::Error;
use std::io::ErrorKind;
use std::path::Path;
use std::process::Command;
use serde::Deserialize;
pub const GH: &str = "gh";
#[derive(Debug, Deserialize)]
struct Commit {
oid: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PrInfo {
base_ref_oid: String,
head_ref_oid: String,
merge_commit: Option<Commit>,
}
#[derive(Debug, PartialEq)]
pub enum Plan {
Refs { since: String, until: String },
Merged { merge: String, before: String },
Patch { base: Option<String> },
}
pub fn plan(
info: &PrInfo,
is_local: impl Fn(&str) -> bool,
first_parent: impl Fn(&str) -> Option<String>,
) -> Plan {
let base = info.base_ref_oid.as_str();
if is_local(base) && is_local(&info.head_ref_oid) {
return Plan::Refs {
since: base.to_string(),
until: info.head_ref_oid.clone(),
};
}
let merged = info.merge_commit.as_ref().map(|c| c.oid.as_str());
match merged.and_then(|merge| Some((merge, first_parent(merge)?))) {
Some((merge, before)) => Plan::Merged {
merge: merge.to_string(),
before,
},
None => Plan::Patch {
base: is_local(base).then(|| base.to_string()),
},
}
}
fn run(program: &str, dir: &Path, args: &[&str]) -> Result<Vec<u8>, Box<dyn Error>> {
let output = Command::new(program)
.args(args)
.current_dir(dir)
.output()
.map_err(|e| match e.kind() {
ErrorKind::NotFound => format!(
"--pr requires the GitHub CLI (`{program}`) installed and authenticated: \
https://cli.github.com"
),
_ => format!("cannot run `{program}`: {e}"),
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("`{program} {}` failed: {}", args.join(" "), stderr.trim()).into());
}
Ok(output.stdout)
}
pub fn info(program: &str, dir: &Path, number: u64) -> Result<PrInfo, Box<dyn Error>> {
let number = number.to_string();
let fields = "baseRefOid,headRefOid,mergeCommit";
let json = run(program, dir, &["pr", "view", &number, "--json", fields])?;
Ok(serde_json::from_slice(&json)
.map_err(|e| format!("unexpected answer from `{program} pr view`: {e}"))?)
}
pub fn patch(program: &str, dir: &Path, number: u64) -> Result<Vec<u8>, Box<dyn Error>> {
run(
program,
dir,
&["pr", "diff", &number.to_string(), "--color=never"],
)
}
#[cfg(test)]
#[path = "pr_test.rs"]
mod tests;