Skip to main content

code_analyze_mcp/
traversal.rs

1//! Directory traversal with .gitignore support.
2//!
3//! Provides recursive directory walking with automatic filtering based on `.gitignore` and `.ignore` files.
4//! Uses the `ignore` crate for cross-platform, efficient file system traversal.
5
6use ignore::WalkBuilder;
7use std::path::{Path, PathBuf};
8use std::time::Instant;
9use thiserror::Error;
10use tracing::instrument;
11
12#[derive(Debug, Clone)]
13pub struct WalkEntry {
14    pub path: PathBuf,
15    /// Depth in the directory tree (0 = root).
16    pub depth: usize,
17    pub is_dir: bool,
18    pub is_symlink: bool,
19    pub symlink_target: Option<PathBuf>,
20}
21
22#[derive(Debug, Error)]
23pub enum TraversalError {
24    #[error("IO error: {0}")]
25    Io(#[from] std::io::Error),
26}
27
28/// Walk a directory with support for .gitignore and .ignore.
29/// max_depth=0 maps to unlimited recursion (None), positive values limit depth.
30#[instrument(skip_all, fields(path = %root.display(), max_depth))]
31pub fn walk_directory(
32    root: &Path,
33    max_depth: Option<u32>,
34) -> Result<Vec<WalkEntry>, TraversalError> {
35    let start = Instant::now();
36    let mut builder = WalkBuilder::new(root);
37    builder.hidden(true).standard_filters(true);
38
39    // Map max_depth: 0 = unlimited (None), positive = Some(n)
40    if let Some(depth) = max_depth
41        && depth > 0
42    {
43        builder.max_depth(Some(depth as usize));
44    }
45
46    let mut entries = Vec::new();
47
48    for result in builder.build() {
49        match result {
50            Ok(entry) => {
51                let path = entry.path().to_path_buf();
52                let depth = entry.depth();
53                let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
54                let is_symlink = entry.path_is_symlink();
55
56                let symlink_target = if is_symlink {
57                    std::fs::read_link(&path).ok()
58                } else {
59                    None
60                };
61
62                entries.push(WalkEntry {
63                    path,
64                    depth,
65                    is_dir,
66                    is_symlink,
67                    symlink_target,
68                });
69            }
70            Err(e) => {
71                tracing::warn!(error = %e, "skipping unreadable entry");
72                continue;
73            }
74        }
75    }
76
77    let dir_count = entries.iter().filter(|e| e.is_dir).count();
78    let file_count = entries.iter().filter(|e| !e.is_dir).count();
79
80    tracing::debug!(
81        entries = entries.len(),
82        dirs = dir_count,
83        files = file_count,
84        duration_ms = start.elapsed().as_millis() as u64,
85        "walk complete"
86    );
87
88    Ok(entries)
89}