vtcode_commons/
vtcodegitignore.rs1use anyhow::{Result, anyhow};
4use glob::Pattern;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use tokio::fs;
8
9#[derive(Debug, Clone)]
11pub struct VTCodeGitignore {
12 root_dir: PathBuf,
14 patterns: Vec<CompiledPattern>,
16 loaded: bool,
18}
19
20#[derive(Debug, Clone)]
22struct CompiledPattern {
23 original: String,
25 pattern: Pattern,
27 negated: bool,
29}
30
31impl VTCodeGitignore {
32 pub async fn new() -> Result<Self> {
34 let current_dir = std::env::current_dir()
35 .map_err(|e| anyhow!("Failed to get current directory: {}", e))?;
36
37 Self::from_directory(¤t_dir).await
38 }
39
40 pub async fn from_directory(root_dir: &Path) -> Result<Self> {
42 let gitignore_path = root_dir.join(".vtcodegitignore");
43
44 let mut patterns = Vec::new();
45 let mut loaded = false;
46
47 if gitignore_path.exists() {
48 match Self::load_patterns(&gitignore_path).await {
49 Ok(loaded_patterns) => {
50 patterns = loaded_patterns;
51 loaded = true;
52 }
53 Err(e) => {
54 tracing::warn!("Failed to load .vtcodegitignore: {}", e);
56 }
57 }
58 }
59
60 Ok(Self {
61 root_dir: root_dir.to_path_buf(),
62 patterns,
63 loaded,
64 })
65 }
66
67 async fn load_patterns(file_path: &Path) -> Result<Vec<CompiledPattern>> {
69 let content = fs::read_to_string(file_path)
70 .await
71 .map_err(|e| anyhow!("Failed to read .vtcodegitignore: {}", e))?;
72
73 let mut patterns = Vec::new();
74
75 for (line_num, line) in content.lines().enumerate() {
76 let line = line.trim();
77
78 if line.is_empty() || line.starts_with('#') {
80 continue;
81 }
82
83 let (pattern_str, negated) = if let Some(stripped) = line.strip_prefix('!') {
85 (stripped.to_string(), true)
86 } else {
87 (line.to_string(), false)
88 };
89
90 let glob_pattern = Self::convert_gitignore_to_glob(&pattern_str);
92
93 match Pattern::new(&glob_pattern) {
94 Ok(pattern) => {
95 patterns.push(CompiledPattern {
96 original: pattern_str,
97 pattern,
98 negated,
99 });
100 }
101 Err(e) => {
102 return Err(anyhow!(
103 "Invalid pattern on line {}: '{}': {}",
104 line_num + 1,
105 pattern_str,
106 e
107 ));
108 }
109 }
110 }
111
112 Ok(patterns)
113 }
114
115 fn convert_gitignore_to_glob(pattern: &str) -> String {
117 let mut result = pattern.to_string();
118
119 if result.ends_with('/') {
121 result = format!("{}/**", result.trim_end_matches('/'));
122 }
123
124 if !result.starts_with('/') && !result.starts_with("**/") && !result.contains('/') {
126 result = format!("**/{}", result);
128 }
129
130 result
131 }
132
133 pub fn should_exclude(&self, file_path: &Path) -> bool {
135 if !self.loaded || self.patterns.is_empty() {
136 return false;
137 }
138
139 let relative_path = match file_path.strip_prefix(&self.root_dir) {
141 Ok(rel) => rel,
142 Err(_) => {
143 file_path
145 }
146 };
147
148 let path_str = relative_path.to_string_lossy();
149
150 let mut excluded = false;
152
153 for pattern in &self.patterns {
154 if pattern.pattern.matches(&path_str) {
155 if pattern.original.ends_with('/') && file_path.is_file() {
156 continue;
158 }
159 if pattern.negated {
160 excluded = false;
162 } else {
163 excluded = true;
165 }
166 }
167 }
168
169 excluded
170 }
171
172 pub fn filter_paths(&self, paths: Vec<PathBuf>) -> Vec<PathBuf> {
174 if !self.loaded {
175 return paths;
176 }
177
178 paths
179 .into_iter()
180 .filter(|path| !self.should_exclude(path))
181 .collect()
182 }
183
184 pub fn is_loaded(&self) -> bool {
186 self.loaded
187 }
188
189 pub fn pattern_count(&self) -> usize {
191 self.patterns.len()
192 }
193
194 pub fn root_dir(&self) -> &Path {
196 &self.root_dir
197 }
198}
199
200impl Default for VTCodeGitignore {
201 fn default() -> Self {
202 Self {
203 root_dir: PathBuf::new(),
204 patterns: Vec::new(),
205 loaded: false,
206 }
207 }
208}
209
210pub static VTCODE_GITIGNORE: once_cell::sync::Lazy<tokio::sync::RwLock<Arc<VTCodeGitignore>>> =
212 once_cell::sync::Lazy::new(|| tokio::sync::RwLock::new(Arc::new(VTCodeGitignore::default())));
213
214pub async fn initialize_vtcode_gitignore() -> Result<()> {
216 let gitignore = VTCodeGitignore::new().await?;
217 let mut global_gitignore = VTCODE_GITIGNORE.write().await;
218 *global_gitignore = Arc::new(gitignore);
219 Ok(())
220}
221
222pub async fn snapshot_global_vtcode_gitignore() -> Arc<VTCodeGitignore> {
224 VTCODE_GITIGNORE.read().await.clone()
225}
226
227pub async fn should_exclude_file(file_path: &Path) -> bool {
229 let gitignore = snapshot_global_vtcode_gitignore().await;
230 gitignore.should_exclude(file_path)
231}
232
233pub async fn filter_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
235 let gitignore = snapshot_global_vtcode_gitignore().await;
236 gitignore.filter_paths(paths)
237}
238
239pub async fn reload_vtcode_gitignore() -> Result<()> {
241 initialize_vtcode_gitignore().await
242}