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