use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct RepoEntry {
pub path: PathBuf,
pub name: String,
pub is_workspace_root: bool,
}
const MAX_DEPTH: usize = 3;
pub fn discover_repos(workspace: &Path) -> Vec<RepoEntry> {
if workspace.join(".git").exists() {
let name = workspace
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| ".".to_string());
return vec![RepoEntry {
path: workspace.to_path_buf(),
name,
is_workspace_root: true,
}];
}
let mut out: Vec<RepoEntry> = Vec::new();
walk(workspace, 0, &mut out);
out.sort_by_key(|r| r.name.to_lowercase());
out
}
fn walk(dir: &Path, depth: usize, out: &mut Vec<RepoEntry>) {
if depth > MAX_DEPTH {
return;
}
let Ok(rd) = std::fs::read_dir(dir) else {
return;
};
for entry in rd.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = entry.file_name().to_string_lossy().into_owned();
if name.starts_with('.') || name == "node_modules" || name == "target" {
continue;
}
if path.join(".git").exists() {
out.push(RepoEntry {
path: path.clone(),
name: name.clone(),
is_workspace_root: false,
});
continue;
}
walk(&path, depth + 1, out);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn workspace_is_a_repo_returns_just_itself() {
let d = tempfile::tempdir().unwrap();
std::fs::create_dir(d.path().join(".git")).unwrap();
let sub = d.path().join("sub");
std::fs::create_dir(&sub).unwrap();
std::fs::create_dir(sub.join(".git")).unwrap();
let repos = discover_repos(d.path());
assert_eq!(repos.len(), 1);
assert!(repos[0].is_workspace_root);
assert_eq!(repos[0].path, d.path());
}
#[test]
fn discovers_sibling_repos() {
let d = tempfile::tempdir().unwrap();
for name in ["alpha", "beta", "gamma"] {
let p = d.path().join(name);
std::fs::create_dir(&p).unwrap();
std::fs::create_dir(p.join(".git")).unwrap();
}
std::fs::create_dir(d.path().join("just-a-dir")).unwrap();
let repos = discover_repos(d.path());
assert_eq!(repos.len(), 3);
assert_eq!(repos[0].name, "alpha");
assert_eq!(repos[1].name, "beta");
assert_eq!(repos[2].name, "gamma");
assert!(!repos.iter().any(|r| r.is_workspace_root));
}
#[test]
fn skips_dot_and_node_modules() {
let d = tempfile::tempdir().unwrap();
let mn = d.path().join(".mnml");
std::fs::create_dir(&mn).unwrap();
std::fs::create_dir(mn.join(".git")).unwrap();
let nm = d.path().join("node_modules").join("pkg");
std::fs::create_dir_all(&nm).unwrap();
std::fs::create_dir(nm.join(".git")).unwrap();
let real = d.path().join("real");
std::fs::create_dir(&real).unwrap();
std::fs::create_dir(real.join(".git")).unwrap();
let repos = discover_repos(d.path());
assert_eq!(repos.len(), 1);
assert_eq!(repos[0].name, "real");
}
}