use crate::config::IgnoreConfig;
use globset::{Glob, GlobSet, GlobSetBuilder};
use std::path::Path;
use tracing::warn;
#[derive(Clone)]
pub struct IgnoreFilter {
globset: Option<GlobSet>,
}
impl Default for IgnoreFilter {
fn default() -> Self {
Self::new()
}
}
impl IgnoreFilter {
pub fn new() -> Self {
Self { globset: None }
}
pub fn from_config(config: &IgnoreConfig) -> Self {
if config.patterns.is_empty() {
return Self::new();
}
let mut builder = GlobSetBuilder::new();
for pattern in &config.patterns {
match Glob::new(pattern) {
Ok(glob) => {
builder.add(glob);
}
Err(e) => {
warn!(pattern = %pattern, error = %e, "Invalid ignore pattern");
}
}
}
let globset = match builder.build() {
Ok(set) => Some(set),
Err(e) => {
warn!(error = %e, "Failed to build globset");
None
}
};
Self { globset }
}
pub fn add_pattern(&mut self, pattern: &str) -> Result<(), globset::Error> {
let glob = Glob::new(pattern)?;
let mut builder = GlobSetBuilder::new();
builder.add(glob);
self.globset = Some(builder.build()?);
Ok(())
}
pub fn is_ignored(&self, path: &Path) -> bool {
if let Some(ref globset) = self.globset {
let path_str = path.to_string_lossy().replace('\\', "/");
globset.is_match(Path::new(&path_str))
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_filter() {
let filter = IgnoreFilter::new();
assert!(!filter.is_ignored(Path::new("/project/src/main.rs")));
}
#[test]
fn test_simple_pattern() {
let config = IgnoreConfig {
patterns: vec!["**/node_modules/**".to_string()],
};
let filter = IgnoreFilter::from_config(&config);
assert!(filter.is_ignored(Path::new("/project/node_modules/pkg/index.js")));
assert!(filter.is_ignored(Path::new("/project/sub/node_modules/pkg/index.js")));
assert!(!filter.is_ignored(Path::new("/project/src/main.rs")));
}
#[test]
fn test_glob_pattern_with_extension() {
let config = IgnoreConfig {
patterns: vec!["**/*.test.{js,ts}".to_string()],
};
let filter = IgnoreFilter::from_config(&config);
assert!(filter.is_ignored(Path::new("/project/src/app.test.js")));
assert!(filter.is_ignored(Path::new("/project/src/app.test.ts")));
assert!(!filter.is_ignored(Path::new("/project/src/app.js")));
}
#[test]
fn test_multiple_patterns() {
let config = IgnoreConfig {
patterns: vec![
"**/node_modules/**".to_string(),
"**/target/**".to_string(),
"**/.git/**".to_string(),
],
};
let filter = IgnoreFilter::from_config(&config);
assert!(filter.is_ignored(Path::new("/project/node_modules/pkg")));
assert!(filter.is_ignored(Path::new("/project/target/debug/main")));
assert!(filter.is_ignored(Path::new("/project/.git/config")));
assert!(!filter.is_ignored(Path::new("/project/src/main.rs")));
}
#[test]
fn test_invalid_pattern_is_skipped() {
let config = IgnoreConfig {
patterns: vec![
"**/valid/**".to_string(),
"[invalid".to_string(), "**/also_valid/**".to_string(),
],
};
let filter = IgnoreFilter::from_config(&config);
assert!(filter.is_ignored(Path::new("/project/valid/file")));
assert!(filter.is_ignored(Path::new("/project/also_valid/file")));
}
#[test]
fn test_add_pattern() {
let mut filter = IgnoreFilter::new();
filter.add_pattern("**/node_modules/**").unwrap();
assert!(filter.is_ignored(Path::new("/project/node_modules/pkg")));
}
#[test]
fn test_directory_pattern() {
let config = IgnoreConfig {
patterns: vec!["**/test/**".to_string(), "**/tests/**".to_string()],
};
let filter = IgnoreFilter::from_config(&config);
assert!(filter.is_ignored(Path::new("/project/tests/unit.rs")));
assert!(filter.is_ignored(Path::new("/project/test/unit.rs")));
assert!(!filter.is_ignored(Path::new("/project/src/contest.rs")));
}
#[test]
fn test_extension_pattern() {
let config = IgnoreConfig {
patterns: vec!["**/*.{log,tmp,bak}".to_string()],
};
let filter = IgnoreFilter::from_config(&config);
assert!(filter.is_ignored(Path::new("/project/debug.log")));
assert!(filter.is_ignored(Path::new("/project/session.tmp")));
assert!(filter.is_ignored(Path::new("/project/config.bak")));
assert!(!filter.is_ignored(Path::new("/project/main.rs")));
}
#[test]
fn test_single_star_pattern() {
let config = IgnoreConfig {
patterns: vec!["*.log".to_string()],
};
let filter = IgnoreFilter::from_config(&config);
assert!(filter.is_ignored(Path::new("debug.log")));
}
#[test]
fn test_double_star_pattern() {
let config = IgnoreConfig {
patterns: vec!["**/*.log".to_string()],
};
let filter = IgnoreFilter::from_config(&config);
assert!(filter.is_ignored(Path::new("debug.log")));
assert!(filter.is_ignored(Path::new("logs/debug.log")));
assert!(filter.is_ignored(Path::new("deep/nested/path/debug.log")));
}
#[test]
fn test_specific_file_pattern() {
let config = IgnoreConfig {
patterns: vec!["**/secrets.txt".to_string()],
};
let filter = IgnoreFilter::from_config(&config);
assert!(filter.is_ignored(Path::new("secrets.txt")));
assert!(filter.is_ignored(Path::new("config/secrets.txt")));
assert!(!filter.is_ignored(Path::new("config/settings.txt")));
}
}