1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#![expect(
unused_results,
reason = "WalkBuilder configuration uses fluent setters only for their mutation side effects."
)]
//! Shared directory walker helpers built on the `ignore` crate.
//!
//! All file traversal in vtcode should go through these builders so that
//! `.gitignore`, `.ignore`, `.git/exclude`, and the centralized exclusion
//! constants are applied consistently.
use ignore::{DirEntry, WalkBuilder};
use std::path::Path;
use crate::exclusions::{DEFAULT_EXCLUDED_DIRS, VTCODE_IGNORE_FILE};
/// Build a multi-threaded [`WalkBuilder`] with sensible defaults.
///
/// - Respects `.gitignore`, `.ignore`, `.git/exclude`, and parent ignore files
/// - Does not follow symlinks
/// - Uses the `ignore` crate's default thread pool
///
/// Callers that need to prune additional directories should use
/// [`filter_entry`](WalkBuilder::filter_entry) with [`is_excluded_dir`].
pub fn build_default_walker(root: &Path) -> WalkBuilder {
let mut builder = WalkBuilder::new(root);
apply_defaults(&mut builder);
builder
}
/// Build a single-threaded [`WalkBuilder`] with the same defaults as
/// [`build_default_walker`].
///
/// Use this in synchronous contexts where spawning the `ignore` crate's
/// thread pool would be wasteful (e.g., inside `spawn_blocking` closures
/// that already run on a dedicated thread).
pub fn build_walker_single_threaded(root: &Path) -> WalkBuilder {
let mut builder = WalkBuilder::new(root);
builder.threads(1);
apply_defaults(&mut builder);
builder
}
/// Apply standard walker defaults to an existing [`WalkBuilder`].
///
/// Sets gitignore support, hidden file visibility, and symlink policy.
/// Callers that need additional customization (e.g., parallel walkers,
/// symlink following) can call this then override specific settings.
pub fn apply_defaults(builder: &mut WalkBuilder) {
// Respect all standard ignore-file mechanisms.
builder.git_ignore(true);
builder.git_global(true);
builder.git_exclude(true);
builder.ignore(true);
builder.parents(true);
// `.vtcodegitignore` mirrors `.gitignore` but is scoped to VT Code's own
// file operations. It has higher precedence than the standard ignore files
// (including its `!` re-include rules), so a user can whitelist a path that
// `.gitignore` prunes.
builder.add_custom_ignore_filename(VTCODE_IGNORE_FILE);
// Do not follow symlinks by default.
builder.follow_links(false);
// Do not skip hidden files by default. The `ignore` crate skips them
// by default, but the previous traversal code did not. Callers that
// want to hide dotfiles should filter them explicitly.
builder.hidden(false);
}
/// Returns `true` if `entry` is a directory whose name appears in
/// [`DEFAULT_EXCLUDED_DIRS`].
///
/// Intended for use inside [`WalkBuilder::filter_entry`] closures:
///
/// ```ignore
/// builder.filter_entry(|entry| !vtcode_commons::walk::is_excluded_dir(entry));
/// ```
pub fn is_excluded_dir(entry: &DirEntry) -> bool {
if !entry.file_type().is_some_and(|ft| ft.is_dir()) {
return false;
}
entry
.file_name()
.to_str()
.is_some_and(|name| DEFAULT_EXCLUDED_DIRS.contains(&name))
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;
use super::*;
fn collected_paths(root: &Path) -> Vec<String> {
let walker = build_default_walker(root).build();
let mut paths = walker
.filter_map(Result::ok)
.filter(|entry| entry.path() != root)
.map(|entry| {
entry
.path()
.strip_prefix(root)
.unwrap_or_else(|_| entry.path())
.to_string_lossy()
.into_owned()
})
.collect::<Vec<_>>();
paths.sort();
paths
}
#[test]
fn default_walker_respects_vtcodegitignore() {
let temp = tempdir().expect("tempdir");
let root = temp.path();
fs::write(root.join(".vtcodegitignore"), "ignored_dir/\nignored_file.txt\n").expect("write ignore file");
fs::create_dir(root.join("ignored_dir")).expect("mkdir ignored_dir");
fs::write(root.join("ignored_dir/secret.rs"), "x").expect("write secret");
fs::write(root.join("ignored_file.txt"), "x").expect("write ignored file");
fs::write(root.join("kept.rs"), "x").expect("write kept file");
let paths = collected_paths(root);
assert!(paths.contains(&"kept.rs".to_owned()), "kept file should remain: {paths:?}");
assert!(!paths.iter().any(|p| p.contains("ignored_file.txt")), "ignored file leaked: {paths:?}");
assert!(!paths.iter().any(|p| p.contains("ignored_dir")), "ignored dir leaked: {paths:?}");
}
#[test]
fn default_walker_vtcodegitignore_negation_reincludes() {
let temp = tempdir().expect("tempdir");
let root = temp.path();
// Exclude every log, then re-include one. The custom-ignore file's
// negation must win over its own earlier pattern (mirrors the repo's
// `!README.md` style allow-list).
fs::write(root.join(".vtcodegitignore"), "*.log\n!important.log\n").expect("write ignore file");
fs::write(root.join("important.log"), "x").expect("write important log");
fs::write(root.join("noise.log"), "x").expect("write noise log");
fs::write(root.join("kept.rs"), "x").expect("write kept file");
let paths = collected_paths(root);
assert!(paths.contains(&"important.log".to_owned()), "negated file should be re-included: {paths:?}");
assert!(!paths.iter().any(|p| p.ends_with("noise.log")), "non-negated file leaked: {paths:?}");
assert!(paths.contains(&"kept.rs".to_owned()), "unrelated file should remain: {paths:?}");
}
}