use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use eyre::eyre;
use crate::Result;
pub fn git_version() -> Option<(u32, u32, u32)> {
static CACHED: OnceLock<Option<(u32, u32, u32)>> = OnceLock::new();
*CACHED.get_or_init(|| {
let output = std::process::Command::new("git")
.arg("--version")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let s = String::from_utf8_lossy(&output.stdout);
let ver = s.split_whitespace().nth(2)?;
let mut parts = ver.split('.').filter_map(|p| {
let digits: String = p.chars().take_while(|c| c.is_ascii_digit()).collect();
digits.parse::<u32>().ok()
});
Some((
parts.next()?,
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
))
})
}
pub fn git_at_least(major: u32, minor: u32) -> bool {
match git_version() {
Some((maj, min, _)) => (maj, min) >= (major, minor),
None => false,
}
}
pub fn find_git_path() -> Result<PathBuf> {
if let Some(git_dir) = std::env::var_os("GIT_DIR") {
let p = PathBuf::from(&git_dir);
let p = if p.is_absolute() {
p
} else {
std::env::current_dir()?.join(p)
};
return Ok(p);
}
let cwd = std::env::current_dir()?;
xx::file::find_up(&cwd, &[".git"])
.ok_or_else(|| eyre!("No .git found in this or any parent directory"))
}
pub fn find_work_tree_root() -> PathBuf {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
if let Some(wt) = std::env::var_os("GIT_WORK_TREE") {
let p = PathBuf::from(&wt);
return if p.is_absolute() { p } else { cwd.join(p) };
}
xx::file::find_up(&cwd, &[".git"])
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.unwrap_or(cwd)
}
pub fn resolve_git_dir(git_path: &Path) -> Result<PathBuf> {
if git_path.is_dir() {
return Ok(git_path.to_path_buf());
}
let content = std::fs::read_to_string(git_path)
.map_err(|e| eyre!("failed to read {}: {e}", git_path.display()))?;
let gitdir = content
.strip_prefix("gitdir: ")
.map(|s| s.trim())
.ok_or_else(|| eyre!("unexpected .git file format in {}", git_path.display()))?;
let gitdir_path = PathBuf::from(gitdir);
let resolved = if gitdir_path.is_absolute() {
gitdir_path
} else {
git_path
.parent()
.ok_or_else(|| {
eyre!(
"could not determine parent directory of .git file: {}",
git_path.display()
)
})?
.join(&gitdir_path)
};
Ok(resolved)
}
fn resolve_common_git_dir(git_dir: &Path) -> Result<PathBuf> {
let commondir_file = git_dir.join("commondir");
if commondir_file.is_file() {
let content = std::fs::read_to_string(&commondir_file)
.map_err(|e| eyre!("failed to read {}: {e}", commondir_file.display()))?;
let rel = content.trim();
let resolved = git_dir.join(rel);
Ok(std::fs::canonicalize(&resolved).unwrap_or(resolved))
} else {
Ok(git_dir.to_path_buf())
}
}
pub fn resolve_git_relative_path(path: &Path) -> Result<PathBuf> {
if path.exists() {
return Ok(path.to_path_buf());
}
if let Ok(rest) = path.strip_prefix(".git") {
let git_path = find_git_path()?;
let git_dir = resolve_git_dir(&git_path)?;
let resolved = git_dir.join(rest);
if resolved.exists() {
return Ok(resolved);
}
}
Ok(path.to_path_buf())
}
pub fn resolve_git_hooks_dir(git_path: &Path) -> Result<PathBuf> {
let git_dir = resolve_git_dir(git_path)?;
let common_dir = resolve_common_git_dir(&git_dir)?;
Ok(common_dir.join("hooks"))
}
pub fn worktree_hooks_path() -> Option<PathBuf> {
let wt_config = std::process::Command::new("git")
.args([
"config",
"--type=bool",
"--get",
"extensions.worktreeConfig",
])
.output()
.ok()?;
if !wt_config.status.success() || String::from_utf8_lossy(&wt_config.stdout).trim() != "true" {
return None;
}
let output = std::process::Command::new("git")
.args(["config", "--worktree", "--get", "core.hooksPath"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if path.is_empty() {
None
} else {
Some(PathBuf::from(path))
}
}