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().map_err(|e| anyhow!("Failed to get current directory: {e}"))?;
27
28 Self::from_directory(¤t_dir).await
29 }
30
31 pub async fn from_directory(root_dir: &Path) -> Result<Self> {
33 let gitignore_path = root_dir.join(".vtcodegitignore");
34
35 let mut loaded = false;
36 let mut builder = GitignoreBuilder::new(root_dir);
37
38 if gitignore_path.exists() {
39 match Self::load_patterns(&gitignore_path, &mut builder).await {
40 Ok(()) => {
41 loaded = true;
42 }
43 Err(e) => {
44 tracing::warn!("Failed to load .vtcodegitignore: {}", e);
46 }
47 }
48 }
49
50 let matcher = builder.build().unwrap_or_else(|_| {
51 Gitignore::empty()
53 });
54
55 Ok(Self { root_dir: root_dir.to_path_buf(), matcher, loaded })
56 }
57
58 async fn load_patterns(file_path: &Path, builder: &mut GitignoreBuilder) -> Result<()> {
60 let content = fs::read_to_string(file_path)
61 .await
62 .map_err(|e| anyhow!("Failed to read .vtcodegitignore: {e}"))?;
63
64 for (line_num, line) in content.lines().enumerate() {
65 let line = line.trim();
66
67 if line.is_empty() || line.starts_with('#') {
69 continue;
70 }
71
72 builder
73 .add_line(None, line)
74 .map_err(|e| anyhow!("Invalid pattern on line {}: '{}': {}", line_num + 1, line, e))?;
75 }
76
77 Ok(())
78 }
79
80 pub fn should_exclude(&self, file_path: &Path) -> bool {
82 if !self.loaded {
83 return false;
84 }
85
86 let relative_path = match file_path.strip_prefix(&self.root_dir) {
88 Ok(rel) => rel,
89 Err(_) => file_path,
90 };
91
92 self.matcher
93 .matched_path_or_any_parents(relative_path, file_path.is_dir())
94 .is_ignore()
95 }
96
97 pub fn filter_paths(&self, paths: Vec<PathBuf>) -> Vec<PathBuf> {
99 if !self.loaded {
100 return paths;
101 }
102
103 paths.into_iter().filter(|path| !self.should_exclude(path)).collect()
104 }
105
106 pub fn is_loaded(&self) -> bool {
108 self.loaded
109 }
110
111 pub fn pattern_count(&self) -> usize {
113 self.matcher.num_ignores() as usize
114 }
115
116 pub fn root_dir(&self) -> &Path {
118 &self.root_dir
119 }
120}
121
122impl Default for VTCodeGitignore {
123 fn default() -> Self {
124 let root_dir = PathBuf::new();
125 let matcher = Gitignore::empty();
126 Self { root_dir, matcher, loaded: false }
127 }
128}
129
130pub static VTCODE_GITIGNORE: once_cell::sync::Lazy<tokio::sync::RwLock<Arc<VTCodeGitignore>>> =
132 once_cell::sync::Lazy::new(|| tokio::sync::RwLock::new(Arc::new(VTCodeGitignore::default())));
133
134pub async fn initialize_vtcode_gitignore() -> Result<()> {
136 let gitignore = VTCodeGitignore::new().await?;
137 let mut global_gitignore = VTCODE_GITIGNORE.write().await;
138 *global_gitignore = Arc::new(gitignore);
139 Ok(())
140}
141
142pub async fn snapshot_global_vtcode_gitignore() -> Arc<VTCodeGitignore> {
144 VTCODE_GITIGNORE.read().await.clone()
145}
146
147pub async fn should_exclude_file(file_path: &Path) -> bool {
149 let gitignore = snapshot_global_vtcode_gitignore().await;
150 gitignore.should_exclude(file_path)
151}
152
153pub async fn filter_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
155 let gitignore = snapshot_global_vtcode_gitignore().await;
156 gitignore.filter_paths(paths)
157}
158
159pub async fn reload_vtcode_gitignore() -> Result<()> {
161 initialize_vtcode_gitignore().await
162}