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('.')
56 || matches!(
57 name.as_ref(),
58 "node_modules" | "venv" | "target" | "vendor" | "bower_components" | "AppData"
59 )
60 {
61 return false;
62 }
63 true
64 });
65
66 for entry in walker {
67 let Ok(entry) = entry else {
70 continue;
71 };
72 if entry.file_type().is_dir() && is_git_repo(entry.path()) {
73 repos.push(entry.path().to_path_buf());
74 }
75 }
76
77 Ok(repos)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use std::fs;
84 use tempfile::TempDir;
85
86 #[test]
87 fn test_is_git_repo_true() {
88 let tmp = TempDir::new().unwrap();
89 fs::create_dir(tmp.path().join(".git")).unwrap();
90 assert!(is_git_repo(tmp.path()));
91 }
92
93 #[test]
94 fn test_is_git_repo_false() {
95 let tmp = TempDir::new().unwrap();
96 assert!(!is_git_repo(tmp.path()));
97 }
98
99 #[test]
102 fn test_is_git_repo_worktree_gitfile() {
103 let tmp = TempDir::new().unwrap();
104 fs::write(tmp.path().join(".git"), "gitdir: /repo/.git/worktrees/wt").unwrap();
105 assert!(is_git_repo(tmp.path()));
106 }
107
108 #[test]
109 fn test_scan_for_repos_finds_repos() {
110 let tmp = TempDir::new().unwrap();
111 let repo1 = tmp.path().join("project1");
113 let repo2 = tmp.path().join("project2");
114 fs::create_dir_all(repo1.join(".git")).unwrap();
115 fs::create_dir_all(repo2.join(".git")).unwrap();
116 fs::create_dir_all(tmp.path().join("not_a_repo")).unwrap();
118
119 let repos = scan_for_repos(tmp.path()).unwrap();
120 assert_eq!(repos.len(), 2);
121 }
122
123 #[test]
124 fn test_scan_for_repos_skips_node_modules() {
125 let tmp = TempDir::new().unwrap();
126 fs::create_dir_all(tmp.path().join("real_repo").join(".git")).unwrap();
128 fs::create_dir_all(
130 tmp.path()
131 .join("real_repo")
132 .join("node_modules")
133 .join("some_pkg")
134 .join(".git"),
135 )
136 .unwrap();
137
138 let repos = scan_for_repos(tmp.path()).unwrap();
139 assert_eq!(repos.len(), 1);
140 }
141
142 #[test]
143 fn test_scan_empty_directory() {
144 let tmp = TempDir::new().unwrap();
145 let repos = scan_for_repos(tmp.path()).unwrap();
146 assert!(repos.is_empty());
147 }
148}