use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Duration;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use super::GitError;
const GIT_TIMEOUT: Duration = Duration::from_secs(60);
pub(crate) async fn git_query(root: &Path, args: &[&str]) -> Result<Option<String>, GitError> {
match run_git(root, args).await {
Ok(stdout) => Ok(Some(stdout)),
Err(GitError::NonZero { code: Some(1), .. }) => Ok(None),
Err(err) => Err(err),
}
}
pub(crate) async fn run_git(root: &Path, args: &[&str]) -> Result<String, GitError> {
spawn_git(root, args, GitEnv::Scrubbed).await
}
#[derive(Clone, Copy)]
pub(super) enum GitEnv<'a> {
Scrubbed,
Index(&'a Path),
Inherited,
}
pub(super) async fn committing_index(root: &Path) -> Result<Option<PathBuf>, GitError> {
let Some(inherited) = std::env::var_os("GIT_INDEX_FILE") else {
return Ok(None);
};
let (ours, hooks) = tokio::join!(
spawn_git(root, &["rev-parse", "--absolute-git-dir"], GitEnv::Scrubbed),
spawn_git(
Path::new("."),
&["rev-parse", "--absolute-git-dir"],
GitEnv::Inherited
),
);
let (Ok(ours), Ok(hooks)) = (ours, hooks) else {
return Ok(None);
};
owned_index(&inherited, Path::new(&hooks), Path::new(&ours))
}
pub(super) fn owned_index(
index: &OsStr,
hook_git_dir: &Path,
git_dir: &Path,
) -> Result<Option<PathBuf>, GitError> {
if !same_directory(hook_git_dir, git_dir) {
return Ok(None);
}
std::path::absolute(index).map(Some).map_err(|err| {
GitError::Spawn(format!(
"cannot resolve the index this commit is made from ({}): {err}",
Path::new(index).display()
))
})
}
pub(crate) fn same_directory(left: &Path, right: &Path) -> bool {
left == right
|| std::fs::canonicalize(left)
.and_then(|left| std::fs::canonicalize(right).map(|right| left == right))
.unwrap_or(false)
}
pub(super) async fn spawn_git(
root: &Path,
args: &[&str],
env: GitEnv<'_>,
) -> Result<String, GitError> {
let stdout = spawn_git_bytes(root, args, env).await?;
Ok(String::from_utf8_lossy(&stdout).trim().to_owned())
}
pub(super) async fn spawn_git_bytes<S: AsRef<OsStr>>(
root: &Path,
args: &[S],
env: GitEnv<'_>,
) -> Result<Vec<u8>, GitError> {
spawn_git_io(root, args, env, None).await
}
pub(super) async fn spawn_git_bytes_with_input<S: AsRef<OsStr>>(
root: &Path,
args: &[S],
env: GitEnv<'_>,
input: &[u8],
) -> Result<Vec<u8>, GitError> {
spawn_git_io(root, args, env, Some(input)).await
}
async fn spawn_git_io<S: AsRef<OsStr>>(
root: &Path,
args: &[S],
env: GitEnv<'_>,
input: Option<&[u8]>,
) -> Result<Vec<u8>, GitError> {
let mut command = Command::new("git");
command
.args(args)
.current_dir(root)
.stdin(if input.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
if !matches!(env, GitEnv::Inherited) {
command
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_COMMON_DIR")
.env_remove("GIT_INDEX_FILE")
.env_remove("GIT_OBJECT_DIRECTORY")
.env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
.env_remove("GIT_QUARANTINE_PATH");
}
if let GitEnv::Index(index) = env {
command.env("GIT_INDEX_FILE", index);
}
let run = async {
let mut child = command.spawn()?;
let stdin = child.stdin.take();
let feed = async {
if let (Some(mut stdin), Some(input)) = (stdin, input) {
let _ = stdin.write_all(input).await;
}
};
let ((), output) = tokio::join!(feed, child.wait_with_output());
output
};
let output = match tokio::time::timeout(GIT_TIMEOUT, run).await {
Ok(result) => result,
Err(_) => {
return Err(GitError::Spawn(format!(
"git {} timed out after {}s",
args.iter()
.map(|arg| arg.as_ref().to_string_lossy())
.collect::<Vec<_>>()
.join(" "),
GIT_TIMEOUT.as_secs()
)));
}
}
.map_err(|err| GitError::Spawn(err.to_string()))?;
if !output.status.success() {
return Err(GitError::NonZero {
code: output.status.code(),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_owned(),
});
}
Ok(output.stdout)
}