use anyhow::{Result, anyhow};
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs;
#[derive(Debug, Clone)]
pub struct VTCodeGitignore {
root_dir: PathBuf,
matcher: Gitignore,
loaded: bool,
}
impl VTCodeGitignore {
pub async fn new() -> Result<Self> {
let current_dir = std::env::current_dir()
.map_err(|e| anyhow!("Failed to get current directory: {}", e))?;
Self::from_directory(¤t_dir).await
}
pub async fn from_directory(root_dir: &Path) -> Result<Self> {
let gitignore_path = root_dir.join(".vtcodegitignore");
let mut loaded = false;
let mut builder = GitignoreBuilder::new(root_dir);
if gitignore_path.exists() {
match Self::load_patterns(&gitignore_path, &mut builder).await {
Ok(()) => {
loaded = true;
}
Err(e) => {
tracing::warn!("Failed to load .vtcodegitignore: {}", e);
}
}
}
let matcher = builder.build().unwrap_or_else(|_| {
GitignoreBuilder::new(root_dir)
.build()
.expect("empty gitignore builder should always succeed")
});
Ok(Self {
root_dir: root_dir.to_path_buf(),
matcher,
loaded,
})
}
async fn load_patterns(file_path: &Path, builder: &mut GitignoreBuilder) -> Result<()> {
let content = fs::read_to_string(file_path)
.await
.map_err(|e| anyhow!("Failed to read .vtcodegitignore: {}", e))?;
for (line_num, line) in content.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
builder.add_line(None, line).map_err(|e| {
anyhow!(
"Invalid pattern on line {}: '{}': {}",
line_num + 1,
line,
e
)
})?;
}
Ok(())
}
pub fn should_exclude(&self, file_path: &Path) -> bool {
if !self.loaded {
return false;
}
let relative_path = match file_path.strip_prefix(&self.root_dir) {
Ok(rel) => rel,
Err(_) => file_path,
};
self.matcher
.matched_path_or_any_parents(relative_path, file_path.is_dir())
.is_ignore()
}
pub fn filter_paths(&self, paths: Vec<PathBuf>) -> Vec<PathBuf> {
if !self.loaded {
return paths;
}
paths
.into_iter()
.filter(|path| !self.should_exclude(path))
.collect()
}
pub fn is_loaded(&self) -> bool {
self.loaded
}
pub fn pattern_count(&self) -> usize {
self.matcher.num_ignores() as usize
}
pub fn root_dir(&self) -> &Path {
&self.root_dir
}
}
impl Default for VTCodeGitignore {
fn default() -> Self {
let root_dir = PathBuf::new();
let matcher = GitignoreBuilder::new(&root_dir)
.build()
.expect("empty gitignore builder should always succeed");
Self {
root_dir,
matcher,
loaded: false,
}
}
}
pub static VTCODE_GITIGNORE: once_cell::sync::Lazy<tokio::sync::RwLock<Arc<VTCodeGitignore>>> =
once_cell::sync::Lazy::new(|| tokio::sync::RwLock::new(Arc::new(VTCodeGitignore::default())));
pub async fn initialize_vtcode_gitignore() -> Result<()> {
let gitignore = VTCodeGitignore::new().await?;
let mut global_gitignore = VTCODE_GITIGNORE.write().await;
*global_gitignore = Arc::new(gitignore);
Ok(())
}
pub async fn snapshot_global_vtcode_gitignore() -> Arc<VTCodeGitignore> {
VTCODE_GITIGNORE.read().await.clone()
}
pub async fn should_exclude_file(file_path: &Path) -> bool {
let gitignore = snapshot_global_vtcode_gitignore().await;
gitignore.should_exclude(file_path)
}
pub async fn filter_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
let gitignore = snapshot_global_vtcode_gitignore().await;
gitignore.filter_paths(paths)
}
pub async fn reload_vtcode_gitignore() -> Result<()> {
initialize_vtcode_gitignore().await
}