vtcode_commons/
vtcodegitignore.rs1use anyhow::{Result, anyhow};
7use ignore::gitignore::{Gitignore, GitignoreBuilder};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use tokio::fs;
11
12#[derive(Debug, Clone)]
14pub struct VTCodeGitignore {
15 root_dir: PathBuf,
17 matcher: Gitignore,
19 loaded: bool,
21}
22
23impl VTCodeGitignore {
24 pub async fn new() -> Result<Self> {
26 let current_dir = std::env::current_dir()
27 .map_err(|e| anyhow!("Failed to get current directory: {}", e))?;
28
29 Self::from_directory(¤t_dir).await
30 }
31
32 pub async fn from_directory(root_dir: &Path) -> Result<Self> {
34 let gitignore_path = root_dir.join(".vtcodegitignore");
35
36 let mut loaded = false;
37 let mut builder = GitignoreBuilder::new(root_dir);
38
39 if gitignore_path.exists() {
40 match Self::load_patterns(&gitignore_path, &mut builder).await {
41 Ok(()) => {
42 loaded = true;
43 }
44 Err(e) => {
45 tracing::warn!("Failed to load .vtcodegitignore: {}", e);
47 }
48 }
49 }
50
51 let matcher = builder.build().unwrap_or_else(|_| {
52 GitignoreBuilder::new(root_dir)
54 .build()
55 .expect("empty gitignore builder should always succeed")
56 });
57
58 Ok(Self {
59 root_dir: root_dir.to_path_buf(),
60 matcher,
61 loaded,
62 })
63 }
64
65 async fn load_patterns(file_path: &Path, builder: &mut GitignoreBuilder) -> Result<()> {
67 let content = fs::read_to_string(file_path)
68 .await
69 .map_err(|e| anyhow!("Failed to read .vtcodegitignore: {}", e))?;
70
71 for (line_num, line) in content.lines().enumerate() {
72 let line = line.trim();
73
74 if line.is_empty() || line.starts_with('#') {
76 continue;
77 }
78
79 builder.add_line(None, line).map_err(|e| {
80 anyhow!(
81 "Invalid pattern on line {}: '{}': {}",
82 line_num + 1,
83 line,
84 e
85 )
86 })?;
87 }
88
89 Ok(())
90 }
91
92 pub fn should_exclude(&self, file_path: &Path) -> bool {
94 if !self.loaded {
95 return false;
96 }
97
98 let relative_path = match file_path.strip_prefix(&self.root_dir) {
100 Ok(rel) => rel,
101 Err(_) => file_path,
102 };
103
104 self.matcher
105 .matched_path_or_any_parents(relative_path, file_path.is_dir())
106 .is_ignore()
107 }
108
109 pub fn filter_paths(&self, paths: Vec<PathBuf>) -> Vec<PathBuf> {
111 if !self.loaded {
112 return paths;
113 }
114
115 paths
116 .into_iter()
117 .filter(|path| !self.should_exclude(path))
118 .collect()
119 }
120
121 pub fn is_loaded(&self) -> bool {
123 self.loaded
124 }
125
126 pub fn pattern_count(&self) -> usize {
128 self.matcher.num_ignores() as usize
129 }
130
131 pub fn root_dir(&self) -> &Path {
133 &self.root_dir
134 }
135}
136
137impl Default for VTCodeGitignore {
138 fn default() -> Self {
139 let root_dir = PathBuf::new();
140 let matcher = GitignoreBuilder::new(&root_dir)
141 .build()
142 .expect("empty gitignore builder should always succeed");
143 Self {
144 root_dir,
145 matcher,
146 loaded: false,
147 }
148 }
149}
150
151pub static VTCODE_GITIGNORE: once_cell::sync::Lazy<tokio::sync::RwLock<Arc<VTCodeGitignore>>> =
153 once_cell::sync::Lazy::new(|| tokio::sync::RwLock::new(Arc::new(VTCodeGitignore::default())));
154
155pub async fn initialize_vtcode_gitignore() -> Result<()> {
157 let gitignore = VTCodeGitignore::new().await?;
158 let mut global_gitignore = VTCODE_GITIGNORE.write().await;
159 *global_gitignore = Arc::new(gitignore);
160 Ok(())
161}
162
163pub async fn snapshot_global_vtcode_gitignore() -> Arc<VTCodeGitignore> {
165 VTCODE_GITIGNORE.read().await.clone()
166}
167
168pub async fn should_exclude_file(file_path: &Path) -> bool {
170 let gitignore = snapshot_global_vtcode_gitignore().await;
171 gitignore.should_exclude(file_path)
172}
173
174pub async fn filter_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
176 let gitignore = snapshot_global_vtcode_gitignore().await;
177 gitignore.filter_paths(paths)
178}
179
180pub async fn reload_vtcode_gitignore() -> Result<()> {
182 initialize_vtcode_gitignore().await
183}