Skip to main content

walker/
walk.rs

1use crate::filter::{apply_walk_builder, FilterOptions};
2use std::path::PathBuf;
3use std::sync::atomic::{AtomicUsize, Ordering};
4use std::sync::Arc;
5use thiserror::Error;
6use walkdir::WalkDir;
7
8#[derive(Debug, Clone, Copy, Default)]
9pub enum WalkMode {
10    #[default]
11    Standard,
12    Full,
13}
14
15#[derive(Debug, Error)]
16pub enum WalkError {
17    #[error(transparent)]
18    Ignore(#[from] ignore::Error),
19    #[error(transparent)]
20    Walkdir(#[from] walkdir::Error),
21}
22
23pub struct WalkOutcome {
24    pub files_seen: usize,
25}
26
27/// Walk each root in order, invoking `on_file` for every file in parallel (rayon workers per `ignore`).
28pub fn walk_roots_fn(
29    roots: &[PathBuf],
30    opts: &FilterOptions,
31    mode: WalkMode,
32    on_file: impl Fn(PathBuf) + Sync + Send + Clone + 'static,
33) -> Result<WalkOutcome, WalkError> {
34    let files_seen = Arc::new(AtomicUsize::new(0));
35
36    match mode {
37        WalkMode::Standard => {
38            for root in roots {
39                let wb = apply_walk_builder(root, opts)?;
40                let walker = wb.build_parallel();
41                let seen = Arc::clone(&files_seen);
42
43                walker.run({
44                    let on_file = on_file.clone();
45                    move || {
46                        let on_file = on_file.clone();
47                        let seen = Arc::clone(&seen);
48                        Box::new(move |res| {
49                            if let Ok(entry) = res {
50                                if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
51                                    seen.fetch_add(1, Ordering::Relaxed);
52                                    on_file(entry.path().to_path_buf());
53                                }
54                            }
55                            ignore::WalkState::Continue
56                        })
57                    }
58                });
59            }
60        }
61        WalkMode::Full => {
62            for root in roots {
63                for entry in WalkDir::new(root)
64                    .follow_links(false)
65                    .into_iter()
66                    .filter_map(Result::ok)
67                {
68                    if entry.file_type().is_file() {
69                        files_seen.fetch_add(1, Ordering::Relaxed);
70                        on_file(entry.path().to_path_buf());
71                    }
72                }
73            }
74        }
75    }
76
77    Ok(WalkOutcome {
78        files_seen: files_seen.load(Ordering::SeqCst),
79    })
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use std::collections::HashSet;
86    use std::fs;
87    use std::path::PathBuf;
88    use std::sync::{Arc, Mutex};
89    use tempfile::tempdir;
90
91    #[test]
92    fn standard_skips_gitignored_and_full_includes() {
93        let dir = tempdir().expect("temp dir");
94        let root = dir.path();
95        fs::write(root.join(".ignore"), "node_modules/\n").expect("write ignore");
96        fs::create_dir_all(root.join("node_modules")).expect("create node_modules");
97        fs::write(root.join("node_modules").join("a.js"), "x").expect("write ignored file");
98        fs::write(root.join("keep.txt"), "k").expect("write keep file");
99
100        let roots = vec![root.to_path_buf()];
101        let opts = FilterOptions::default();
102        let standard_paths: Arc<Mutex<HashSet<PathBuf>>> = Arc::new(Mutex::new(HashSet::new()));
103        let standard_paths_ref = Arc::clone(&standard_paths);
104        walk_roots_fn(&roots, &opts, WalkMode::Standard, move |p| {
105            standard_paths_ref.lock().expect("lock").insert(p);
106        })
107        .expect("standard walk");
108
109        let full_paths: Arc<Mutex<HashSet<PathBuf>>> = Arc::new(Mutex::new(HashSet::new()));
110        let full_paths_ref = Arc::clone(&full_paths);
111        walk_roots_fn(&roots, &opts, WalkMode::Full, move |p| {
112            full_paths_ref.lock().expect("lock").insert(p);
113        })
114        .expect("full walk");
115
116        let standard_paths = standard_paths.lock().expect("lock");
117        let full_paths = full_paths.lock().expect("lock");
118        assert!(standard_paths.contains(&root.join("keep.txt")));
119        assert!(!standard_paths.contains(&root.join("node_modules").join("a.js")));
120        assert!(full_paths.contains(&root.join("keep.txt")));
121        assert!(full_paths.contains(&root.join("node_modules").join("a.js")));
122    }
123}