use super::{GitTreeError, StepCommit};
use std::path::Path;
use std::process::Command;
const INHERITED_GIT_ENV: &[&str] = &[
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_OBJECT_DIRECTORY",
"GIT_PREFIX",
"GIT_COMMON_DIR",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
];
fn base_cmd(repo: &Path, args: &[&str]) -> Command {
let mut cmd = Command::new("git");
for var in INHERITED_GIT_ENV {
cmd.env_remove(var);
}
cmd.arg("-C").arg(repo).args(args);
cmd
}
pub(super) fn git(repo: &Path, args: &[&str]) -> Result<Vec<u8>, GitTreeError> {
let output = spawn_output(base_cmd(repo, args))?;
if !output.status.success() {
return Err(GitTreeError::Git {
command: args.join(" "),
repo: repo.to_path_buf(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
});
}
Ok(output.stdout)
}
fn git_optional(repo: &Path, args: &[&str]) -> Result<Option<Vec<u8>>, GitTreeError> {
let output = spawn_output(base_cmd(repo, args))?;
Ok(output.status.success().then_some(output.stdout))
}
#[cfg(not(test))]
fn spawn_output(mut cmd: Command) -> std::io::Result<std::process::Output> {
cmd.output()
}
#[cfg(test)]
fn spawn_output(mut cmd: Command) -> std::io::Result<std::process::Output> {
cmd.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
crate::test_support::spawn_locked(&mut cmd)?.wait_with_output()
}
#[derive(Debug)]
pub(super) struct LogEntry {
pub(super) oid: String,
pub(super) timestamp: i64,
pub(super) subject: String,
}
pub(super) fn git_log_first_parent(repo: &Path) -> Result<Vec<LogEntry>, GitTreeError> {
let out = git(
repo,
&[
"log",
"--first-parent",
"--format=%H %ct%x00%s",
"--reverse",
"HEAD",
],
)?;
parse_log(&out)
}
pub(super) fn parse_log(stdout: &[u8]) -> Result<Vec<LogEntry>, GitTreeError> {
let text = String::from_utf8_lossy(stdout);
let mut result = Vec::new();
for line in text.lines() {
let (head, subject) = line
.split_once('\x00')
.ok_or_else(|| GitTreeError::LogFormat(line.to_string()))?;
let mut parts = head.splitn(2, ' ');
let oid = parts
.next()
.ok_or_else(|| GitTreeError::LogFormat(line.to_string()))?
.to_string();
let ts_str = parts
.next()
.ok_or_else(|| GitTreeError::LogFormat(line.to_string()))?;
let ts: i64 = ts_str
.parse()
.map_err(|_| GitTreeError::LogFormat(line.to_string()))?;
result.push(LogEntry {
oid,
timestamp: ts,
subject: subject.to_string(),
});
}
Ok(result)
}
pub(super) fn walk_branch_steps(
repo: &Path,
branch: &str,
) -> Result<Vec<StepCommit>, GitTreeError> {
let out = git(
repo,
&[
"log",
"--reverse",
"--first-parent",
"--format=%H %ct%x00%s",
branch,
"--not",
"--branches=config/*",
],
)?;
parse_step_commits(&out)
}
pub(super) fn parse_step_commits(stdout: &[u8]) -> Result<Vec<StepCommit>, GitTreeError> {
let text = String::from_utf8_lossy(stdout);
let mut result = Vec::new();
for line in text.lines() {
let (head, subject) = line
.split_once('\x00')
.ok_or_else(|| GitTreeError::LogFormat(line.to_string()))?;
let (oid, ts) = head
.split_once(' ')
.ok_or_else(|| GitTreeError::LogFormat(line.to_string()))?;
let ts: i64 = ts
.parse()
.map_err(|_| GitTreeError::LogFormat(line.to_string()))?;
let short_oid = oid.get(..8).unwrap_or(oid).to_string();
result.push(StepCommit {
oid: oid.to_string(),
short_oid,
timestamp_unix: ts,
subject: subject.to_string(),
});
}
Ok(result)
}
pub(super) fn for_each_ref_agents(repo: &Path) -> Result<Vec<u8>, GitTreeError> {
git(
repo,
&[
"for-each-ref",
"--format=%(refname:short) %(objectname) %(committerdate:unix)",
"refs/heads/agents/",
],
)
}
pub(super) fn for_each_ref_under(repo: &Path, prefix: &str) -> Result<Vec<u8>, GitTreeError> {
git(
repo,
&["for-each-ref", "--format=%(refname) %(objectname)", prefix],
)
}
pub(crate) fn for_each_ref_config(repo: &Path) -> Result<Vec<u8>, GitTreeError> {
git(
repo,
&[
"for-each-ref",
"--format=%(refname:short) %(objectname) %(committerdate:unix)",
"refs/heads/config/",
],
)
}
pub(crate) fn ls_tree(repo: &Path, refspec: &str) -> Result<Vec<u8>, GitTreeError> {
git(repo, &["ls-tree", "-r", "--name-only", refspec])
}
pub(crate) fn show_file(repo: &Path, refspec: &str, path: &str) -> Result<Vec<u8>, GitTreeError> {
git(repo, &["show", &format!("{refspec}:{path}")])
}
pub(crate) fn merge_base(repo: &Path, a: &str, b: &str) -> Result<Option<String>, GitTreeError> {
Ok(git_optional(repo, &["merge-base", a, b])?
.map(|out| String::from_utf8_lossy(&out).trim().to_string()))
}
pub(crate) fn is_ancestor(repo: &Path, a: &str, b: &str) -> Result<bool, GitTreeError> {
Ok(git_optional(repo, &["merge-base", "--is-ancestor", a, b])?.is_some())
}