use crate::error::Result;
use crate::worktree::{self, WorktreeInfo};
use git2::Repository;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct WorkspaceRepo {
pub name: String,
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct Workspace {
pub root: PathBuf,
pub repos: Vec<WorkspaceRepo>,
}
impl Workspace {
pub fn is_empty(&self) -> bool {
self.repos.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct WorkspaceRow {
pub repo_name: String,
pub repo_path: PathBuf,
pub info: WorktreeInfo,
}
pub fn discover(root: &Path) -> Result<Workspace> {
let entries = std::fs::read_dir(root)?;
let mut repos: Vec<WorkspaceRepo> = Vec::new();
let mut seen: Vec<PathBuf> = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let Ok(repo) = Repository::open(&path) else {
continue;
};
if repo.is_bare() {
continue;
}
let Some(main_workdir) = main_workdir(&repo) else {
continue;
};
if !repo.is_worktree() && !paths_equal(&main_workdir, &path) {
continue;
}
let canon = main_workdir.canonicalize().unwrap_or_else(|_| main_workdir.clone());
if seen.contains(&canon) {
continue;
}
seen.push(canon);
let name = main_workdir
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "repo".into());
repos.push(WorkspaceRepo {
name,
path: main_workdir,
});
}
repos.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.path.cmp(&b.path)));
let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
for r in &mut repos {
if used.insert(r.name.clone()) {
continue;
}
let mut n = 2;
loop {
let candidate = format!("{}-{}", r.name, n);
if used.insert(candidate.clone()) {
r.name = candidate;
break;
}
n += 1;
}
}
Ok(Workspace {
root: root.to_path_buf(),
repos,
})
}
fn main_workdir(repo: &Repository) -> Option<PathBuf> {
if repo.is_worktree() {
let admin = repo.path();
let main = admin.parent()?.parent()?.parent()?;
Repository::open(main).ok()?.workdir().map(Path::to_path_buf)
} else {
repo.workdir().map(Path::to_path_buf)
}
}
fn paths_equal(a: &Path, b: &Path) -> bool {
let ca = a.canonicalize().unwrap_or_else(|_| a.to_path_buf());
let cb = b.canonicalize().unwrap_or_else(|_| b.to_path_buf());
ca == cb
}
pub fn autodetect(cwd: &Path) -> Option<Workspace> {
if Repository::discover(cwd).is_ok() {
return None;
}
let ws = discover(cwd).ok()?;
if ws.is_empty() {
None
} else {
Some(ws)
}
}
pub fn merge_worktrees(workspace: &Workspace) -> Result<Vec<WorkspaceRow>> {
let mut rows: Vec<WorkspaceRow> = Vec::new();
for repo in &workspace.repos {
let Ok(handle) = Repository::open(&repo.path) else {
continue;
};
let Ok(trees) = worktree::list(&handle) else {
continue;
};
for info in trees {
rows.push(WorkspaceRow {
repo_name: repo.name.clone(),
repo_path: repo.path.clone(),
info,
});
}
}
Ok(rows)
}