vtcode_core/utils/
vtcodegitignore.rs1use anyhow::{Result, anyhow};
2use glob::Pattern;
3use std::path::{Path, PathBuf};
4use tokio::fs;
5
6#[derive(Debug, Clone)]
8pub struct VTCodeGitignore {
9 root_dir: PathBuf,
11 patterns: Vec<CompiledPattern>,
13 loaded: bool,
15}
16
17#[derive(Debug, Clone)]
19struct CompiledPattern {
20 original: String,
22 pattern: Pattern,
24 negated: bool,
26}
27
28impl VTCodeGitignore {
29 pub async fn new() -> Result<Self> {
31 let current_dir = std::env::current_dir()
32 .map_err(|e| anyhow!("Failed to get current directory: {}", e))?;
33
34 Self::from_directory(¤t_dir).await
35 }
36
37 pub async fn from_directory(root_dir: &Path) -> Result<Self> {
39 let gitignore_path = root_dir.join(".vtcodegitignore");
40
41 let mut patterns = Vec::new();
42 let mut loaded = false;
43
44 if gitignore_path.exists() {
45 match Self::load_patterns(&gitignore_path).await {
46 Ok(loaded_patterns) => {
47 patterns = loaded_patterns;
48 loaded = true;
49 }
50 Err(e) => {
51 eprintln!("Warning: Failed to load .vtcodegitignore: {}", e);
53 }
54 }
55 }
56
57 Ok(Self {
58 root_dir: root_dir.to_path_buf(),
59 patterns,
60 loaded,
61 })
62 }
63
64 async fn load_patterns(file_path: &Path) -> Result<Vec<CompiledPattern>> {
66 let content = fs::read_to_string(file_path)
67 .await
68 .map_err(|e| anyhow!("Failed to read .vtcodegitignore: {}", e))?;
69
70 let mut patterns = Vec::new();
71
72 for (line_num, line) in content.lines().enumerate() {
73 let line = line.trim();
74
75 if line.is_empty() || line.starts_with('#') {
77 continue;
78 }
79
80 let (pattern_str, negated) = if let Some(stripped) = line.strip_prefix('!') {
82 (stripped.to_string(), true)
83 } else {
84 (line.to_string(), false)
85 };
86
87 let glob_pattern = Self::convert_gitignore_to_glob(&pattern_str);
89
90 match Pattern::new(&glob_pattern) {
91 Ok(pattern) => {
92 patterns.push(CompiledPattern {
93 original: pattern_str,
94 pattern,
95 negated,
96 });
97 }
98 Err(e) => {
99 return Err(anyhow!(
100 "Invalid pattern on line {}: '{}': {}",
101 line_num + 1,
102 pattern_str,
103 e
104 ));
105 }
106 }
107 }
108
109 Ok(patterns)
110 }
111
112 fn convert_gitignore_to_glob(pattern: &str) -> String {
114 let mut result = pattern.to_string();
115
116 if result.ends_with('/') {
118 result = format!("{}/**", result.trim_end_matches('/'));
119 }
120
121 if !result.starts_with('/') && !result.starts_with("**/") && !result.contains('/') {
123 result = format!("**/{}", result);
125 }
126
127 result
128 }
129
130 pub fn should_exclude(&self, file_path: &Path) -> bool {
132 if !self.loaded || self.patterns.is_empty() {
133 return false;
134 }
135
136 let relative_path = match file_path.strip_prefix(&self.root_dir) {
138 Ok(rel) => rel,
139 Err(_) => {
140 file_path
142 }
143 };
144
145 let path_str = relative_path.to_string_lossy();
146
147 let mut excluded = false;
149
150 for pattern in &self.patterns {
151 if pattern.pattern.matches(&path_str) {
152 if pattern.original.ends_with('/') && file_path.is_file() {
153 continue;
155 }
156 if pattern.negated {
157 excluded = false;
159 } else {
160 excluded = true;
162 }
163 }
164 }
165
166 excluded
167 }
168
169 pub fn filter_paths(&self, paths: Vec<PathBuf>) -> Vec<PathBuf> {
171 if !self.loaded {
172 return paths;
173 }
174
175 paths
176 .into_iter()
177 .filter(|path| !self.should_exclude(path))
178 .collect()
179 }
180
181 pub fn is_loaded(&self) -> bool {
183 self.loaded
184 }
185
186 pub fn pattern_count(&self) -> usize {
188 self.patterns.len()
189 }
190
191 pub fn root_dir(&self) -> &Path {
193 &self.root_dir
194 }
195}
196
197impl Default for VTCodeGitignore {
198 fn default() -> Self {
199 Self {
200 root_dir: PathBuf::new(),
201 patterns: Vec::new(),
202 loaded: false,
203 }
204 }
205}
206
207static VTCODE_GITIGNORE: once_cell::sync::Lazy<tokio::sync::RwLock<VTCodeGitignore>> =
209 once_cell::sync::Lazy::new(|| tokio::sync::RwLock::new(VTCodeGitignore::default()));
210
211pub async fn initialize_vtcode_gitignore() -> Result<()> {
213 let gitignore = VTCodeGitignore::new().await?;
214 let mut global_gitignore = VTCODE_GITIGNORE.write().await;
215 *global_gitignore = gitignore;
216 Ok(())
217}
218
219pub async fn get_global_vtcode_gitignore() -> tokio::sync::RwLockReadGuard<'static, VTCodeGitignore>
221{
222 VTCODE_GITIGNORE.read().await
223}
224
225pub async fn should_exclude_file(file_path: &Path) -> bool {
227 let gitignore = get_global_vtcode_gitignore().await;
228 gitignore.should_exclude(file_path)
229}
230
231pub async fn filter_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
233 let gitignore = get_global_vtcode_gitignore().await;
234 gitignore.filter_paths(paths)
235}
236
237pub async fn reload_vtcode_gitignore() -> Result<()> {
239 initialize_vtcode_gitignore().await
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use std::fs::File;
246 use std::io::Write;
247 use tempfile::TempDir;
248
249 #[tokio::test]
251 async fn test_vtcodegitignore_integration() {
252 let temp_dir = TempDir::new().unwrap();
254 let gitignore_path = temp_dir.path().join(".vtcodegitignore");
255
256 let mut file = File::create(&gitignore_path).unwrap();
258 writeln!(file, "*.log").unwrap();
259 writeln!(file, "target/").unwrap();
260 writeln!(file, "!important.log").unwrap();
261
262 assert!(gitignore_path.exists());
264
265 let gitignore = VTCodeGitignore::from_directory(temp_dir.path())
267 .await
268 .unwrap();
269 assert!(gitignore.is_loaded());
270 assert_eq!(gitignore.pattern_count(), 3);
271
272 assert!(gitignore.should_exclude(&temp_dir.path().join("debug.log")));
274 assert!(gitignore.should_exclude(&temp_dir.path().join("target/binary")));
275 assert!(!gitignore.should_exclude(&temp_dir.path().join("important.log")));
276 assert!(!gitignore.should_exclude(&temp_dir.path().join("source.rs")));
277
278 println!("✓ VTCodeGitignore functionality works correctly!");
279 }
280
281 #[tokio::test]
282 async fn test_basic_pattern_matching() {
283 let temp_dir = TempDir::new().unwrap();
284 let gitignore_path = temp_dir.path().join(".vtcodegitignore");
285
286 let mut file = File::create(&gitignore_path).unwrap();
288 writeln!(file, "*.log").unwrap();
289 writeln!(file, "target/").unwrap();
290 writeln!(file, "!important.log").unwrap();
291
292 let gitignore = VTCodeGitignore::from_directory(temp_dir.path())
293 .await
294 .unwrap();
295 assert!(gitignore.is_loaded());
296 assert_eq!(gitignore.pattern_count(), 3);
297
298 assert!(gitignore.should_exclude(&temp_dir.path().join("debug.log")));
300 assert!(gitignore.should_exclude(&temp_dir.path().join("target/debug.exe")));
301 assert!(!gitignore.should_exclude(&temp_dir.path().join("important.log")));
302 assert!(!gitignore.should_exclude(&temp_dir.path().join("source.rs")));
303 }
304
305 #[tokio::test]
306 async fn test_no_gitignore_file() {
307 let temp_dir = TempDir::new().unwrap();
308 let gitignore = VTCodeGitignore::from_directory(temp_dir.path())
309 .await
310 .unwrap();
311 assert!(!gitignore.is_loaded());
312 assert_eq!(gitignore.pattern_count(), 0);
313 assert!(!gitignore.should_exclude(&temp_dir.path().join("anyfile.txt")));
314 }
315
316 #[tokio::test]
317 async fn test_empty_gitignore_file() {
318 let temp_dir = TempDir::new().unwrap();
319 let gitignore_path = temp_dir.path().join(".vtcodegitignore");
320
321 File::create(&gitignore_path).unwrap();
323
324 let gitignore = VTCodeGitignore::from_directory(temp_dir.path())
325 .await
326 .unwrap();
327 assert!(gitignore.is_loaded());
328 assert_eq!(gitignore.pattern_count(), 0);
329 }
330
331 #[tokio::test]
332 async fn test_comments_and_empty_lines() {
333 let temp_dir = TempDir::new().unwrap();
334 let gitignore_path = temp_dir.path().join(".vtcodegitignore");
335
336 let mut file = File::create(&gitignore_path).unwrap();
338 writeln!(file, "# This is a comment").unwrap();
339 writeln!(file, "").unwrap();
340 writeln!(file, "*.tmp").unwrap();
341 writeln!(file, "# Another comment").unwrap();
342 writeln!(file, "").unwrap();
343
344 let gitignore = VTCodeGitignore::from_directory(temp_dir.path())
345 .await
346 .unwrap();
347 assert!(gitignore.is_loaded());
348 assert_eq!(gitignore.pattern_count(), 1); assert!(gitignore.should_exclude(&temp_dir.path().join("file.tmp")));
351 assert!(!gitignore.should_exclude(&temp_dir.path().join("file.txt")));
352 }
353
354 #[tokio::test]
355 async fn test_path_filtering() {
356 let temp_dir = TempDir::new().unwrap();
357 let gitignore_path = temp_dir.path().join(".vtcodegitignore");
358
359 let mut file = File::create(&gitignore_path).unwrap();
361 writeln!(file, "*.log").unwrap();
362 writeln!(file, "temp/").unwrap();
363
364 let gitignore = VTCodeGitignore::from_directory(temp_dir.path())
365 .await
366 .unwrap();
367
368 let paths = vec![
369 temp_dir.path().join("app.log"),
370 temp_dir.path().join("source.rs"),
371 temp_dir.path().join("temp/cache.dat"),
372 temp_dir.path().join("important.txt"),
373 ];
374
375 let filtered = gitignore.filter_paths(paths);
376
377 assert_eq!(filtered.len(), 2);
378 assert!(filtered.contains(&temp_dir.path().join("source.rs")));
379 assert!(filtered.contains(&temp_dir.path().join("important.txt")));
380 assert!(!filtered.contains(&temp_dir.path().join("app.log")));
381 assert!(!filtered.contains(&temp_dir.path().join("temp/cache.dat")));
382 }
383}