agpm_cli/utils/fs/
discovery.rs

1//! File discovery and search operations.
2//!
3//! This module provides utilities for finding files matching patterns
4//! in directory trees.
5
6use anyhow::Result;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10/// Recursively finds files matching a pattern in a directory tree.
11///
12/// This function performs a recursive search through the directory tree,
13/// matching files whose names contain the specified pattern. The search
14/// is case-sensitive and uses simple string matching (not regex).
15///
16/// # Arguments
17///
18/// * `dir` - The directory to search in
19/// * `pattern` - The pattern to match in filenames (substring match)
20///
21/// # Returns
22///
23/// A vector of [`PathBuf`]s for all matching files, or an error if the directory
24/// cannot be read.
25///
26/// # Examples
27///
28/// ```rust,no_run
29/// use agpm_cli::utils::fs::find_files;
30/// use std::path::Path;
31///
32/// # fn example() -> anyhow::Result<()> {
33/// // Find all Rust source files
34/// let rust_files = find_files(Path::new("src"), ".rs")?;
35///
36/// // Find all markdown files
37/// let md_files = find_files(Path::new("docs"), ".md")?;
38///
39/// // Find files with "test" in the name
40/// let test_files = find_files(Path::new("."), "test")?;
41/// # Ok(())
42/// # }
43/// ```
44///
45/// # Behavior
46///
47/// - Searches recursively through all subdirectories
48/// - Only returns regular files (not directories or symlinks)
49/// - Uses substring matching (case-sensitive)
50/// - Returns empty vector if no matches found
51/// - Continues searching even if some subdirectories are inaccessible
52///
53/// # Performance
54///
55/// For large directory trees or when searching for many different patterns,
56/// consider using external tools like `fd` or implementing caching for repeated searches.
57pub fn find_files(dir: &Path, pattern: &str) -> Result<Vec<PathBuf>> {
58    let mut files = Vec::new();
59    find_files_recursive(dir, pattern, &mut files)?;
60    Ok(files)
61}
62
63fn find_files_recursive(dir: &Path, pattern: &str, files: &mut Vec<PathBuf>) -> Result<()> {
64    if !dir.is_dir() {
65        return Ok(());
66    }
67
68    for entry in fs::read_dir(dir)? {
69        let entry = entry?;
70        let path = entry.path();
71
72        if path.is_dir() {
73            find_files_recursive(&path, pattern, files)?;
74        } else if path.is_file()
75            && let Some(name) = path.file_name()
76            && name.to_string_lossy().contains(pattern)
77        {
78            files.push(path);
79        }
80    }
81
82    Ok(())
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use tempfile::tempdir;
89
90    #[test]
91    fn test_find_files() {
92        let temp = tempdir().unwrap();
93        let root = temp.path();
94
95        // Create test files
96        std::fs::write(root.join("test.rs"), "").unwrap();
97        std::fs::write(root.join("main.rs"), "").unwrap();
98        crate::utils::fs::ensure_dir(&root.join("src")).unwrap();
99        std::fs::write(root.join("src/lib.rs"), "").unwrap();
100        std::fs::write(root.join("src/test.txt"), "").unwrap();
101
102        let files = find_files(root, ".rs").unwrap();
103        assert_eq!(files.len(), 3);
104
105        let files = find_files(root, "test").unwrap();
106        assert_eq!(files.len(), 2);
107    }
108
109    #[test]
110    fn test_find_files_with_patterns() {
111        let temp = tempdir().unwrap();
112        let root = temp.path();
113
114        // Create test files
115        std::fs::write(root.join("README.md"), "").unwrap();
116        std::fs::write(root.join("test.MD"), "").unwrap(); // Different case
117        std::fs::write(root.join("file.txt"), "").unwrap();
118        crate::utils::fs::ensure_dir(&root.join("hidden")).unwrap();
119        std::fs::write(root.join("hidden/.secret.md"), "").unwrap();
120
121        // Pattern matching
122        let files = find_files(root, ".md").unwrap();
123        assert_eq!(files.len(), 2); // README.md and .secret.md
124
125        let files = find_files(root, ".MD").unwrap();
126        assert_eq!(files.len(), 1); // test.MD
127
128        // Substring matching
129        let files = find_files(root, "test").unwrap();
130        assert_eq!(files.len(), 1);
131
132        let files = find_files(root, "secret").unwrap();
133        assert_eq!(files.len(), 1);
134    }
135}