1use std::path::{Path, PathBuf};
2
3pub fn resolve_git_root(start: &Path) -> Option<PathBuf> {
12 let abs = start.canonicalize().unwrap_or_else(|_| start.to_path_buf());
13 let mut cur = abs.as_path();
14 loop {
15 let dot_git = cur.join(".git");
16 if dot_git.is_dir() {
17 return Some(cur.to_path_buf());
18 }
19 if dot_git.is_file() {
20 if let Ok(content) = std::fs::read_to_string(&dot_git) {
21 let content = content.trim();
22 if let Some(gitdir) = content.strip_prefix("gitdir:") {
23 let gitdir = gitdir.trim().replace('\\', "/");
24 if let Some(pos) = gitdir.find("/worktrees/") {
25 let common_git = &gitdir[..pos];
29 return Path::new(common_git).parent().map(|p| p.to_path_buf());
30 }
31 }
32 }
33 return Some(cur.to_path_buf());
35 }
36 cur = cur.parent()?;
37 }
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43
44 #[test]
45 fn resolve_git_root_normal_repo() {
46 let tmp = tempfile::tempdir().unwrap();
47 let repo = tmp.path().join("my-repo");
48 std::fs::create_dir_all(repo.join(".git")).unwrap();
49
50 let result = resolve_git_root(&repo);
51 assert_eq!(result.unwrap(), repo.canonicalize().unwrap());
52 }
53
54 #[test]
55 fn resolve_git_root_worktree() {
56 let tmp = tempfile::tempdir().unwrap();
57 let repo = tmp.path().join("repo");
58 std::fs::create_dir_all(repo.join(".git").join("worktrees").join("feat-x")).unwrap();
59
60 let wt = repo.join(".claude").join("worktrees").join("feat-x");
61 std::fs::create_dir_all(&wt).unwrap();
62
63 let gitdir = repo.join(".git").join("worktrees").join("feat-x");
64 let gitdir_str = gitdir
65 .canonicalize()
66 .unwrap()
67 .to_string_lossy()
68 .replace('\\', "/");
69 std::fs::write(wt.join(".git"), format!("gitdir: {gitdir_str}")).unwrap();
70
71 let resolved = resolve_git_root(&wt).unwrap();
72 let repo_canon = repo.canonicalize().unwrap();
73 let norm = |p: &Path| p.to_string_lossy().replace('\\', "/").to_lowercase();
74 assert_eq!(
75 norm(&resolved),
76 norm(&repo_canon),
77 "worktree should resolve to repo root"
78 );
79 }
80
81 #[test]
82 fn resolve_git_root_submodule() {
83 let tmp = tempfile::tempdir().unwrap();
84 let sub = tmp.path().join("parent").join("submodule");
85 std::fs::create_dir_all(&sub).unwrap();
86 std::fs::write(sub.join(".git"), "gitdir: ../../.git/modules/submodule").unwrap();
87
88 let resolved = resolve_git_root(&sub).unwrap();
89 assert_eq!(resolved, sub.canonicalize().unwrap());
90 }
91
92 #[test]
93 fn resolve_git_root_non_git_returns_none() {
94 let tmp = tempfile::tempdir().unwrap();
95 let dir = tmp.path().join("not-a-repo");
96 std::fs::create_dir_all(&dir).unwrap();
97
98 assert!(resolve_git_root(&dir).is_none());
99 }
100}