use super::imp::{VcsRevision, cmd_label, do_run, do_run_bytes};
use anyhow::{Context, bail};
use camino::{Utf8Path, Utf8PathBuf};
use git_stub::GitCommitHash;
use std::process::Command;
pub(super) fn git_merge_base_head(
repo_root: &Utf8Path,
revision: &VcsRevision,
) -> anyhow::Result<GitCommitHash> {
if git_merge_head_exists(repo_root) {
let mb_head = git_merge_base(repo_root, "HEAD", revision)?;
let mb_merge_head = git_merge_base(repo_root, "MERGE_HEAD", revision)?;
if git_is_ancestor(repo_root, mb_head, mb_merge_head)? {
Ok(mb_merge_head)
} else {
Ok(mb_head)
}
} else {
git_merge_base(repo_root, "HEAD", revision)
}
}
fn git_merge_base(
repo_root: &Utf8Path,
base_ref: &str,
revision: &VcsRevision,
) -> anyhow::Result<GitCommitHash> {
let mut cmd = git_start(repo_root);
cmd.arg("merge-base").arg("--all").arg(base_ref).arg(revision.as_str());
let label = cmd_label(&cmd);
let stdout = do_run(&mut cmd)?;
let stdout = stdout.trim();
if stdout.contains(" ") || stdout.contains("\n") {
bail!(
"unexpected output from {} (contains whitespace -- \
multiple merge bases?)",
label
);
}
stdout.parse().with_context(|| {
format!("git merge-base returned invalid commit hash: {:?}", stdout)
})
}
pub(super) fn git_is_ancestor(
repo_root: &Utf8Path,
potential_ancestor: GitCommitHash,
commit: GitCommitHash,
) -> anyhow::Result<bool> {
let mut cmd = git_start(repo_root);
cmd.args([
"merge-base",
"--is-ancestor",
&potential_ancestor.to_string(),
&commit.to_string(),
]);
let output =
cmd.output().context("running git merge-base --is-ancestor")?;
match output.status.code() {
Some(0) => Ok(true),
Some(1) => Ok(false),
Some(code) => {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(anyhow::anyhow!(
"git merge-base --is-ancestor exited with unexpected \
code {code} (args: {} {}): {}",
potential_ancestor,
commit,
stderr.trim(),
))
}
None => {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(anyhow::anyhow!(
"git merge-base --is-ancestor terminated by signal \
(args: {} {}): {}",
potential_ancestor,
commit,
stderr.trim(),
))
}
}
}
fn git_merge_head_exists(repo_root: &Utf8Path) -> bool {
let mut cmd = git_start(repo_root);
cmd.args(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"]);
matches!(cmd.status(), Ok(status) if status.success())
}
pub(super) fn git_ls_tree(
repo_root: &Utf8Path,
revision: GitCommitHash,
directory: &Utf8Path,
) -> anyhow::Result<Vec<Utf8PathBuf>> {
let mut cmd = git_start(repo_root);
cmd.arg("ls-tree")
.arg("-r")
.arg("-z")
.arg("--name-only")
.arg("--full-tree")
.arg(revision.to_string())
.arg(directory);
let label = cmd_label(&cmd);
let stdout = do_run(&mut cmd)?;
stdout
.trim()
.split("\0")
.filter(|s| !s.is_empty())
.map(|path| {
let found_path = Utf8PathBuf::from(path);
let Ok(relative) = found_path.strip_prefix(directory) else {
bail!(
"git ls-tree unexpectedly returned a path that did not start \
with {:?}: {:?} (cmd: {})",
directory,
found_path,
label,
);
};
Ok(relative.to_owned())
})
.collect::<Result<Vec<_>, _>>()
}
pub(super) fn git_show_file(
repo_root: &Utf8Path,
revision: GitCommitHash,
path: &Utf8Path,
) -> anyhow::Result<Vec<u8>> {
let mut cmd = git_start(repo_root);
cmd.arg("cat-file").arg("blob").arg(format!("{}:{}", revision, path));
do_run_bytes(&mut cmd)
}
pub(super) fn git_first_commit_for_file(
repo_root: &Utf8Path,
revision: GitCommitHash,
path: &Utf8Path,
) -> anyhow::Result<GitCommitHash> {
let mut cmd = git_start(repo_root);
cmd.arg("log")
.arg("-m")
.arg("--diff-filter=A")
.arg("--format=%H")
.arg(revision.to_string())
.arg("--")
.arg(path);
let stdout = do_run(&mut cmd)?;
let commit = stdout.trim();
let first_commit = commit.lines().next().with_context(|| {
format!(
"no commit found that added file {:?} \
(searched backwards from {})",
path, revision,
)
})?;
first_commit.parse().with_context(|| {
format!(
"git returned invalid commit hash {:?} for {:?}",
first_commit, path
)
})
}
fn git_start(repo_root: &Utf8Path) -> Command {
let git = std::env::var("GIT").ok().unwrap_or_else(|| String::from("git"));
let mut command = Command::new(&git);
command.current_dir(repo_root);
command
}