vtcode_commons/
vtcodegitignore.rs1#![expect(
2 clippy::cast_possible_truncation,
3 unused_results,
4 reason = "Ignore-pattern counts use the platform's documented compact representation and builder calls are side effects."
5)]
6
7use anyhow::{Result, anyhow};
13use ignore::gitignore::{Gitignore, GitignoreBuilder};
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use tokio::fs;
17
18#[derive(Debug, Clone)]
20pub struct VTCodeGitignore {
21 root_dir: PathBuf,
23 matcher: Gitignore,
25 loaded: bool,
27}
28
29impl VTCodeGitignore {
30 pub async fn new() -> Result<Self> {
32 let current_dir = std::env::current_dir().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 loaded = false;
42 let mut builder = GitignoreBuilder::new(root_dir);
43
44 if gitignore_path.exists() {
45 match Self::load_patterns(&gitignore_path, &mut builder).await {
46 Ok(()) => {
47 loaded = true;
48 }
49 Err(e) => {
50 tracing::warn!("Failed to load .vtcodegitignore: {}", e);
52 }
53 }
54 }
55
56 let matcher = builder.build().unwrap_or_else(|_| {
57 Gitignore::empty()
59 });
60
61 Ok(Self { root_dir: root_dir.to_path_buf(), matcher, loaded })
62 }
63
64 async fn load_patterns(file_path: &Path, builder: &mut GitignoreBuilder) -> Result<()> {
66 let content = fs::read_to_string(file_path)
67 .await
68 .map_err(|e| anyhow!("Failed to read .vtcodegitignore: {e}"))?;
69
70 for (line_num, line) in content.lines().enumerate() {
71 let line = line.trim();
72
73 if line.is_empty() || line.starts_with('#') {
75 continue;
76 }
77
78 builder
79 .add_line(None, line)
80 .map_err(|e| anyhow!("Invalid pattern on line {}: '{}': {}", line_num + 1, line, e))?;
81 }
82
83 Ok(())
84 }
85
86 pub fn should_exclude(&self, file_path: &Path) -> bool {
88 if !self.loaded {
89 return false;
90 }
91
92 let relative_path = match file_path.strip_prefix(&self.root_dir) {
94 Ok(rel) => rel,
95 Err(_) => file_path,
96 };
97
98 self.matcher
99 .matched_path_or_any_parents(relative_path, file_path.is_dir())
100 .is_ignore()
101 }
102
103 pub fn filter_paths(&self, paths: Vec<PathBuf>) -> Vec<PathBuf> {
105 if !self.loaded {
106 return paths;
107 }
108
109 paths.into_iter().filter(|path| !self.should_exclude(path)).collect()
110 }
111
112 pub fn is_loaded(&self) -> bool {
114 self.loaded
115 }
116
117 pub fn pattern_count(&self) -> usize {
119 self.matcher.num_ignores() as usize
120 }
121
122 pub fn root_dir(&self) -> &Path {
124 &self.root_dir
125 }
126}
127
128impl Default for VTCodeGitignore {
129 fn default() -> Self {
130 let root_dir = PathBuf::new();
131 let matcher = Gitignore::empty();
132 Self { root_dir, matcher, loaded: false }
133 }
134}
135
136static VTCODE_GITIGNORE: once_cell::sync::Lazy<tokio::sync::RwLock<Arc<VTCodeGitignore>>> =
138 once_cell::sync::Lazy::new(|| tokio::sync::RwLock::new(Arc::new(VTCodeGitignore::default())));
139
140pub async fn initialize_vtcode_gitignore() -> Result<()> {
142 let gitignore = VTCodeGitignore::new().await?;
143 let mut global_gitignore = VTCODE_GITIGNORE.write().await;
144 *global_gitignore = Arc::new(gitignore);
145 Ok(())
146}
147
148pub async fn snapshot_global_vtcode_gitignore() -> Arc<VTCodeGitignore> {
150 VTCODE_GITIGNORE.read().await.clone()
151}
152
153pub async fn should_exclude_file(file_path: &Path) -> bool {
155 let gitignore = snapshot_global_vtcode_gitignore().await;
156 gitignore.should_exclude(file_path)
157}
158
159pub async fn filter_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
161 let gitignore = snapshot_global_vtcode_gitignore().await;
162 gitignore.filter_paths(paths)
163}
164
165pub async fn reload_vtcode_gitignore() -> Result<()> {
167 initialize_vtcode_gitignore().await
168}