use anyhow::{Context, Result, anyhow, bail};
use std::io::{self, ErrorKind, Write};
use std::path::Path;
use std::process::{Command, Stdio};
pub struct PullRequest<'a> {
pub title: &'a str,
pub body: &'a str,
pub base: &'a str,
pub head: &'a str,
pub draft: bool,
pub extra_args: &'a [String],
}
fn gh(dir: &Path, args: &[&str]) -> Command {
let mut command = Command::new("gh");
command.current_dir(dir).args(args);
command
}
fn start_error(err: io::Error) -> anyhow::Error {
if err.kind() == ErrorKind::NotFound {
anyhow!(
"git agent pr creates pull requests with the GitHub CLI, which is not installed; \
get it from https://cli.github.com, then run `gh auth login`"
)
} else {
anyhow::Error::new(err).context("failed to run gh")
}
}
fn output(dir: &Path, args: &[&str]) -> Result<String> {
let output = gh(dir, args).output().map_err(start_error)?;
if !output.status.success() {
bail!(
"`gh {}` failed: {}",
args[..2].join(" "),
String::from_utf8_lossy(&output.stderr).trim()
);
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
}
pub fn default_branch(dir: &Path) -> Result<String> {
let name = output(
dir,
&[
"repo",
"view",
"--json",
"defaultBranchRef",
"--jq",
".defaultBranchRef.name",
],
)
.context("could not ask GitHub for the default branch; name the base branch with --base")?;
if name.is_empty() {
bail!("GitHub reported no default branch; name the base branch with --base");
}
Ok(name)
}
pub fn open_pull_request(dir: &Path, branch: &str) -> Result<Option<String>> {
let url = output(
dir,
&[
"pr",
"list",
"--head",
branch,
"--state",
"open",
"--json",
"url",
"--jq",
".[0].url // empty",
],
)?;
Ok((!url.is_empty()).then_some(url))
}
pub fn create_pull_request(dir: &Path, pull_request: &PullRequest) -> Result<()> {
let mut command = gh(
dir,
&[
"pr",
"create",
"--title",
pull_request.title,
"--body-file",
"-",
"--base",
pull_request.base,
"--head",
pull_request.head,
],
);
if pull_request.draft {
command.arg("--draft");
}
let mut child = command
.args(pull_request.extra_args)
.stdin(Stdio::piped())
.spawn()
.map_err(start_error)?;
let written = child
.stdin
.take()
.context("gh has no input")?
.write_all(pull_request.body.as_bytes());
let status = child.wait().context("failed to wait for gh")?;
written.context("failed to send the description to gh")?;
if !status.success() {
bail!("`gh pr create` failed ({status})");
}
Ok(())
}