use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, Default)]
pub struct ScanOptions {
pub max_depth: Option<usize>,
pub nested: bool,
}
pub fn find_git_repos(root: &Path, opts: ScanOptions) -> Vec<PathBuf> {
let mut found = Vec::new();
walk(root, 0, opts, &mut found);
found.sort();
found.dedup();
found
}
fn walk(dir: &Path, depth: usize, opts: ScanOptions, found: &mut Vec<PathBuf>) {
let is_repo = dir.join(".git").exists();
if is_repo {
found.push(dir.to_path_buf());
if !opts.nested {
return;
}
}
if opts.max_depth.is_some_and(|max| depth >= max) {
return;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
if file_type.is_symlink() || !file_type.is_dir() {
continue;
}
let name = entry.file_name();
if should_skip_dir(&name.to_string_lossy()) {
continue;
}
walk(&entry.path(), depth + 1, opts, found);
}
}
fn should_skip_dir(name: &str) -> bool {
name.starts_with('.') || matches!(name, "node_modules" | "target")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skips_hidden_and_heavy_dirs() {
assert!(should_skip_dir(".git"));
assert!(should_skip_dir(".cache"));
assert!(should_skip_dir("node_modules"));
assert!(should_skip_dir("target"));
assert!(!should_skip_dir("src"));
assert!(!should_skip_dir("my-repo"));
}
}