Menu
AkurAI-Build
publicLatest change 266a1aa21e346770b48e96f008ab6c2a5a603961 - Complete PR interfaces, isolated CI and crash-safe merge recovery by Ólafur Búi Ólafsson
use std::{path::Path, process::Command};
use anyhow::{Context, Result, ensure};
use serde::Serialize;
pub(crate) fn command(cwd: &Path) -> Command {
let mut command = Command::new("git");
command
.current_dir(cwd)
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_CONFIG_NOSYSTEM", "1");
command
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub(crate) struct RemoteBranch {
pub name: String,
pub sha: String,
pub is_default: bool,
}
pub(crate) fn remote_branches(url: &str, default_branch: &str) -> Result<Vec<RemoteBranch>> {
let output = Command::new("git")
.args(["ls-remote", "--heads", url])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.context("failed to list branches")?;
ensure!(
output.status.success(),
"git ls-remote failed: {}",
String::from_utf8_lossy(&output.stderr)
.lines()
.next()
.unwrap_or("unknown error")
);
let mut branches: Vec<RemoteBranch> = String::from_utf8(output.stdout)
.context("invalid git output")?
.lines()
.filter_map(|line| line.split_once('\t'))
.filter_map(|(sha, reference)| {
reference
.strip_prefix("refs/heads/")
.map(|name| RemoteBranch {
name: name.to_owned(),
sha: sha.to_owned(),
is_default: name == default_branch,
})
})
.collect();
branches.sort_by(|left, right| left.name.cmp(&right.name));
Ok(branches)
}
pub(crate) fn output<const N: usize>(cwd: &Path, arguments: [&str; N]) -> Result<String> {
let output = command(cwd).args(arguments).output()?;
ensure!(
output.status.success(),
"git failed: {}",
String::from_utf8_lossy(&output.stderr)
.chars()
.take(16 * 1024)
.collect::<String>()
.trim()
);
Ok(String::from_utf8_lossy(&output.stdout)
.trim_end()
.to_owned())
}
/// Run git and return raw stdout bytes: no lossy UTF-8 substitution and no
/// trimming, so binary blobs (images, etc.) survive intact.
pub(crate) fn output_bytes<const N: usize>(cwd: &Path, arguments: [&str; N]) -> Result<Vec<u8>> {
let output = command(cwd).args(arguments).output()?;
ensure!(
output.status.success(),
"git failed: {}",
String::from_utf8_lossy(&output.stderr)
.chars()
.take(16 * 1024)
.collect::<String>()
.trim()
);
Ok(output.stdout)
}
#[derive(Serialize)]
pub(crate) struct DiffPreview {
pub patch: String,
pub truncated: bool,
}
pub(crate) fn diff_preview(cwd: &Path, base: &str, head: &str) -> Result<DiffPreview> {
use std::io::Read;
use std::process::Stdio;
crate::config::validate_commit(base)?;
crate::config::validate_commit(head)?;
const LIMIT: usize = 512 * 1024;
let mut child = command(cwd)
.args([
"diff",
"--no-ext-diff",
"--no-textconv",
"--no-color",
"--unified=3",
&format!("{base}...{head}"),
"--",
])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
let mut bytes = Vec::new();
let read = child
.stdout
.take()
.context("missing Git output")?
.take((LIMIT + 1) as u64)
.read_to_end(&mut bytes);
let truncated = bytes.len() > LIMIT;
if truncated || read.is_err() {
let _ = child.kill();
}
let status = child.wait()?;
read?;
ensure!(
truncated || status.success(),
"diff unavailable for these commits"
);
bytes.truncate(LIMIT);
Ok(DiffPreview {
patch: String::from_utf8_lossy(&bytes).into_owned(),
truncated,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn reads_git_version_successfully() {
let dir = tempdir().expect("create temp dir");
let version = output(dir.path(), ["version"]).expect("git version should succeed");
assert!(
version.starts_with("git version"),
"should start with 'git version'"
);
}
#[test]
fn errors_on_bad_arguments() {
let dir = tempdir().expect("create temp dir");
let result = output(dir.path(), ["--invalid-flag-that-does-not-exist"]);
assert!(result.is_err(), "bad arguments should error, not panic");
}
#[test]
fn errors_on_non_repository() {
let dir = tempdir().expect("create temp dir");
// TMPDIR may live inside a worktree; prevent discovering its parent repo.
let result = output(dir.path(), ["--git-dir=missing.git", "rev-parse", "HEAD"]);
assert!(result.is_err(), "non-repo should produce Err");
}
#[test]
fn reads_commit_sha_in_valid_repo() -> Result<()> {
let dir = tempdir()?;
let repo = dir.path().join("repo");
fs::create_dir(&repo)?;
let init = std::process::Command::new("git")
.arg("init")
.args(["-b", "main"])
.arg(&repo)
.output()?;
ensure!(init.status.success(), "git init failed");
let commit = std::process::Command::new("git")
.arg("-C")
.arg(&repo)
.args([
"-c",
"user.name=test",
"-c",
"user.email=test@test",
"commit",
"--allow-empty",
"-m",
"init",
])
.output()?;
ensure!(commit.status.success(), "git commit failed");
let sha = output(repo.as_path(), ["rev-parse", "HEAD"])?;
assert_eq!(sha.len(), 40, "SHA should be 40 hex chars");
assert!(
sha.chars().all(|c| c.is_ascii_hexdigit()),
"SHA should be hex"
);
Ok(())
}
#[test]
fn lists_sorted_remote_branches_and_marks_default() -> Result<()> {
let dir = tempdir()?;
let repo = dir.path().join("repo");
fs::create_dir(&repo)?;
ensure!(
Command::new("git")
.arg("init")
.args(["-b", "main"])
.arg(&repo)
.status()?
.success(),
"git init failed"
);
ensure!(
Command::new("git")
.arg("-C")
.arg(&repo)
.args([
"-c",
"user.name=test",
"-c",
"user.email=test@test",
"commit",
"--allow-empty",
"-m",
"init",
])
.status()?
.success(),
"git commit failed"
);
ensure!(
Command::new("git")
.arg("-C")
.arg(&repo)
.args(["branch", "feature"])
.status()?
.success(),
"git branch failed"
);
let sha = output(&repo, ["rev-parse", "HEAD"])?;
let branches = remote_branches(repo.to_str().context("utf-8 path")?, "main")?;
assert_eq!(
branches,
vec![
RemoteBranch {
name: "feature".into(),
sha: sha.clone(),
is_default: false,
},
RemoteBranch {
name: "main".into(),
sha,
is_default: true,
},
]
);
Ok(())
}
}