context_builder/
file_utils.rs1use ignore::{DirEntry, WalkBuilder};
2use std::fs;
3use std::io::{self, Write};
4use std::path::{Path, PathBuf};
5
6pub fn collect_files(
8 base_path: &Path,
9 filters: &[String],
10 ignores: &[String],
11) -> io::Result<Vec<DirEntry>> {
12 let mut ignores = ignores.to_vec();
13 ignores.push(".context-builder.toml".to_string());
14
15 let mut walker = WalkBuilder::new(base_path);
16 if !filters.is_empty() {
21 let mut type_builder = ignore::types::TypesBuilder::new();
22 type_builder.add_defaults();
23 for filter in filters {
24 let _ = type_builder.add(filter, &format!("*.{}", filter));
25 type_builder.select(filter);
26 }
27 let types = type_builder.build().unwrap();
28 walker.types(types);
29 }
30
31 Ok(walker
32 .build()
33 .filter_map(Result::ok)
34 .filter(|e| e.file_type().is_some_and(|ft| ft.is_file()))
35 .filter(|e| {
36 let path = e.path();
37 !path.components().any(|c| {
39 let comp = c.as_os_str();
40 ignores
41 .iter()
42 .any(|name| comp == std::ffi::OsStr::new(name))
43 })
44 })
45 .collect())
46}
47
48pub fn confirm_processing(file_count: usize) -> io::Result<bool> {
50 if file_count > 100 {
51 print!(
52 "Warning: You're about to process {} files. This might take a while. Continue? [y/N] ",
53 file_count
54 );
55 io::stdout().flush()?;
56 let mut input = String::new();
57 io::stdin().read_line(&mut input)?;
58 if !input.trim().eq_ignore_ascii_case("y") {
59 return Ok(false);
60 }
61 }
62 Ok(true)
63}
64
65pub fn confirm_overwrite(file_path: &str) -> io::Result<bool> {
67 print!("The file '{}' already exists. Overwrite? [y/N] ", file_path);
68 io::stdout().flush()?;
69 let mut input = String::new();
70 io::stdin().read_line(&mut input)?;
71
72 if input.trim().eq_ignore_ascii_case("y") {
73 Ok(true)
74 } else {
75 Ok(false)
76 }
77}
78
79pub fn find_latest_file(dir: &Path) -> io::Result<Option<PathBuf>> {
80 if !dir.is_dir() {
81 return Ok(None);
82 }
83
84 let mut latest_file = None;
85 let mut latest_time = std::time::SystemTime::UNIX_EPOCH;
86
87 for entry in fs::read_dir(dir)? {
88 let entry = entry?;
89 let path = entry.path();
90 if path.is_file() {
91 let metadata = fs::metadata(&path)?;
92 let modified = metadata.modified()?;
93 if modified > latest_time {
94 latest_time = modified;
95 latest_file = Some(path);
96 }
97 }
98 }
99
100 Ok(latest_file)
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use std::fs;
107 use std::path::Path;
108 use tempfile::tempdir;
109
110 fn to_rel_paths(mut entries: Vec<DirEntry>, base: &Path) -> Vec<String> {
111 entries.sort_by_key(|e| e.path().to_path_buf());
112 entries
113 .iter()
114 .map(|e| {
115 e.path()
116 .strip_prefix(base)
117 .unwrap()
118 .to_string_lossy()
119 .replace('\\', "/")
120 })
121 .collect()
122 }
123
124 #[test]
125 fn collect_files_respects_filters() {
126 let dir = tempdir().unwrap();
127 let base = dir.path();
128
129 fs::create_dir_all(base.join("src")).unwrap();
131 fs::create_dir_all(base.join("scripts")).unwrap();
132 fs::write(base.join("src").join("main.rs"), "fn main() {}").unwrap();
133 fs::write(base.join("Cargo.toml"), "[package]\nname=\"x\"").unwrap();
134 fs::write(base.join("README.md"), "# readme").unwrap();
135 fs::write(base.join("scripts").join("build.sh"), "#!/bin/sh\n").unwrap();
136
137 let filters = vec!["rs".to_string(), "toml".to_string()];
138 let ignores: Vec<String> = vec![];
139
140 let files = collect_files(base, &filters, &ignores).unwrap();
141 let relative_paths = to_rel_paths(files, base);
142
143 assert!(relative_paths.contains(&"src/main.rs".to_string()));
144 assert!(relative_paths.contains(&"Cargo.toml".to_string()));
145 assert!(!relative_paths.contains(&"README.md".to_string()));
146 assert!(!relative_paths.contains(&"scripts/build.sh".to_string()));
147 }
148
149 #[test]
150 fn collect_files_respects_ignores_for_dirs_and_files() {
151 let dir = tempdir().unwrap();
152 let base = dir.path();
153
154 fs::create_dir_all(base.join("src")).unwrap();
155 fs::create_dir_all(base.join("target")).unwrap();
156 fs::create_dir_all(base.join("node_modules")).unwrap();
157
158 fs::write(base.join("src").join("main.rs"), "fn main() {}").unwrap();
159 fs::write(base.join("target").join("artifact.txt"), "bin").unwrap();
160 fs::write(base.join("node_modules").join("pkg.js"), "console.log();").unwrap();
161 fs::write(base.join("README.md"), "# readme").unwrap();
162
163 let filters: Vec<String> = vec![];
164 let ignores: Vec<String> = vec!["target".into(), "node_modules".into(), "README.md".into()];
165
166 let files = collect_files(base, &filters, &ignores).unwrap();
167 let relative_paths = to_rel_paths(files, base);
168
169 assert!(relative_paths.contains(&"src/main.rs".to_string()));
170 assert!(!relative_paths.contains(&"target/artifact.txt".to_string()));
171 assert!(!relative_paths.contains(&"node_modules/pkg.js".to_string()));
172 assert!(!relative_paths.contains(&"README.md".to_string()));
173 }
174}