1pub mod git;
12
13use std::path::{Path, PathBuf};
14
15use anyhow::Result;
16use walkdir::WalkDir;
17
18const MAX_SCAN_DEPTH: usize = 8;
23
24pub fn is_git_repo(path: &Path) -> bool {
30 let dot_git = path.join(".git");
31 dot_git.is_dir() || dot_git.is_file()
32}
33
34pub fn scan_for_repos(root: &Path) -> Result<Vec<PathBuf>> {
40 let mut repos = Vec::new();
41
42 let walker = WalkDir::new(root)
43 .follow_links(false)
44 .max_depth(MAX_SCAN_DEPTH)
45 .into_iter()
46 .filter_entry(|entry| {
47 if entry.path() == root {
49 return true;
50 }
51 let name = entry.file_name().to_string_lossy();
52 if name.starts_with('.')
54 || matches!(
55 name.as_ref(),
56 "node_modules" | "venv" | "target" | "AppData"
57 )
58 {
59 return false;
60 }
61 true
62 });
63
64 for entry in walker {
65 let entry = entry?;
66 if entry.file_type().is_dir() && is_git_repo(entry.path()) {
67 repos.push(entry.path().to_path_buf());
68 }
69 }
70
71 Ok(repos)
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use std::fs;
78 use tempfile::TempDir;
79
80 #[test]
81 fn test_is_git_repo_true() {
82 let tmp = TempDir::new().unwrap();
83 fs::create_dir(tmp.path().join(".git")).unwrap();
84 assert!(is_git_repo(tmp.path()));
85 }
86
87 #[test]
88 fn test_is_git_repo_false() {
89 let tmp = TempDir::new().unwrap();
90 assert!(!is_git_repo(tmp.path()));
91 }
92
93 #[test]
96 fn test_is_git_repo_worktree_gitfile() {
97 let tmp = TempDir::new().unwrap();
98 fs::write(tmp.path().join(".git"), "gitdir: /repo/.git/worktrees/wt").unwrap();
99 assert!(is_git_repo(tmp.path()));
100 }
101
102 #[test]
103 fn test_scan_for_repos_finds_repos() {
104 let tmp = TempDir::new().unwrap();
105 let repo1 = tmp.path().join("project1");
107 let repo2 = tmp.path().join("project2");
108 fs::create_dir_all(repo1.join(".git")).unwrap();
109 fs::create_dir_all(repo2.join(".git")).unwrap();
110 fs::create_dir_all(tmp.path().join("not_a_repo")).unwrap();
112
113 let repos = scan_for_repos(tmp.path()).unwrap();
114 assert_eq!(repos.len(), 2);
115 }
116
117 #[test]
118 fn test_scan_for_repos_skips_node_modules() {
119 let tmp = TempDir::new().unwrap();
120 fs::create_dir_all(tmp.path().join("real_repo").join(".git")).unwrap();
122 fs::create_dir_all(
124 tmp.path()
125 .join("real_repo")
126 .join("node_modules")
127 .join("some_pkg")
128 .join(".git"),
129 )
130 .unwrap();
131
132 let repos = scan_for_repos(tmp.path()).unwrap();
133 assert_eq!(repos.len(), 1);
134 }
135
136 #[test]
137 fn test_scan_empty_directory() {
138 let tmp = TempDir::new().unwrap();
139 let repos = scan_for_repos(tmp.path()).unwrap();
140 assert!(repos.is_empty());
141 }
142}