use std::collections::{BTreeSet, HashSet};
use std::path::{Path, PathBuf};
pub(crate) fn discover_git_repos(
roots: &[PathBuf],
excluded_dir_names: &BTreeSet<String>,
exclude_repos: &[String],
system_repo: Option<&str>,
) -> Vec<PathBuf> {
let exlude_set: HashSet<String> = exclude_repos.iter().map(|s| s.to_lowercase()).collect();
let mut repos = Vec::new();
for root in roots {
let root_dot_git = root.join(".git");
if root_dot_git.exists()
&& (root_dot_git.is_dir() || is_git_worktree_file(&root_dot_git))
&& !exlude_set.contains(&root.to_string_lossy().to_lowercase())
{
repos.push(root.clone());
}
discover_git_repos_recursive(root, excluded_dir_names, &mut repos, 0, 4);
}
repos.retain(|r| {
let abs = r.to_string_lossy().to_lowercase();
let name = r
.file_name()
.map(|n| n.to_string_lossy().to_lowercase())
.unwrap_or_default();
!exlude_set.contains(&abs) && !exlude_set.contains(&name)
});
if let Some(system) = system_repo {
let system_path = PathBuf::from(system);
let system_abs = system.to_lowercase();
let system_name = system_path
.file_name()
.map(|n| n.to_string_lossy().to_lowercase())
.unwrap_or_default();
if system_path.exists()
&& system_path.join(".git").exists()
&& !repos.contains(&system_path)
&& !exlude_set.contains(&system_abs)
&& !exlude_set.contains(&system_name)
{
repos.push(system_path);
}
}
repos
}
fn discover_git_repos_recursive(
dir: &Path,
excluded_dir_names: &BTreeSet<String>,
repos: &mut Vec<PathBuf>,
depth: usize,
max_depth: usize,
) {
if depth > max_depth {
return;
}
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(e) => {
eprintln!("⚠️ cannot read directory {}: {}", dir.display(), e);
return;
}
};
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => {
eprintln!("⚠️ cannot read entry in {}: {}", dir.display(), e);
continue;
}
};
let path = entry.path();
if !path.is_dir() || path.is_symlink() {
continue;
}
let name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if excluded_dir_names.contains(&name) || name == "objects" {
continue;
}
let dot_git = path.join(".git");
if dot_git.exists() && (dot_git.is_dir() || is_git_worktree_file(&dot_git)) {
repos.push(path.clone());
} else if name.starts_with('.') {
continue;
}
discover_git_repos_recursive(&path, excluded_dir_names, repos, depth + 1, max_depth);
}
}
pub(crate) fn is_git_worktree_file(dot_git: &Path) -> bool {
std::fs::read_to_string(dot_git)
.map(|content| content.trim().starts_with("gitdir:"))
.unwrap_or(false)
}
pub(crate) fn is_safe_git_path(path: &Path) -> bool {
if path.is_absolute() {
return false;
}
let mut components = path.components();
if let Some(first) = components.next() {
if first.as_os_str() == ".." {
return false;
}
}
if let Some(first) = components.next() {
if first.as_os_str() == ".." {
return false;
}
}
if path.to_string_lossy().starts_with('-') {
return false;
}
true
}
pub(crate) fn is_safe_branch_name(branch: &str) -> bool {
if branch.is_empty() {
return false;
}
if branch.starts_with('-') {
return false;
}
if branch.contains("..") {
return false;
}
if branch.contains('\n') || branch.contains('\r') || branch.contains('\0') {
return false;
}
if branch.ends_with('.') {
return false;
}
if branch.contains('\\') || branch.contains('~') || branch.contains('^') || branch.contains(':')
{
return false;
}
if branch.contains('?') || branch.contains('*') || branch.contains('[') {
return false;
}
if branch.contains(' ') {
return false;
}
true
}