use std::fs;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use ignore::WalkBuilder;
use rayon::prelude::*;
use dashmap::DashMap;
use super::config::{DEFAULT_REPO_NAME, SKIP_DIRECTORIES, UNKNOWN_REPO_NAME, MAX_SCAN_DEPTH, ESTIMATED_REPO_COUNT};
fn is_git_file(path: &Path) -> bool {
match fs::File::open(path) {
Ok(file) => {
let reader = BufReader::new(file);
reader
.lines()
.take(5)
.filter_map(Result::ok)
.any(|line| line.trim_start().starts_with("gitdir:"))
}
Err(_) => false,
}
}
pub fn find_repos_from_path(search_path: impl AsRef<Path>) -> Vec<(String, PathBuf)> {
let search_path = search_path.as_ref();
let repositories = Arc::new(Mutex::new(Vec::with_capacity(ESTIMATED_REPO_COUNT)));
let seen_paths = Arc::new(DashMap::with_capacity(ESTIMATED_REPO_COUNT));
let name_counts = Arc::new(DashMap::with_capacity(ESTIMATED_REPO_COUNT));
let search_path_buf = search_path.to_path_buf();
let walker = WalkBuilder::new(search_path)
.follow_links(true) .max_depth(Some(MAX_SCAN_DEPTH)) .threads(num_cpus::get().min(8)) .filter_entry(|entry| {
let file_name = entry.file_name().to_str().unwrap_or("");
if SKIP_DIRECTORIES.contains(&file_name) {
return false;
}
if file_name == ".git" {
return false;
}
true
})
.build_parallel();
walker.run(|| {
let repositories = Arc::clone(&repositories);
let seen_paths = Arc::clone(&seen_paths);
let name_counts = Arc::clone(&name_counts);
let search_path_buf = search_path_buf.clone();
Box::new(move |result| {
use ignore::WalkState;
if let Ok(entry) = result {
let path = entry.path();
if !entry.file_type().is_some_and(|ft| ft.is_dir()) {
return WalkState::Continue;
}
let git_path = path.join(".git");
if git_path.exists() {
let is_git_repo = if git_path.is_dir() {
true
} else if git_path.is_file() {
is_git_file(&git_path)
} else {
false
};
if is_git_repo {
let path_buf = path.to_path_buf();
if seen_paths.insert(path_buf.clone(), ()).is_some() {
return WalkState::Continue;
}
let base_name = if path == search_path_buf {
search_path_buf
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(DEFAULT_REPO_NAME)
.to_string()
} else {
path.file_name()
.and_then(|n| n.to_str())
.unwrap_or(UNKNOWN_REPO_NAME)
.to_string()
};
let repo_name = {
let mut entry = name_counts.entry(base_name.clone()).or_insert(0);
*entry += 1;
let count = *entry;
if count > 1 {
format!("{}-{}", base_name, count)
} else {
base_name
}
};
repositories.lock().unwrap().push((repo_name, path_buf));
}
}
}
WalkState::Continue
})
});
let mut repos = Arc::try_unwrap(repositories)
.map(|mutex| mutex.into_inner().unwrap())
.unwrap_or_else(|arc| arc.lock().unwrap().clone());
repos.par_sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
repos
}
pub fn find_repos() -> Vec<(String, PathBuf)> {
find_repos_from_path(".")
}
pub fn init_command(scanning_msg: &str) -> (std::time::Instant, Vec<(String, PathBuf)>) {
println!();
print!("{}", scanning_msg);
std::io::stdout().flush().expect("Failed to flush stdout during repository scanning - this indicates a terminal or I/O issue");
let start_time = std::time::Instant::now();
let repos = find_repos();
(start_time, repos)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dashmap_concurrent_access() {
let map: Arc<DashMap<String, i32>> = Arc::new(DashMap::new());
let handles: Vec<_> = (0..10)
.map(|i| {
let map_clone = Arc::clone(&map);
std::thread::spawn(move || {
for j in 0..100 {
let key = format!("key-{}-{}", i, j);
map_clone.insert(key, i * 1000 + j);
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
assert_eq!(map.len(), 1000, "All concurrent inserts should succeed");
}
#[test]
fn test_dashmap_no_race_conditions() {
let map: Arc<DashMap<String, i32>> = Arc::new(DashMap::new());
let handles: Vec<_> = (0..10)
.map(|_| {
let map_clone = Arc::clone(&map);
std::thread::spawn(move || {
for _ in 0..1000 {
let mut entry = map_clone.entry("counter".to_string()).or_insert(0);
*entry += 1;
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
assert_eq!(*map.get("counter").unwrap(), 10000, "Counter should be atomic");
}
#[test]
fn test_path_deduplication_with_dashmap() {
use std::path::PathBuf;
let seen: Arc<DashMap<PathBuf, ()>> = Arc::new(DashMap::new());
let path1 = PathBuf::from("/test/repo1");
let path2 = PathBuf::from("/test/repo2");
let path1_dup = PathBuf::from("/test/repo1");
assert!(seen.insert(path1.clone(), ()).is_none());
assert!(seen.insert(path1_dup, ()).is_some());
assert!(seen.insert(path2, ()).is_none());
assert_eq!(seen.len(), 2);
}
}