use std::error::Error;
use std::path::{Path, PathBuf};
pub fn find_repository_root(start_path: &Path) -> Result<PathBuf, Box<dyn Error>> {
let start = if start_path.is_relative() {
std::env::current_dir()?.join(start_path)
} else {
start_path.to_path_buf()
};
let mut current = start.canonicalize()?;
loop {
let git_entry = current.join(".git");
if git_entry.exists() {
if git_entry.is_file()
&& let Ok(common_dir) = resolve_git_common_dir(¤t)
&& let Some(main_root) = common_dir.parent()
{
return Ok(main_root.to_path_buf());
}
return Ok(current);
}
match current.parent() {
Some(parent) => current = parent.to_path_buf(),
None => {
return Err(format!(
"Not in a git repository (or any of the parent directories): {}",
start_path.display()
)
.into());
}
}
}
}
pub fn resolve_git_dir(repo_path: &Path) -> Result<PathBuf, Box<dyn Error>> {
let git_path = repo_path.join(".git");
let metadata = std::fs::metadata(&git_path)
.map_err(|e| format!("Cannot access {}: {}", git_path.display(), e))?;
if metadata.is_dir() {
return Ok(git_path);
}
let content = std::fs::read_to_string(&git_path)
.map_err(|e| format!("Cannot read {}: {}", git_path.display(), e))?;
let gitdir_line = content.trim();
let gitdir_path = gitdir_line.strip_prefix("gitdir: ").ok_or_else(|| {
format!(
"Invalid .git file format in {}: {}",
git_path.display(),
gitdir_line
)
})?;
let gitdir = Path::new(gitdir_path);
let resolved = if gitdir.is_absolute() {
gitdir.to_path_buf()
} else {
repo_path.join(gitdir)
};
resolved
.canonicalize()
.map_err(|e| format!("Cannot resolve git dir {}: {}", resolved.display(), e).into())
}
pub fn resolve_git_common_dir(repo_path: &Path) -> Result<PathBuf, Box<dyn Error>> {
let git_dir = resolve_git_dir(repo_path)?;
let commondir_path = git_dir.join("commondir");
if commondir_path.is_file() {
let content = std::fs::read_to_string(&commondir_path)
.map_err(|e| format!("Cannot read {}: {}", commondir_path.display(), e))?;
let commondir = content.trim();
let common = if Path::new(commondir).is_absolute() {
PathBuf::from(commondir)
} else {
git_dir.join(commondir)
};
return common
.canonicalize()
.map_err(|e| format!("Cannot resolve common dir {}: {}", common.display(), e).into());
}
Ok(git_dir)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use crate::test_utils::TestGitRepository;
#[test]
fn test_find_repository_root_in_git_repo() {
let test_repo = TestGitRepository::new().unwrap();
test_repo.init().unwrap();
let repo_root = test_repo.path();
let sub_dir = repo_root.join("src").join("commands");
fs::create_dir_all(&sub_dir).unwrap();
assert_eq!(
find_repository_root(repo_root).unwrap(),
repo_root.canonicalize().unwrap()
);
assert_eq!(
find_repository_root(&sub_dir).unwrap(),
repo_root.canonicalize().unwrap()
);
assert_eq!(
find_repository_root(&repo_root.join("src")).unwrap(),
repo_root.canonicalize().unwrap()
);
}
#[test]
fn test_find_repository_root_not_in_repo() {
let test_repo = TestGitRepository::new().unwrap();
let result = find_repository_root(test_repo.path());
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Not in a git repository")
);
}
#[test]
fn test_resolve_git_dir_normal_repo() {
let repo = TestGitRepository::new().unwrap();
repo.init().unwrap();
let git_dir = resolve_git_dir(repo.path()).unwrap();
assert!(git_dir.is_dir());
assert_eq!(git_dir.file_name().unwrap(), ".git");
}
#[test]
fn test_resolve_git_dir_worktree() {
let repo = TestGitRepository::new().unwrap();
repo.init_with_commit().unwrap();
let worktree_tmp = tempfile::TempDir::new().unwrap();
let worktree_dir = worktree_tmp.path().join("worktree");
repo.run_git_command(&[
"worktree",
"add",
worktree_dir.to_str().unwrap(),
"-b",
"wt-branch",
])
.unwrap();
let git_dir = resolve_git_dir(&worktree_dir).unwrap();
assert!(git_dir.is_dir());
assert!(git_dir.to_str().unwrap().contains("worktrees"));
std::fs::remove_dir_all(&worktree_dir).ok();
repo.run_git_command(&["worktree", "prune"]).ok();
}
#[test]
fn test_resolve_git_common_dir_normal_repo() {
let repo = TestGitRepository::new().unwrap();
repo.init().unwrap();
let common_dir = resolve_git_common_dir(repo.path()).unwrap();
let git_dir = resolve_git_dir(repo.path()).unwrap();
assert_eq!(common_dir, git_dir);
}
#[test]
fn test_resolve_git_common_dir_worktree() {
let repo = TestGitRepository::new().unwrap();
repo.init_with_commit().unwrap();
let worktree_tmp = tempfile::TempDir::new().unwrap();
let worktree_dir = worktree_tmp.path().join("worktree");
repo.run_git_command(&[
"worktree",
"add",
worktree_dir.to_str().unwrap(),
"-b",
"wt-common-branch",
])
.unwrap();
let common_dir = resolve_git_common_dir(&worktree_dir).unwrap();
let main_git_dir = resolve_git_dir(repo.path()).unwrap();
assert_eq!(
common_dir.canonicalize().unwrap(),
main_git_dir.canonicalize().unwrap()
);
std::fs::remove_dir_all(&worktree_dir).ok();
repo.run_git_command(&["worktree", "prune"]).ok();
}
#[test]
fn test_find_repository_root_from_worktree() {
let repo = TestGitRepository::new().unwrap();
repo.init_with_commit().unwrap();
let worktree_tmp = tempfile::TempDir::new().unwrap();
let worktree_dir = worktree_tmp.path().join("worktree");
repo.run_git_command(&[
"worktree",
"add",
worktree_dir.to_str().unwrap(),
"-b",
"wt-find-root-branch",
])
.unwrap();
let found_root = find_repository_root(&worktree_dir).unwrap();
assert_eq!(
found_root.canonicalize().unwrap(),
repo.path().canonicalize().unwrap()
);
let sub_dir = worktree_dir.join("subdir");
fs::create_dir_all(&sub_dir).unwrap();
let found_root_from_sub = find_repository_root(&sub_dir).unwrap();
assert_eq!(
found_root_from_sub.canonicalize().unwrap(),
repo.path().canonicalize().unwrap()
);
std::fs::remove_dir_all(&worktree_dir).ok();
repo.run_git_command(&["worktree", "prune"]).ok();
}
#[test]
fn test_resolve_git_dir_not_a_repo() {
let temp = tempfile::TempDir::new().unwrap();
let result = resolve_git_dir(temp.path());
assert!(result.is_err());
}
}