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