use super::{GitTreeError, StepCommit};
use std::path::Path;
use std::process::Command;
fn base_cmd(repo: &Path, args: &[&str]) -> Command {
let mut cmd = crate::git_env::git();
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)
}
pub(super) 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))
}
fn spawn_output(mut cmd: Command) -> std::io::Result<std::process::Output> {
crate::git_env::output(&mut cmd)
}
#[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 ref_name(repo: &Path, branch: &str) -> Result<Option<String>, GitTreeError> {
Ok(git_optional(repo, &["show", &format!("{branch}:name")])?
.map(|out| String::from_utf8_lossy(&out).trim().to_string())
.filter(|name| !name.is_empty()))
}
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(super) fn blob(repo: &Path, oid: &str) -> Result<Vec<u8>, GitTreeError> {
git(repo, &["cat-file", "blob", oid])
}
pub(super) mod browse;