context_builder/
file_utils.rs

1use ignore::{DirEntry, WalkBuilder};
2use std::io::{self, Write};
3use std::path::Path;
4
5/// Collects all files to be processed using `ignore` crate for efficient traversal.
6pub fn collect_files(
7    base_path: &Path,
8    filters: &[String],
9    ignores: &[String],
10) -> io::Result<Vec<DirEntry>> {
11    let mut walker = WalkBuilder::new(base_path);
12    // By default, the "ignore" crate respects .gitignore and hidden files, so we don't need walker.hidden(false)
13
14    // Apply custom ignore filtering later during iteration since `add_ignore` expects file paths to ignore files, not patterns.
15
16    if !filters.is_empty() {
17        let mut type_builder = ignore::types::TypesBuilder::new();
18        type_builder.add_defaults();
19        for filter in filters {
20            let _ = type_builder.add(filter, &format!("*.{}", filter));
21            type_builder.select(filter);
22        }
23        let types = type_builder.build().unwrap();
24        walker.types(types);
25    }
26
27    Ok(walker
28        .build()
29        .filter_map(Result::ok)
30        .filter(|e| e.file_type().is_some_and(|ft| ft.is_file()))
31        .filter(|e| {
32            let path = e.path();
33            // Exclude any entry that contains an ignored directory or file name as a path component
34            !path.components().any(|c| {
35                let comp = c.as_os_str();
36                ignores
37                    .iter()
38                    .any(|name| comp == std::ffi::OsStr::new(name))
39            })
40        })
41        .collect())
42}
43
44/// Asks for user confirmation if the number of files is large.
45pub fn confirm_processing(file_count: usize) -> io::Result<bool> {
46    if file_count > 100 {
47        print!(
48            "Warning: You're about to process {} files. This might take a while. Continue? [y/N] ",
49            file_count
50        );
51        io::stdout().flush()?;
52        let mut input = String::new();
53        io::stdin().read_line(&mut input)?;
54        if !input.trim().eq_ignore_ascii_case("y") {
55            return Ok(false);
56        }
57    }
58    Ok(true)
59}
60
61/// Asks for user confirmation to overwrite an existing file.
62pub fn confirm_overwrite(file_path: &str) -> io::Result<bool> {
63    print!("The file '{}' already exists. Overwrite? [y/N] ", file_path);
64    io::stdout().flush()?;
65    let mut input = String::new();
66    io::stdin().read_line(&mut input)?;
67
68    if input.trim().eq_ignore_ascii_case("y") {
69        Ok(true)
70    } else {
71        Ok(false)
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use std::fs;
79    use std::path::Path;
80    use tempfile::tempdir;
81
82    fn to_rel_paths(mut entries: Vec<DirEntry>, base: &Path) -> Vec<String> {
83        entries.sort_by_key(|e| e.path().to_path_buf());
84        entries
85            .iter()
86            .map(|e| {
87                e.path()
88                    .strip_prefix(base)
89                    .unwrap()
90                    .to_string_lossy()
91                    .replace('\\', "/")
92            })
93            .collect()
94    }
95
96    #[test]
97    fn collect_files_respects_filters() {
98        let dir = tempdir().unwrap();
99        let base = dir.path();
100
101        // create files
102        fs::create_dir_all(base.join("src")).unwrap();
103        fs::create_dir_all(base.join("scripts")).unwrap();
104        fs::write(base.join("src").join("main.rs"), "fn main() {}").unwrap();
105        fs::write(base.join("Cargo.toml"), "[package]\nname=\"x\"").unwrap();
106        fs::write(base.join("README.md"), "# readme").unwrap();
107        fs::write(base.join("scripts").join("build.sh"), "#!/bin/sh\n").unwrap();
108
109        let filters = vec!["rs".to_string(), "toml".to_string()];
110        let ignores: Vec<String> = vec![];
111
112        let files = collect_files(base, &filters, &ignores).unwrap();
113        let relative_paths = to_rel_paths(files, base);
114
115        assert!(relative_paths.contains(&"src/main.rs".to_string()));
116        assert!(relative_paths.contains(&"Cargo.toml".to_string()));
117        assert!(!relative_paths.contains(&"README.md".to_string()));
118        assert!(!relative_paths.contains(&"scripts/build.sh".to_string()));
119    }
120
121    #[test]
122    fn collect_files_respects_ignores_for_dirs_and_files() {
123        let dir = tempdir().unwrap();
124        let base = dir.path();
125
126        fs::create_dir_all(base.join("src")).unwrap();
127        fs::create_dir_all(base.join("target")).unwrap();
128        fs::create_dir_all(base.join("node_modules")).unwrap();
129
130        fs::write(base.join("src").join("main.rs"), "fn main() {}").unwrap();
131        fs::write(base.join("target").join("artifact.txt"), "bin").unwrap();
132        fs::write(base.join("node_modules").join("pkg.js"), "console.log();").unwrap();
133        fs::write(base.join("README.md"), "# readme").unwrap();
134
135        let filters: Vec<String> = vec![];
136        let ignores: Vec<String> = vec!["target".into(), "node_modules".into(), "README.md".into()];
137
138        let files = collect_files(base, &filters, &ignores).unwrap();
139        let relative_paths = to_rel_paths(files, base);
140
141        assert!(relative_paths.contains(&"src/main.rs".to_string()));
142        assert!(!relative_paths.contains(&"target/artifact.txt".to_string()));
143        assert!(!relative_paths.contains(&"node_modules/pkg.js".to_string()));
144        assert!(!relative_paths.contains(&"README.md".to_string()));
145    }
146}