use std::collections::BTreeMap; use std::fs; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; #[derive(Clone, Debug)] pub struct Repository { pub user: String, pub name: String, pub path: PathBuf, pub description: Option, } /// Discover git repositories exactly two levels beneath `root`: /// `root/user/repo` or `root/user/repo.git`. pub fn discover(root: &Path) -> Result> { let root = root .canonicalize() .with_context(|| format!("canonicalizing repository root {}", root.display()))?; let mut found = BTreeMap::new(); for user in sorted_dirs(&root)? { let Some(user_name) = user.file_name().and_then(|n| n.to_str()) else { continue; }; if user_name.starts_with('.') { continue; } for path in sorted_dirs(&user)? { let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else { continue; }; if file_name.starts_with('.') { continue; } let name = file_name.strip_suffix(".git").unwrap_or(file_name); let Ok(repo) = gix::open(&path) else { continue; }; if name.is_empty() { continue; } let description = crate::generate::description(&repo); found.entry((user_name.to_owned(), name.to_owned())).or_insert(Repository { user: user_name.to_owned(), name: name.to_owned(), path, description, }); } } Ok(found.into_values().collect()) } fn sorted_dirs(path: &Path) -> Result> { let mut dirs = fs::read_dir(path) .with_context(|| format!("reading {}", path.display()))? .filter_map(|entry| entry.ok()) .filter_map(|entry| entry.file_type().ok()?.is_dir().then_some(entry.path())) .collect::>(); dirs.sort(); Ok(dirs) }