src/server/repos.rs
Ref: Size: 3.8 KiB
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.
/// Note: this performs a full directory scan on each call. Fine for a small
/// number of repos; consider caching with a short TTL if this becomes a bottleneck.
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());
}
}