code_analyze_mcp/
traversal.rs1use 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 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#[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 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}