use std::path::Path;
use std::time::Duration;
use procpilot::{Cmd, RetryPolicy, RunError, RunOutput};
pub fn run_jj(repo_path: &Path, args: &[&str]) -> Result<RunOutput, RunError> {
Cmd::new("jj").in_dir(repo_path).args(args).run()
}
pub fn run_git(repo_path: &Path, args: &[&str]) -> Result<RunOutput, RunError> {
Cmd::new("git").in_dir(repo_path).args(args).run()
}
pub fn run_jj_with_timeout(
repo_path: &Path,
args: &[&str],
timeout: Duration,
) -> Result<RunOutput, RunError> {
Cmd::new("jj")
.in_dir(repo_path)
.args(args)
.timeout(timeout)
.run()
}
pub fn run_git_with_timeout(
repo_path: &Path,
args: &[&str],
timeout: Duration,
) -> Result<RunOutput, RunError> {
Cmd::new("git")
.in_dir(repo_path)
.args(args)
.timeout(timeout)
.run()
}
pub fn run_jj_with_retry(
repo_path: &Path,
args: &[&str],
is_transient: impl Fn(&RunError) -> bool + Send + Sync + 'static,
) -> Result<RunOutput, RunError> {
Cmd::new("jj")
.in_dir(repo_path)
.args(args)
.retry(RetryPolicy::default().when(is_transient))
.run()
}
pub fn run_git_with_retry(
repo_path: &Path,
args: &[&str],
is_transient: impl Fn(&RunError) -> bool + Send + Sync + 'static,
) -> Result<RunOutput, RunError> {
Cmd::new("git")
.in_dir(repo_path)
.args(args)
.retry(RetryPolicy::default().when(is_transient))
.run()
}
pub fn jj_merge_base(
repo_path: &Path,
a: &str,
b: &str,
) -> Result<Option<String>, RunError> {
let revset = format!("latest(::({a}) & ::({b}))");
let output = run_jj(
repo_path,
&[
"log", "-r", &revset, "--no-graph", "--limit", "1", "-T", "commit_id",
],
)?;
let id = output.stdout_lossy().trim().to_string();
Ok(if id.is_empty() { None } else { Some(id) })
}
pub fn git_merge_base(
repo_path: &Path,
a: &str,
b: &str,
) -> Result<Option<String>, RunError> {
match run_git(repo_path, &["merge-base", a, b]) {
Ok(output) => {
let id = output.stdout_lossy().trim().to_string();
Ok(if id.is_empty() { None } else { Some(id) })
}
Err(RunError::NonZeroExit { status, .. }) if status.code() == Some(1) => Ok(None),
Err(e) => Err(e),
}
}
pub fn is_transient_error(err: &RunError) -> bool {
procpilot::default_transient(err)
}
#[cfg(test)]
mod tests {
use super::*;
fn jj_installed() -> bool {
procpilot::binary_available("jj")
}
fn git_installed() -> bool {
procpilot::binary_available("git")
}
#[test]
fn is_transient_matches_stale_and_lock() {
#[cfg(unix)]
let status = {
use std::os::unix::process::ExitStatusExt;
std::process::ExitStatus::from_raw(256)
};
#[cfg(windows)]
let status = {
use std::os::windows::process::ExitStatusExt;
std::process::ExitStatus::from_raw(1)
};
let err = RunError::NonZeroExit {
command: Cmd::new("jj").display(),
status,
stdout: vec![],
stderr: "The working copy is stale".into(),
};
assert!(is_transient_error(&err));
}
#[test]
fn run_jj_fails_gracefully_when_not_installed() {
if jj_installed() {
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let err = run_jj(tmp.path(), &["status"]).expect_err("jj not installed");
assert!(err.is_spawn_failure());
}
#[test]
fn git_merge_base_returns_none_for_unrelated() {
if !git_installed() {
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
std::process::Command::new("git")
.args(["init", "--quiet"])
.current_dir(tmp.path())
.status()
.expect("git init");
let _ = git_merge_base(tmp.path(), "HEAD", "HEAD");
}
}