use std::process::Command;
const DIFF_EXCLUDE: &[&str] = &[":(exclude)*.lock", ":(exclude)package-lock.json"];
pub(crate) fn branch_base_ref(wt_path: &str) -> Option<String> {
if let Some(name) = git_line(wt_path, &["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])
&& ref_exists(wt_path, &name)
{
return Some(name);
}
["main", "master", "origin/main", "origin/master"]
.into_iter()
.find(|name| ref_exists(wt_path, name))
.map(str::to_string)
}
pub(super) fn merge_base(wt_path: &str, base_ref: &str) -> Option<String> {
git_line(wt_path, &["merge-base", base_ref, "HEAD"])
}
pub(super) fn branch_diff_signal(
wt_path: &str,
start_sha: Option<&str>,
base_ref: Option<&str>,
) -> Option<String> {
let start = start_sha?;
let base_sha = merge_base(wt_path, base_ref?)?;
let start_sha_full = git_line(wt_path, &["rev-parse", start])?;
if base_sha == start_sha_full {
return None;
}
let totals = git_line(wt_path, &["diff", "--shortstat", &format!("{base_sha}..HEAD")])
.unwrap_or_default();
Some(format!(
" (This branch carries earlier commits that are not in the diff above. Whole branch: {totals}. Run `aid show <task-id> --diff --branch` to see all of it)\n"
))
}
pub(super) fn range_arg_sets(
start_sha: Option<&str>,
base_ref: Option<&str>,
stat: bool,
) -> Vec<Vec<String>> {
let mut sets: Vec<Vec<String>> = Vec::with_capacity(3);
for range in [
start_sha.map(|sha| format!("{sha}..HEAD")),
base_ref.map(|base| format!("{base}...HEAD")),
None,
] {
let mut args = vec!["diff".to_string()];
args.extend(range);
if stat {
args.push("--stat".to_string());
}
sets.push(args);
}
sets
}
pub(super) fn generate_diff(wt_path: &str, args_sets: &[Vec<String>], fallback: &str) -> String {
for args in args_sets {
if let Some(output) = run_git_diff(wt_path, &diff_args(args))
&& !output.trim().is_empty()
{
return output;
}
}
fallback.to_string()
}
pub(super) fn generate_diff_file(
wt_path: &str,
args_sets: &[Vec<String>],
fallback: &str,
file: &str,
) -> String {
for args in args_sets {
if let Some(output) = run_git_diff(wt_path, &diff_args_file(args, file))
&& !output.trim().is_empty()
{
return output;
}
}
fallback.to_string()
}
pub(super) fn head_matches_start(wt_path: &str, start_sha: &str) -> bool {
git_line(wt_path, &["rev-parse", "HEAD"]).is_some_and(|head| head == start_sha)
}
fn diff_args(base_args: &[String]) -> Vec<String> {
let mut args = base_args.to_vec();
args.push("--".to_string());
args.push(".".to_string());
args.extend(DIFF_EXCLUDE.iter().map(|value| value.to_string()));
args
}
fn diff_args_file(base_args: &[String], file: &str) -> Vec<String> {
let mut args = base_args.to_vec();
args.push("--".to_string());
args.push(file.to_string());
args
}
fn run_git_diff(wt_path: &str, args: &[String]) -> Option<String> {
let output = Command::new("git").arg("-C").arg(wt_path).args(args).output().ok()?;
if !output.status.success() {
return None;
}
Some(String::from_utf8_lossy(&output.stdout).into())
}
fn ref_exists(wt_path: &str, name: &str) -> bool {
Command::new("git")
.args(["-C", wt_path, "rev-parse", "--verify", "--quiet"])
.arg(format!("{name}^{{commit}}"))
.output()
.is_ok_and(|output| output.status.success())
}
fn git_line(wt_path: &str, args: &[&str]) -> Option<String> {
let output = Command::new("git").args(["-C", wt_path]).args(args).output().ok()?;
if !output.status.success() {
return None;
}
let line = String::from_utf8_lossy(&output.stdout).trim().to_string();
(!line.is_empty()).then_some(line)
}