use anyhow::Result;
use crossbeam_channel::Sender;
use std::io::Write;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::anomalous_paths;
pub fn find_git_repositories(base_path: &Path) -> Result<Vec<PathBuf>> {
let mut repo_paths = Vec::new();
if base_path.join(".git").exists() {
repo_paths.push(base_path.to_path_buf());
} else if base_path.is_dir() {
for entry in std::fs::read_dir(base_path)? {
let path = entry?.path();
if path.is_dir() && path.join(".git").exists() {
repo_paths.push(path);
}
}
}
repo_paths.sort();
Ok(repo_paths)
}
pub fn for_each_repository(
repo_paths: &[PathBuf],
mut process: impl FnMut(&Path, &str) -> Result<()>,
) {
for repo_path in repo_paths {
let repository_name = repo_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
print!("Scanning {repository_name}... ");
let _ = std::io::stdout().flush();
if let Err(e) = process(repo_path, &repository_name) {
eprintln!("Failed to process {repo_path:?}: {e:?}");
} else {
println!("done");
}
let _ = std::io::stdout().flush();
}
}
pub fn all_files_from_path(root: &Path, path_tx: Sender<PathBuf>) -> Result<()> {
if root.is_file() {
if !anomalous_paths::is_anomalous(root) {
let _ = path_tx.send(PathBuf::from(root));
}
} else if root.is_dir() {
for entry in WalkDir::new(root).into_iter().filter_map(Result::ok) {
if entry.file_type().is_file() {
if anomalous_paths::is_anomalous(entry.path()) {
continue;
}
if path_tx.send(entry.into_path()).is_err() {
break;
}
}
}
}
drop(path_tx);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use std::fs;
use tempfile::tempdir;
fn collect_files(root: &Path) -> HashSet<PathBuf> {
let (tx, rx) = crossbeam_channel::unbounded();
all_files_from_path(root, tx).expect("walk root");
rx.into_iter().collect()
}
#[test]
fn walks_directory_and_skips_anomalous_paths() {
let dir = tempdir().expect("create temp dir");
let normal_file = dir.path().join("main.rs");
fs::write(&normal_file, "fn main() {}").expect("write normal file");
let nested_dir = dir.path().join("src").join("nested");
fs::create_dir_all(&nested_dir).expect("create nested dir");
let nested_file = nested_dir.join("lib.rs");
fs::write(&nested_file, "// nested").expect("write nested file");
let anomalous_dir = dir.path().join("third_party");
fs::create_dir_all(&anomalous_dir).expect("create anomalous dir");
let anomalous_file = anomalous_dir.join("vendored.rs");
fs::write(&anomalous_file, "// vendored").expect("write anomalous file");
let found = collect_files(dir.path());
assert!(found.contains(&normal_file));
assert!(found.contains(&nested_file));
assert!(!found.contains(&anomalous_file));
assert_eq!(found.len(), 2);
}
#[test]
fn for_each_repository_visits_every_path_with_its_derived_name() {
let repo_paths = vec![PathBuf::from("/repos/alpha"), PathBuf::from("/repos/beta")];
let mut visited = Vec::new();
for_each_repository(&repo_paths, |path, name| {
visited.push((path.to_path_buf(), name.to_string()));
Ok(())
});
assert_eq!(
visited,
vec![
(PathBuf::from("/repos/alpha"), "alpha".to_string()),
(PathBuf::from("/repos/beta"), "beta".to_string()),
]
);
}
#[test]
fn for_each_repository_keeps_going_after_one_repository_errors() {
let repo_paths = vec![
PathBuf::from("/repos/fails"),
PathBuf::from("/repos/succeeds"),
];
let mut visited = Vec::new();
for_each_repository(&repo_paths, |path, _name| {
if path.ends_with("fails") {
anyhow::bail!("simulated failure");
}
visited.push(path.to_path_buf());
Ok(())
});
assert_eq!(
visited,
vec![PathBuf::from("/repos/succeeds")],
"the second repository should still be processed after the first one errors"
);
}
#[test]
fn single_file_path_is_returned_directly() {
let dir = tempdir().expect("create temp dir");
let file = dir.path().join("only.rs");
fs::write(&file, "fn main() {}").expect("write file");
let found = collect_files(&file);
assert_eq!(found, HashSet::from([file]));
}
#[test]
fn single_anomalous_file_path_is_skipped() {
let dir = tempdir().expect("create temp dir");
let nested = dir.path().join("third_party");
fs::create_dir_all(&nested).expect("create dir");
let file = nested.join("only.rs");
fs::write(&file, "fn main() {}").expect("write file");
let found = collect_files(&file);
assert!(found.is_empty());
}
}