3f5e9424
add repo discovery module for scanning repos directory
a73x 2026-03-30 18:39
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
diff --git a/src/server/main.rs b/src/server/main.rs index 779073e..77871b0 100644 --- a/src/server/main.rs +++ b/src/server/main.rs @@ -1,4 +1,5 @@ mod config; mod repos; fn main() { println!("git-collab-server: not yet implemented"); diff --git a/src/server/repos.rs b/src/server/repos.rs new file mode 100644 index 0000000..9b23aec --- /dev/null +++ b/src/server/repos.rs @@ -0,0 +1,128 @@ use std::path::{Path, PathBuf}; /// A discovered git repository on disk. #[derive(Debug, Clone)] pub struct RepoEntry { pub name: String, pub path: PathBuf, pub bare: bool, } /// Scan a directory for git repositories. pub fn discover(repos_dir: &Path) -> Result<Vec<RepoEntry>, std::io::Error> { let mut entries = Vec::new(); let read_dir = std::fs::read_dir(repos_dir)?; for entry in read_dir { let entry = entry?; let path = entry.path(); if !path.is_dir() { continue; } let dir_name = entry.file_name().to_string_lossy().to_string(); if path.join("HEAD").is_file() { let name = dir_name.strip_suffix(".git").unwrap_or(&dir_name).to_string(); entries.push(RepoEntry { name, path, bare: true }); } else if path.join(".git").is_dir() { entries.push(RepoEntry { name: dir_name, path, bare: false }); } } entries.sort_by(|a, b| a.name.cmp(&b.name)); Ok(entries) } /// Resolve a repo name to its on-disk entry. pub fn resolve(repos_dir: &Path, name: &str) -> Option<RepoEntry> { let entries = discover(repos_dir).ok()?; entries.into_iter().find(|e| e.name == name) } /// Open a git2::Repository from a RepoEntry. pub fn open(entry: &RepoEntry) -> Result<git2::Repository, git2::Error> { if entry.bare { git2::Repository::open_bare(&entry.path) } else { git2::Repository::open(&entry.path) } } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; use std::process::Command; fn init_bare(parent: &Path, name: &str) { Command::new("git") .args(["init", "--bare", name]) .current_dir(parent) .output() .expect("git init --bare failed"); } fn init_non_bare(parent: &Path, name: &str) { Command::new("git") .args(["init", name]) .current_dir(parent) .output() .expect("git init failed"); } #[test] fn discover_bare_repos() { let tmp = TempDir::new().unwrap(); init_bare(tmp.path(), "alpha.git"); init_bare(tmp.path(), "beta.git"); let repos = discover(tmp.path()).unwrap(); assert_eq!(repos.len(), 2); assert_eq!(repos[0].name, "alpha"); assert_eq!(repos[1].name, "beta"); assert!(repos[0].bare); } #[test] fn discover_non_bare_repos() { let tmp = TempDir::new().unwrap(); init_non_bare(tmp.path(), "myproject"); let repos = discover(tmp.path()).unwrap(); assert_eq!(repos.len(), 1); assert_eq!(repos[0].name, "myproject"); assert!(!repos[0].bare); } #[test] fn discover_skips_non_repo_dirs() { let tmp = TempDir::new().unwrap(); std::fs::create_dir(tmp.path().join("not-a-repo")).unwrap(); init_bare(tmp.path(), "real.git"); let repos = discover(tmp.path()).unwrap(); assert_eq!(repos.len(), 1); assert_eq!(repos[0].name, "real"); } #[test] fn resolve_finds_repo_by_name() { let tmp = TempDir::new().unwrap(); init_bare(tmp.path(), "myrepo.git"); let entry = resolve(tmp.path(), "myrepo").unwrap(); assert_eq!(entry.name, "myrepo"); } #[test] fn resolve_returns_none_for_unknown() { let tmp = TempDir::new().unwrap(); assert!(resolve(tmp.path(), "nope").is_none()); } #[test] fn open_bare_repo() { let tmp = TempDir::new().unwrap(); init_bare(tmp.path(), "test.git"); let entry = resolve(tmp.path(), "test").unwrap(); let repo = open(&entry).unwrap(); assert!(repo.is_bare()); } }