use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::OnceLock;
use std::time::Duration;
use tokio::io::AsyncReadExt;
const TEST_SEAM_ENV: &str = "OPENLATCH_TEST_GIT_BIN";
const SPAWN_TIMEOUT: Duration = Duration::from_secs(2);
const MAX_STDOUT_BYTES: u64 = 4096;
static GIT_BIN: OnceLock<Option<PathBuf>> = OnceLock::new();
pub async fn resolve(cwd: Option<&str>) -> Option<String> {
let cwd = cwd?;
let git = git_bin()?;
run_git(&git, cwd, &["rev-parse", "--git-dir"]).await?;
let email = run_git(&git, cwd, &["config", "--get", "user.email"]).await?;
let email = email.trim();
if email.is_empty() {
None
} else {
Some(email.to_owned())
}
}
fn git_bin() -> Option<PathBuf> {
if let Some(seam) = std::env::var_os(TEST_SEAM_ENV).filter(|v| !v.is_empty()) {
return Some(PathBuf::from(seam));
}
GIT_BIN.get_or_init(discover_platform).clone()
}
pub(super) fn discover_platform() -> Option<PathBuf> {
#[cfg(windows)]
if let Some(installed) = git_for_windows() {
return Some(installed);
}
let found = path_walk();
if found.is_none() {
tracing::debug!(
target: "identity",
reason = "no_git_binary",
"gitemail is unavailable for this daemon run"
);
}
found
}
fn path_walk() -> Option<PathBuf> {
let name = if cfg!(windows) { "git.exe" } else { "git" };
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path)
.filter(|dir| dir.is_absolute())
.map(|dir| dir.join(name))
.find(|candidate| candidate.is_file())
}
#[cfg(windows)]
fn git_for_windows() -> Option<PathBuf> {
use std::ffi::{OsStr, OsString};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
use windows_sys::Win32::System::Registry::{RegGetValueW, HKEY_LOCAL_MACHINE, RRF_RT_REG_SZ};
const MAX_VALUE_BYTES: u32 = 64 * 1024;
fn wide(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
let subkey = wide("SOFTWARE\\GitForWindows");
let value = wide("InstallPath");
let mut bytes: u32 = 0;
let rc = unsafe {
RegGetValueW(
HKEY_LOCAL_MACHINE,
subkey.as_ptr(),
value.as_ptr(),
RRF_RT_REG_SZ,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut bytes,
)
};
if rc != ERROR_SUCCESS || bytes == 0 || bytes > MAX_VALUE_BYTES {
return None;
}
let mut buf = vec![0u16; bytes as usize / 2 + 1];
let mut written = bytes;
let rc = unsafe {
RegGetValueW(
HKEY_LOCAL_MACHINE,
subkey.as_ptr(),
value.as_ptr(),
RRF_RT_REG_SZ,
std::ptr::null_mut(),
buf.as_mut_ptr().cast(),
&mut written,
)
};
if rc != ERROR_SUCCESS {
return None;
}
let len = (written as usize / 2).saturating_sub(1).min(buf.len());
let install = PathBuf::from(OsString::from_wide(&buf[..len]));
let candidate = install.join("cmd").join("git.exe");
candidate.is_file().then_some(candidate)
}
async fn run_git(git: &Path, cwd: &str, args: &[&str]) -> Option<String> {
let mut cmd = tokio::process::Command::new(git);
cmd.arg("-C")
.arg(cwd)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.kill_on_drop(true);
strip_git_env(&mut cmd);
let Ok(mut child) = cmd.spawn() else {
debug_absent("spawn_failed");
return None;
};
let Some(stdout) = child.stdout.take() else {
debug_absent("io_error");
return None;
};
let collect = async {
let mut buf = Vec::new();
{
let mut capped = stdout.take(MAX_STDOUT_BYTES);
capped.read_to_end(&mut buf).await?;
}
let status = child.wait().await?;
Ok::<_, std::io::Error>((status, buf))
};
let Ok(collected) = tokio::time::timeout(SPAWN_TIMEOUT, collect).await else {
debug_absent("timeout");
return None;
};
let Ok((status, stdout)) = collected else {
debug_absent("io_error");
return None;
};
if !status.success() {
debug_absent("nonzero_exit");
return None;
}
String::from_utf8(stdout).ok()
}
fn strip_git_env(cmd: &mut tokio::process::Command) {
cmd.env_remove("GIT_DIR").env_remove("GIT_WORK_TREE");
for (key, _) in std::env::vars_os() {
if key
.to_string_lossy()
.to_ascii_uppercase()
.starts_with("GIT_CONFIG")
{
cmd.env_remove(&key);
}
}
}
fn debug_absent(reason: &'static str) {
tracing::debug!(target: "identity", reason, "gitemail not resolved");
}
#[cfg(test)]
mod tests {
use super::*;
use crate::daemon::identity::test_support::{git_ok, write_shim, EnvGuard};
fn repo(git: &Path, email: Option<&str>) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
assert!(git_ok(git, dir.path(), &["init", "-q"]), "git init");
if let Some(email) = email {
assert!(
git_ok(git, dir.path(), &["config", "user.email", email]),
"git config user.email"
);
}
dir
}
fn global_email(git: &Path) -> Option<String> {
let out = std::process::Command::new(git)
.args(["config", "--global", "--get", "user.email"])
.stdin(Stdio::null())
.output()
.ok()?;
if !out.status.success() {
return None;
}
let value = String::from_utf8(out.stdout).ok()?.trim().to_owned();
(!value.is_empty()).then_some(value)
}
#[tokio::test]
async fn the_gate_decides_whether_there_is_an_email_at_all() {
let _lock = crate::daemon::identity::ENV_LOCK.lock().await;
let env = EnvGuard::clear();
let Some(git) = discover_platform() else {
return;
};
let local = repo(&git, Some("local@fixture.test"));
assert_eq!(
resolve(local.path().to_str()).await.as_deref(),
Some("local@fixture.test"),
"the repo-local address is the whole point of resolving per-cwd"
);
let no_local = repo(&git, None);
if let Some(global) = global_email(&git) {
assert_eq!(
resolve(no_local.path().to_str()).await.as_deref(),
Some(global.as_str()),
"with no repo-local value git falls back to the global one"
);
} else {
assert_eq!(
resolve(no_local.path().to_str()).await,
None,
"no local and no global address is an absent attribute"
);
}
let bare = tempfile::tempdir().expect("tempdir");
assert_eq!(resolve(bare.path().to_str()).await, None);
assert_eq!(resolve(None).await, None);
let vanished = bare.path().join("gone");
assert_eq!(resolve(vanished.to_str()).await, None);
env.set("GIT_TEST_ASSUME_DIFFERENT_OWNER", "1");
let gate_refuses = !git_ok(&git, local.path(), &["rev-parse", "--git-dir"]);
if gate_refuses {
assert_eq!(
resolve(local.path().to_str()).await,
None,
"a refused gate must suppress the email, silently"
);
}
env.unset("GIT_TEST_ASSUME_DIFFERENT_OWNER");
env.set("GIT_CONFIG_COUNT", "1");
env.set("GIT_CONFIG_KEY_0", "user.email");
env.set("GIT_CONFIG_VALUE_0", "evil@injected.test");
assert_eq!(
resolve(local.path().to_str()).await.as_deref(),
Some("local@fixture.test"),
"the child environment must not be able to dictate attribution"
);
}
#[tokio::test]
async fn a_hung_git_times_out_instead_of_hanging() {
let _lock = crate::daemon::identity::ENV_LOCK.lock().await;
let dir = tempfile::tempdir().expect("tempdir");
let shim = sleep_shim(dir.path());
let started = std::time::Instant::now();
let gated = run_git(
&shim,
dir.path().to_str().expect("utf-8 path"),
&["rev-parse", "--git-dir"],
)
.await;
let elapsed = started.elapsed();
assert_eq!(gated, None, "a timed-out spawn is an absent attribute");
assert!(
elapsed >= SPAWN_TIMEOUT,
"returning before the ceiling means the shim never ran — the timeout was not tested"
);
assert!(
elapsed < Duration::from_secs(5),
"the 2s ceiling must fire long before the child's own 5s sleep"
);
}
fn sleep_shim(dir: &Path) -> PathBuf {
#[cfg(windows)]
let (name, body) = ("slow-git.cmd", "@echo off\r\nping -n 6 127.0.0.1 >nul\r\n");
#[cfg(unix)]
let (name, body) = ("slow-git.sh", "#!/bin/sh\nsleep 5\n");
write_shim(dir, name, body)
}
#[test]
fn the_seam_outranks_the_platform_ladder() {
let _lock = crate::daemon::identity::ENV_LOCK.blocking_lock();
let env = EnvGuard::clear();
let seam = if cfg!(windows) {
"C:\\fixture\\seam-git.exe"
} else {
"/fixture/seam-git"
};
env.set(TEST_SEAM_ENV, seam);
assert_eq!(
git_bin(),
Some(PathBuf::from(seam)),
"the seam is trusted as given — it is not required to exist"
);
env.set(TEST_SEAM_ENV, "");
assert_ne!(git_bin(), Some(PathBuf::new()));
env.unset(TEST_SEAM_ENV);
if let Some(found) = discover_platform() {
assert!(found.is_file(), "the ladder only returns existing binaries");
}
}
}