use anyhow::Result;
use crossbeam_channel::{Receiver, Sender};
use ignore::overrides::OverrideBuilder;
use ignore::{DirEntry, WalkBuilder as InternalWalkBuilder, WalkParallel, WalkState};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub type WalkResult = Result<DirEntry, ignore::Error>;
pub type FnVisitor<'s> = Box<dyn FnMut(WalkResult) -> WalkState + Send + 's>;
type WalkPredicate = Arc<dyn Fn(WalkResult) -> bool + Send + Sync + 'static>;
pub struct Walk {
inner: WalkParallel,
max_capacity: Option<usize>,
quit_while: WalkPredicate,
send_while: WalkPredicate,
}
impl Walk {
pub fn new(inner: WalkParallel, max_capacity: Option<usize>) -> Self {
Self {
inner,
max_capacity,
quit_while: Arc::new(|_| false),
send_while: Arc::new(|_| true),
}
}
pub fn run<'a, F>(self, visit: F)
where
F: FnMut() -> FnVisitor<'a>,
{
self.inner.run(visit)
}
pub fn run_task(self) -> Receiver<WalkResult> {
let (tx, rx) = self.chan::<WalkResult>();
self.inner.run(|| {
let tx = tx.clone();
let quit_fn = self.quit_while.clone();
let send_fn = self.send_while.clone();
Box::new(move |result| {
if quit_fn(result.clone()) {
return WalkState::Quit;
}
if send_fn(result.clone()) {
tx.send(result.clone()).unwrap();
}
WalkState::Continue
})
});
rx
}
#[inline]
pub fn send_while<T>(&mut self, when: T) -> &mut Self
where
T: Fn(WalkResult) -> bool + Sync + Send + 'static,
{
self.send_while = Arc::new(when);
self
}
#[inline]
pub fn quit_while<T>(&mut self, when: T) -> &mut Self
where
T: Fn(WalkResult) -> bool + Sync + Send + 'static,
{
self.quit_while = Arc::new(when);
self
}
#[inline]
pub fn max_capacity(&mut self, limit: Option<usize>) -> &mut Self {
if limit.is_none() && self.max_capacity.is_none() {
return self;
}
self.max_capacity = limit;
self
}
#[inline]
fn chan<T>(&self) -> (Sender<T>, Receiver<T>) {
match &self.max_capacity {
None => crossbeam_channel::unbounded::<T>(),
Some(cap) => crossbeam_channel::bounded::<T>(*cap),
}
}
}
#[derive(Clone)]
pub struct WalkBuilder {
workspace_root: PathBuf,
walker_builder: InternalWalkBuilder,
override_builder: OverrideBuilder,
max_capacity: Option<usize>,
exclude: Vec<String>,
include: Vec<String>,
}
impl WalkBuilder {
pub fn new<P>(workspace_root: P) -> Self
where
P: AsRef<Path>,
{
let workspace_root = workspace_root.as_ref();
let walker_builder = InternalWalkBuilder::new(workspace_root);
let override_builder = OverrideBuilder::new(workspace_root);
Self {
walker_builder,
override_builder,
workspace_root: workspace_root.into(),
max_capacity: None,
exclude: vec![],
include: vec![],
}
}
pub fn build(mut self) -> Result<Walk> {
self.build_overrides()?;
let walk_parallel = self.walker_builder.build_parallel();
let walk = Walk::new(walk_parallel, self.max_capacity);
Ok(walk)
}
#[inline]
pub fn add_ignore<P>(&mut self, file_name: P) -> &Self
where
P: AsRef<OsStr>,
{
let file_path = &self.workspace_root().join(file_name.as_ref());
self.walker_builder.add_custom_ignore_filename(file_path);
self
}
#[inline]
pub fn disable_git_ignore(&mut self, yes: bool) -> &Self {
self.walker_builder.git_ignore(!yes);
self
}
pub fn workspace_root(&self) -> &Path {
self.workspace_root.as_ref()
}
pub fn max_capacity(&self) -> Option<usize> {
self.max_capacity
}
pub fn exclude<T>(&mut self, patterns: Option<Vec<T>>) -> Result<()>
where
T: 'static + AsRef<str>,
{
let patterns = patterns.unwrap_or_default();
if patterns.is_empty() {
return Ok(());
}
let mut patterns: Vec<String> = patterns
.iter()
.map(|p| switch_pattern_negation(p.as_ref()))
.collect();
self.exclude.append(&mut patterns);
Ok(())
}
pub fn include<T>(&mut self, patterns: Option<Vec<T>>) -> Result<()>
where
T: 'static + AsRef<str>,
{
let patterns = patterns.unwrap_or_default();
if patterns.is_empty() {
return Ok(());
}
let mut patterns: Vec<String> = patterns.iter().map(|p| p.as_ref().to_string()).collect();
self.include.append(&mut patterns);
Ok(())
}
fn build_overrides(&mut self) -> Result<()> {
if self.include.is_empty() && self.exclude.is_empty() {
return Ok(());
}
let patterns = match self.include.is_empty() {
true => &self.exclude,
false => &self.include,
};
for pattern in patterns {
self.override_builder.add(pattern)?;
}
let overrides = self.override_builder.build()?;
self.walker_builder.overrides(overrides);
Ok(())
}
}
#[inline]
fn switch_pattern_negation(pattern: &str) -> String {
pattern
.strip_prefix('!')
.map(|p| p.to_string())
.unwrap_or_else(|| format!("!{pattern}"))
}
#[cfg(test)]
mod tests {
use std::fs::File;
use super::*;
use crate::utils::testing::*;
use ignore::DirEntry;
use rayon::prelude::*;
use tempfile::{tempdir, TempDir};
fn create_test_builder() -> (TempDir, WalkBuilder) {
let dir = tempdir().unwrap();
let builder = WalkBuilder::new(&dir);
(dir, builder)
}
#[test]
fn test_walkbuilder_construction() {
let root_dir = PathBuf::from("my_workspace");
let builder = WalkBuilder::new(&root_dir);
assert_eq!(builder.workspace_root(), &root_dir);
}
#[test]
fn test_walkincludebuilder_construction() {
let root_dir = PathBuf::from("my_project");
let builder = WalkBuilder::new(&root_dir);
assert_eq!(builder.workspace_root(), &root_dir);
}
#[test]
fn test_walkexcludebuilder_construction() {
let root_dir = PathBuf::from("my_app");
let builder = WalkBuilder::new(&root_dir);
assert_eq!(builder.workspace_root(), &root_dir);
}
#[test]
fn test_walkbuilder_disable_git_ignore() {
let mut builder = WalkBuilder::new("my_dir");
builder.disable_git_ignore(true);
}
#[test]
fn test_walk_builder_add_ignore_file() {
let mut builder = WalkBuilder::new("my_codebase");
builder.add_ignore(".gitignore");
let expected_path = builder.workspace_root().join(".gitignore");
}
#[test]
fn test_walk_include_builder_add_overrides() {
let mut builder = WalkBuilder::new("my_repo");
builder
.include(Some(vec!["src/**/*.rs", "tests/**/*.rs"]))
.unwrap();
let expected_patterns = ["src/**/*.rs", "tests/**/*.rs"];
}
#[test]
fn test_walk_exclude_builder_add_overrides() {
let mut builder = WalkBuilder::new("my_lib");
builder
.exclude(Some(vec!["vendor/**", ".target/**"]))
.unwrap();
let expected_patterns = ["vendor/**", ".target/**"];
let overrides_res = builder.build_overrides();
assert!(overrides_res.is_ok());
}
#[test]
fn test_walk_builder_build() {
let builder = WalkBuilder::new("my_workspace");
let walk = builder.build();
assert!(walk.is_ok());
}
#[test]
fn test_walk_include_builder_build() {
let mut builder = WalkBuilder::new("my_root");
builder.include(Some(vec!["src/**/*.rs"])).unwrap();
let walk = builder.build();
assert!(walk.is_ok());
}
#[test]
fn test_walk_exclude_builder_build() {
let mut builder = WalkBuilder::new("my_project");
builder.exclude(Some(vec!["vendor/**"])).unwrap();
let walk = builder.build();
assert!(walk.is_ok());
}
#[test]
fn test_workspace_walk_run_task() {
let (tmp_dir, mut builder) = create_test_builder();
builder.add_ignore(".git");
let walker = builder.build().expect("Failed to build workspace walk");
let rx = walker.run_task();
}
#[test]
fn test_workspace_walk_send_while() {
let (tmp_dir, file_path) = create_temp_file("somefile.rs");
let builder = WalkBuilder::new(&tmp_dir);
let mut walker = builder
.clone()
.build()
.expect("Failed to build workspace walk");
let filter_file = |res: Result<DirEntry, ignore::Error>| {
res.is_ok() && res.unwrap().file_type().unwrap().is_file()
};
walker.send_while(filter_file);
let entries: Vec<DirEntry> = walker
.run_task()
.into_iter()
.par_bridge()
.into_par_iter()
.filter_map(Result::ok)
.collect();
assert!(entries.len() == 1);
drop(file_path);
tmp_dir.close().unwrap();
let tmp_dir = tempdir().unwrap();
let tmp_dir = tmp_dir.path();
let tmp_file_1 = tmp_dir.join("anotherfile.rs");
let tmp_file_2 = tmp_dir.join("yetanotherfile.rs");
File::create(tmp_file_1).unwrap();
File::create(tmp_file_2).unwrap();
let builder = WalkBuilder::new(tmp_dir);
let mut walker = builder.build().expect("Failed to build workspace walk");
walker.send_while(filter_file);
let entries: Vec<DirEntry> = walker
.run_task()
.into_iter()
.par_bridge()
.into_par_iter()
.filter_map(Result::ok)
.collect();
assert!(entries.len() == 2);
}
#[test]
fn test_workspace_walk_quit_while() {
let (tmp_dir, builder) = create_test_builder();
let mut walker = builder.build().expect("Failed to build workspace walk");
walker.quit_while(|_result| true);
let rx = walker.run_task();
}
#[test]
fn test_workspace_walk_with_invalid_ignore() {
let (tmp_dir, mut builder) = create_test_builder();
builder.add_ignore("nonexistent_ignore_file");
let result = builder.build();
assert!(result.is_ok());
}
#[test]
fn test_workspace_walk_with_disable_git_ignore() {
let (tmp_dir, mut builder) = create_test_builder();
builder.disable_git_ignore(true);
let walker = builder.build().expect("Failed to build workspace walk");
let rx = walker.run_task();
}
}