context_builder/
file_utils.rs1use ignore::{DirEntry, WalkBuilder, overrides::OverrideBuilder};
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 walker = WalkBuilder::new(base_path);
13 let mut override_builder = OverrideBuilder::new(base_path);
17 for pattern in ignores {
18 let ignore_pattern = format!("!{}", pattern);
23 if let Err(e) = override_builder.add(&ignore_pattern) {
24 return Err(io::Error::new(
25 io::ErrorKind::InvalidInput,
26 format!("Invalid ignore pattern '{}': {}", pattern, e),
27 ));
28 }
29 }
30 if let Err(e) = override_builder.add("!context-builder.toml") {
32 return Err(io::Error::new(
33 io::ErrorKind::InvalidInput,
34 format!("Failed to add config ignore: {}", e),
35 ));
36 }
37
38 let overrides = override_builder.build().map_err(|e| {
39 io::Error::new(
40 io::ErrorKind::InvalidInput,
41 format!("Failed to build overrides: {}", e),
42 )
43 })?;
44 walker.overrides(overrides);
45
46 if !filters.is_empty() {
47 let mut type_builder = ignore::types::TypesBuilder::new();
48 type_builder.add_defaults();
49 for filter in filters {
50 let _ = type_builder.add(filter, &format!("*.{}", filter));
51 type_builder.select(filter);
52 }
53 let types = type_builder.build().unwrap();
54 walker.types(types);
55 }
56
57 let mut files: Vec<DirEntry> = walker
58 .build()
59 .filter_map(Result::ok)
60 .filter(|e| e.file_type().is_some_and(|ft| ft.is_file()))
61 .collect();
62
63 files.sort_by(|a, b| a.path().cmp(b.path()));
65
66 Ok(files)
67}
68
69pub fn confirm_processing(file_count: usize) -> io::Result<bool> {
71 if file_count > 100 {
72 print!(
73 "Warning: You're about to process {} files. This might take a while. Continue? [y/N] ",
74 file_count
75 );
76 io::stdout().flush()?;
77 let mut input = String::new();
78 io::stdin().read_line(&mut input)?;
79 if !input.trim().eq_ignore_ascii_case("y") {
80 return Ok(false);
81 }
82 }
83 Ok(true)
84}
85
86pub fn confirm_overwrite(file_path: &str) -> io::Result<bool> {
88 print!("The file '{}' already exists. Overwrite? [y/N] ", file_path);
89 io::stdout().flush()?;
90 let mut input = String::new();
91 io::stdin().read_line(&mut input)?;
92
93 if input.trim().eq_ignore_ascii_case("y") {
94 Ok(true)
95 } else {
96 Ok(false)
97 }
98}
99
100pub fn find_latest_file(dir: &Path) -> io::Result<Option<PathBuf>> {
101 if !dir.is_dir() {
102 return Ok(None);
103 }
104
105 let mut latest_file = None;
106 let mut latest_time = std::time::SystemTime::UNIX_EPOCH;
107
108 for entry in fs::read_dir(dir)? {
109 let entry = entry?;
110 let path = entry.path();
111 if path.is_file() {
112 let metadata = fs::metadata(&path)?;
113 let modified = metadata.modified()?;
114 if modified > latest_time {
115 latest_time = modified;
116 latest_file = Some(path);
117 }
118 }
119 }
120
121 Ok(latest_file)
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use std::fs;
128 use std::path::Path;
129 use tempfile::tempdir;
130
131 fn to_rel_paths(mut entries: Vec<DirEntry>, base: &Path) -> Vec<String> {
132 entries.sort_by_key(|e| e.path().to_path_buf());
133 entries
134 .iter()
135 .map(|e| {
136 e.path()
137 .strip_prefix(base)
138 .unwrap()
139 .to_string_lossy()
140 .replace('\\', "/")
141 })
142 .collect()
143 }
144
145 #[test]
146 fn collect_files_respects_filters() {
147 let dir = tempdir().unwrap();
148 let base = dir.path();
149
150 fs::create_dir_all(base.join("src")).unwrap();
152 fs::create_dir_all(base.join("scripts")).unwrap();
153 fs::write(base.join("src").join("main.rs"), "fn main() {}").unwrap();
154 fs::write(base.join("Cargo.toml"), "[package]\nname=\"x\"").unwrap();
155 fs::write(base.join("README.md"), "# readme").unwrap();
156 fs::write(base.join("scripts").join("build.sh"), "#!/bin/sh\n").unwrap();
157
158 let filters = vec!["rs".to_string(), "toml".to_string()];
159 let ignores: Vec<String> = vec![];
160
161 let files = collect_files(base, &filters, &ignores).unwrap();
162 let relative_paths = to_rel_paths(files, base);
163
164 assert!(relative_paths.contains(&"src/main.rs".to_string()));
165 assert!(relative_paths.contains(&"Cargo.toml".to_string()));
166 assert!(!relative_paths.contains(&"README.md".to_string()));
167 assert!(!relative_paths.contains(&"scripts/build.sh".to_string()));
168 }
169
170 #[test]
171 fn collect_files_respects_ignores_for_dirs_and_files() {
172 let dir = tempdir().unwrap();
173 let base = dir.path();
174
175 fs::create_dir_all(base.join("src")).unwrap();
176 fs::create_dir_all(base.join("target")).unwrap();
177 fs::create_dir_all(base.join("node_modules")).unwrap();
178
179 fs::write(base.join("src").join("main.rs"), "fn main() {}").unwrap();
180 fs::write(base.join("target").join("artifact.txt"), "bin").unwrap();
181 fs::write(base.join("node_modules").join("pkg.js"), "console.log();").unwrap();
182 fs::write(base.join("README.md"), "# readme").unwrap();
183
184 let filters: Vec<String> = vec![];
185 let ignores: Vec<String> = vec!["target".into(), "node_modules".into(), "README.md".into()];
186
187 let files = collect_files(base, &filters, &ignores).unwrap();
188 let relative_paths = to_rel_paths(files, base);
189
190 assert!(relative_paths.contains(&"src/main.rs".to_string()));
191 assert!(!relative_paths.contains(&"target/artifact.txt".to_string()));
192 assert!(!relative_paths.contains(&"node_modules/pkg.js".to_string()));
193 assert!(!relative_paths.contains(&"README.md".to_string()));
194 }
195
196 #[test]
197 fn collect_files_handles_invalid_ignore_pattern() {
198 let dir = tempdir().unwrap();
199 let base = dir.path();
200
201 fs::create_dir_all(base.join("src")).unwrap();
202 fs::write(base.join("src").join("main.rs"), "fn main() {}").unwrap();
203
204 let filters: Vec<String> = vec![];
205 let ignores: Vec<String> = vec!["[".into()]; let result = collect_files(base, &filters, &ignores);
208 assert!(result.is_err());
209 assert!(
210 result
211 .unwrap_err()
212 .to_string()
213 .contains("Invalid ignore pattern")
214 );
215 }
216
217 #[test]
218 fn collect_files_empty_directory() {
219 let dir = tempdir().unwrap();
220 let base = dir.path();
221
222 let filters: Vec<String> = vec![];
223 let ignores: Vec<String> = vec![];
224
225 let files = collect_files(base, &filters, &ignores).unwrap();
226 assert!(files.is_empty());
227 }
228
229 #[test]
230 fn collect_files_no_matching_filters() {
231 let dir = tempdir().unwrap();
232 let base = dir.path();
233
234 fs::write(base.join("README.md"), "# readme").unwrap();
235 fs::write(base.join("script.py"), "print('hello')").unwrap();
236
237 let filters = vec!["rs".to_string()]; let ignores: Vec<String> = vec![];
239
240 let files = collect_files(base, &filters, &ignores).unwrap();
241 assert!(files.is_empty());
242 }
243
244 #[test]
245 fn collect_files_ignores_config_file() {
246 let dir = tempdir().unwrap();
247 let base = dir.path();
248
249 fs::write(base.join("context-builder.toml"), "[config]").unwrap();
250 fs::write(base.join("other.toml"), "[other]").unwrap();
251
252 let filters: Vec<String> = vec![];
253 let ignores: Vec<String> = vec![];
254
255 let files = collect_files(base, &filters, &ignores).unwrap();
256 let relative_paths = to_rel_paths(files, base);
257
258 assert!(!relative_paths.contains(&"context-builder.toml".to_string()));
259 assert!(relative_paths.contains(&"other.toml".to_string()));
260 }
261
262 #[test]
263 fn confirm_processing_small_count() {
264 let result = confirm_processing(50);
266 assert!(result.is_ok());
267 assert!(result.unwrap());
268 }
269
270 #[test]
271 fn find_latest_file_empty_directory() {
272 let dir = tempdir().unwrap();
273 let result = find_latest_file(dir.path()).unwrap();
274 assert!(result.is_none());
275 }
276
277 #[test]
278 fn find_latest_file_nonexistent_directory() {
279 let dir = tempdir().unwrap();
280 let nonexistent = dir.path().join("nonexistent");
281 let result = find_latest_file(&nonexistent).unwrap();
282 assert!(result.is_none());
283 }
284
285 #[test]
286 fn find_latest_file_single_file() {
287 let dir = tempdir().unwrap();
288 let file_path = dir.path().join("test.txt");
289 fs::write(&file_path, "content").unwrap();
290
291 let result = find_latest_file(dir.path()).unwrap();
292 assert!(result.is_some());
293 assert_eq!(result.unwrap(), file_path);
294 }
295
296 #[test]
297 fn find_latest_file_multiple_files() {
298 let dir = tempdir().unwrap();
299
300 let file1 = dir.path().join("old.txt");
301 let file2 = dir.path().join("new.txt");
302
303 fs::write(&file1, "old content").unwrap();
304 std::thread::sleep(std::time::Duration::from_millis(10));
305 fs::write(&file2, "new content").unwrap();
306
307 let result = find_latest_file(dir.path()).unwrap();
308 assert!(result.is_some());
309 assert_eq!(result.unwrap(), file2);
310 }
311
312 #[test]
313 fn find_latest_file_ignores_directories() {
314 let dir = tempdir().unwrap();
315 let subdir = dir.path().join("subdir");
316 fs::create_dir(&subdir).unwrap();
317
318 let file_path = dir.path().join("test.txt");
319 fs::write(&file_path, "content").unwrap();
320
321 let result = find_latest_file(dir.path()).unwrap();
322 assert!(result.is_some());
323 assert_eq!(result.unwrap(), file_path);
324 }
325
326 #[test]
327 fn test_confirm_processing_requires_user_interaction() {
328 use std::io::Cursor;
338
339 let input = b"y\n";
341 let _ = Cursor::new(input);
342
343 let result = confirm_processing(50);
346 assert!(result.is_ok());
347 assert!(result.unwrap());
348 }
349
350 #[test]
351 fn test_confirm_overwrite_function_exists() {
352 let _: fn(&str) -> std::io::Result<bool> = confirm_overwrite;
364 }
365
366 #[test]
367 fn test_collect_files_handles_permission_errors() {
368 let dir = tempdir().unwrap();
371 let base = dir.path();
372
373 let filters: Vec<String> = vec![];
375 let ignores: Vec<String> = vec!["[invalid".into()]; let result = collect_files(base, &filters, &ignores);
378 assert!(result.is_err());
379 }
380
381 #[test]
382 fn test_find_latest_file_permission_error() {
383 use std::path::Path;
385
386 let nonexistent = Path::new("/this/path/should/not/exist/anywhere");
388 let result = find_latest_file(nonexistent);
389
390 assert!(result.is_ok());
392 assert!(result.unwrap().is_none());
393 }
394
395 #[test]
396 fn test_collect_files_with_symlinks() {
397 let dir = tempdir().unwrap();
399 let base = dir.path();
400
401 fs::write(base.join("regular.txt"), "content").unwrap();
403
404 #[cfg(unix)]
406 {
407 use std::os::unix::fs::symlink;
408 let _ = symlink("regular.txt", base.join("link.txt"));
409 }
410
411 #[cfg(windows)]
413 {
414 fs::write(base.join("another.txt"), "content2").unwrap();
416 }
417
418 let filters: Vec<String> = vec![];
419 let ignores: Vec<String> = vec![];
420
421 let files = collect_files(base, &filters, &ignores).unwrap();
422 assert!(!files.is_empty());
424 }
425}