use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use tokio::task;
use tracing::debug;
use walkdir::WalkDir;
pub async fn discover_git_repositories<P: AsRef<Path>>(
root_path: P,
max_depth: usize,
) -> Result<Vec<PathBuf>> {
let root_path = root_path.as_ref().to_path_buf();
debug!(
"Discovering repositories in {} with max depth {}",
root_path.display(),
max_depth
);
let repositories = task::spawn_blocking(move || -> Result<Vec<PathBuf>> {
let mut repos = Vec::new();
for entry in WalkDir::new(&root_path)
.max_depth(max_depth)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if !path.is_dir() {
continue;
}
let git_dir = path.join(".git");
if git_dir.exists() {
debug!("Found git repository: {}", path.display());
repos.push(path.to_path_buf());
continue;
}
if let Some(
"node_modules" | "target" | "dist" | "build" | ".git" | "vendor" | "__pycache__"
| ".venv" | "venv",
) = path.file_name().and_then(|n| n.to_str())
{
debug!("Skipping directory: {}", path.display());
continue;
}
}
repos.sort();
Ok(repos)
})
.await
.context("Failed to complete repository discovery task")?
.context("Repository discovery failed")?;
debug!("Discovered {} repositories", repositories.len());
Ok(repositories)
}
#[allow(dead_code)]
pub fn is_git_repository<P: AsRef<Path>>(path: P) -> bool {
let git_dir = path.as_ref().join(".git");
git_dir.exists()
}
pub fn get_repository_name<P: AsRef<Path>>(path: P) -> Option<String> {
path.as_ref()
.file_name()
.and_then(|name| name.to_str())
.map(|s| s.to_string())
}
pub fn get_remote_url<P: AsRef<Path>>(repo_path: P) -> Result<Option<String>> {
use git2::Repository;
let repo = Repository::open(&repo_path).with_context(|| {
format!(
"Failed to open git repository: {}",
repo_path.as_ref().display()
)
})?;
let remote_name = match repo.find_remote("origin") {
Ok(_) => "origin".to_string(),
Err(_) => {
let remotes = repo.remotes()?;
match remotes.get(0) {
Some(name) => name.to_string(),
None => {
debug!(
"No remotes found for repository: {}",
repo_path.as_ref().display()
);
return Ok(None);
}
}
}
};
let remote = repo
.find_remote(&remote_name)
.with_context(|| format!("Failed to find remote '{remote_name}' in repository"))?;
Ok(remote.url().map(|url| url.to_string()))
}
pub fn get_current_branch<P: AsRef<Path>>(repo_path: P) -> Result<Option<String>> {
use git2::Repository;
let repo = Repository::open(&repo_path).with_context(|| {
format!(
"Failed to open git repository: {}",
repo_path.as_ref().display()
)
})?;
let head = match repo.head() {
Ok(head) => head,
Err(ref e) if e.code() == git2::ErrorCode::UnbornBranch => {
debug!(
"Repository has no commits yet: {}",
repo_path.as_ref().display()
);
return Ok(None);
}
Err(e) => return Err(e.into()),
};
let branch_name = head.shorthand().map(|s| s.to_string());
Ok(branch_name)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[tokio::test]
async fn test_discover_empty_directory() {
let temp_dir = TempDir::new().unwrap();
let repos = discover_git_repositories(temp_dir.path(), 2).await.unwrap();
assert!(repos.is_empty());
}
#[tokio::test]
async fn test_discover_with_git_repo() {
let temp_dir = TempDir::new().unwrap();
let repo_dir = temp_dir.path().join("test-repo");
fs::create_dir_all(&repo_dir).unwrap();
fs::create_dir_all(repo_dir.join(".git")).unwrap();
let repos = discover_git_repositories(temp_dir.path(), 2).await.unwrap();
assert_eq!(repos.len(), 1);
assert_eq!(repos[0], repo_dir);
}
#[test]
fn test_is_git_repository() {
let temp_dir = TempDir::new().unwrap();
assert!(!is_git_repository(temp_dir.path()));
let git_dir = temp_dir.path().join(".git");
fs::create_dir_all(&git_dir).unwrap();
assert!(is_git_repository(temp_dir.path()));
}
#[test]
fn test_get_repository_name() {
assert_eq!(
get_repository_name("/path/to/my-repo"),
Some("my-repo".to_string())
);
assert_eq!(get_repository_name("/"), None);
}
}