use anyhow::{Context, Result, anyhow};
use std::path::{Path, PathBuf};
#[cfg(test)]
use std::process::Command;
use crate::cmd::Cmd;
pub fn is_path_ignored(repo_path: &Path, file_path: &str) -> bool {
super::unattended_git(Some(repo_path))
.and_then(|mut command| {
command
.args(["check-ignore", "-q", file_path])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
command.status().map_err(anyhow::Error::from)
})
.map(|status| status.success())
.unwrap_or(false)
}
pub fn is_git_repo() -> Result<bool> {
is_git_repo_in(None)
}
pub fn is_git_repo_in(workdir: Option<&Path>) -> Result<bool> {
let cmd = Cmd::new("git").args(&["rev-parse", "--git-dir"]);
let cmd = match workdir {
Some(path) => cmd.workdir(path),
None => cmd,
};
cmd.run_as_check()
}
pub fn get_repo_root_if_present() -> Result<Option<PathBuf>> {
get_repo_root_if_present_in(None)
}
pub fn get_repo_root_if_present_in(workdir: Option<&Path>) -> Result<Option<PathBuf>> {
repository_path_if_present(workdir, "--git-dir")
}
pub(crate) fn worktree_root_if_present(path: &Path) -> Result<Option<PathBuf>> {
repository_path_if_present(Some(path), "--show-toplevel")
}
fn repository_path_if_present(workdir: Option<&Path>, argument: &str) -> Result<Option<PathBuf>> {
let cwd = match workdir {
Some(path) => path
.canonicalize()
.context("Failed to resolve repository path")?,
None => std::env::current_dir().context("Failed to resolve current directory")?,
};
let output = super::unattended_git(Some(&cwd))?
.args(["rev-parse", argument])
.output()
.context("Failed to execute git rev-parse")?;
if output.status.success() {
let git_dir = PathBuf::from(String::from_utf8(output.stdout)?.trim());
return Ok(Some(if git_dir.is_absolute() {
git_dir
} else {
cwd.join(git_dir)
}));
}
let mut current = Some(cwd.as_path());
while let Some(path) = current {
match std::fs::symlink_metadata(path.join(".git")) {
Ok(_) => {
return Err(anyhow!(
"Git metadata exists but repository resolution failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
));
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error).context("Failed to inspect Git metadata"),
}
current = path.parent();
}
Ok(None)
}
#[allow(dead_code)]
pub fn has_commits() -> Result<bool> {
has_commits_in(None)
}
pub fn has_commits_in(workdir: Option<&Path>) -> Result<bool> {
let cmd = Cmd::new("git").args(&["rev-parse", "--verify", "--quiet", "HEAD"]);
let cmd = match workdir {
Some(path) => cmd.workdir(path),
None => cmd,
};
cmd.run_as_check()
}
pub fn get_repo_root() -> Result<PathBuf> {
get_repo_root_in(None)
}
pub fn get_repo_root_in(workdir: Option<&Path>) -> Result<PathBuf> {
let cmd = Cmd::new("git").args(&["rev-parse", "--show-toplevel"]);
let cmd = match workdir {
Some(path) => cmd.workdir(path),
None => cmd,
};
let path = cmd.run_and_capture_stdout()?;
Ok(PathBuf::from(path))
}
pub fn get_repo_root_for(dir: &Path) -> Result<PathBuf> {
let mut command = super::unattended_git(Some(dir))?;
let output = command
.args(["-C", &dir.to_string_lossy(), "rev-parse", "--show-toplevel"])
.output()
.context("Failed to run git rev-parse")?;
if !output.status.success() {
anyhow::bail!("Not a git repository: {}", dir.display());
}
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(PathBuf::from(path))
}
#[allow(dead_code)]
pub fn get_git_common_dir() -> Result<PathBuf> {
get_git_common_dir_in(None)
}
pub fn get_git_common_dir_in(workdir: Option<&Path>) -> Result<PathBuf> {
let cmd = Cmd::new("git").args(&["rev-parse", "--git-common-dir"]);
let cmd = match workdir {
Some(path) => cmd.workdir(path),
None => cmd,
};
let raw = cmd
.run_and_capture_stdout()
.context("Failed to get git common directory")?;
if raw.is_empty() {
return Err(anyhow!(
"git rev-parse --git-common-dir returned empty output"
));
}
let path = PathBuf::from(raw);
let abs_path = if path.is_relative() {
let base = match workdir {
Some(path) => path.to_path_buf(),
None => std::env::current_dir().context("Failed to get current directory")?,
};
base.join(path)
} else {
path
};
Ok(abs_path)
}
pub fn get_repository_key_in(workdir: Option<&Path>) -> Result<PathBuf> {
get_git_common_dir_in(workdir).map(|path| crate::util::canon_or_self(&path))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repository_key_is_shared_by_primary_linked_and_nested_paths() {
let temp = tempfile::tempdir().unwrap();
let repo = temp.path().join("project");
std::fs::create_dir(&repo).unwrap();
crate::test_support::init_repo(&repo);
let linked = temp.path().join("feature");
crate::test_support::run_git(
&repo,
&["worktree", "add", "-b", "feature", linked.to_str().unwrap()],
);
let nested = linked.join("src");
std::fs::create_dir(&nested).unwrap();
let expected = repo.join(".git").canonicalize().unwrap();
for path in [&repo, &linked, &nested] {
assert_eq!(get_repository_key_in(Some(path)).unwrap(), expected);
}
}
#[test]
fn repository_probe_confirms_non_repository() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(get_repo_root_if_present_in(Some(dir.path())).unwrap(), None);
}
#[test]
fn repository_probe_accepts_bare_repository() {
let dir = tempfile::tempdir().unwrap();
let status = Command::new("git")
.args(["init", "--bare", "-q"])
.current_dir(dir.path())
.status()
.unwrap();
assert!(status.success());
assert!(
get_repo_root_if_present_in(Some(dir.path()))
.unwrap()
.is_some()
);
}
#[test]
fn repository_probe_rejects_broken_metadata() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join(".git"), "gitdir: /missing").unwrap();
assert!(get_repo_root_if_present_in(Some(dir.path())).is_err());
}
}