Skip to main content

code_analyze_mcp/
traversal.rs

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