use std::collections::HashSet;
use std::path::{Path, PathBuf};
use crate::error::{CwError, Result};
use crate::git;
#[derive(Debug, Clone)]
pub struct ScopedWorktree {
pub name: String,
pub branch: Option<String>,
pub path: PathBuf,
pub repo_root: PathBuf,
pub is_main: bool,
}
#[derive(Debug, Clone, Default)]
pub struct Scope {
worktrees: Vec<ScopedWorktree>,
}
impl Scope {
pub fn worktrees(&self) -> &[ScopedWorktree] {
&self.worktrees
}
pub fn is_empty(&self) -> bool {
self.worktrees.is_empty()
}
}
const MAX_WALK_DEPTH: usize = 4;
pub fn discover_scope(cwd: &Path) -> Result<Scope> {
if let Ok(repo_root) = git::get_main_repo_root(Some(cwd)) {
let worktrees = collect_family(&repo_root)?;
return Ok(Scope { worktrees });
}
let mut all = Vec::new();
let mut seen_roots: HashSet<PathBuf> = HashSet::new();
walk_for_repos(cwd, 0, &mut all, &mut seen_roots)?;
if all.is_empty() {
return Err(CwError::Other(
"no worktrees in scope (cwd is not in a git repo and no repos found below)".into(),
));
}
Ok(Scope { worktrees: all })
}
fn collect_family(repo_root: &Path) -> Result<Vec<ScopedWorktree>> {
let raw = git::parse_worktrees(repo_root)?;
let main_canon = git::canonicalize_or(repo_root);
let repo_root_canon = main_canon.clone();
let mut out = Vec::with_capacity(raw.len());
for (branch_raw, path) in raw {
let normalized = git::normalize_branch_name(&branch_raw);
let branch = if normalized == "(detached)" {
None
} else {
Some(normalized.to_string())
};
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let is_main = git::canonicalize_or(&path) == main_canon;
out.push(ScopedWorktree {
name,
branch,
path,
repo_root: repo_root_canon.clone(),
is_main,
});
}
Ok(out)
}
fn walk_for_repos(
dir: &Path,
depth: usize,
out: &mut Vec<ScopedWorktree>,
seen: &mut HashSet<PathBuf>,
) -> Result<()> {
if depth > MAX_WALK_DEPTH {
return Ok(());
}
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return Ok(()),
};
for entry in entries.flatten() {
let path = entry.path();
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if file_type.is_symlink() {
continue;
}
if !file_type.is_dir() {
continue;
}
if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
if name.starts_with('.') && name != ".git" {
continue;
}
if name == "node_modules" || name == "target" {
continue;
}
}
if let Ok(repo) = git::get_main_repo_root(Some(&path)) {
if seen.insert(git::canonicalize_or(&repo)) {
out.extend(collect_family(&repo)?);
}
continue;
}
walk_for_repos(&path, depth + 1, out, seen)?;
}
Ok(())
}