use crate::error::{CpdError, Result};
use ignore::{gitignore::GitignoreBuilder, Match};
use std::path::{Path, PathBuf};
pub struct IgnoreFilter {
gitignore: ignore::gitignore::Gitignore,
project_root: PathBuf,
}
impl IgnoreFilter {
pub fn new(project_root: &Path) -> Result<Self> {
let mut builder = GitignoreBuilder::new(project_root);
Self::add_default_patterns(&mut builder)?;
let cpdignore_path = project_root.join(".cpdignore");
if cpdignore_path.exists() {
builder.add(&cpdignore_path);
}
let gitignore_path = project_root.join(".gitignore");
if gitignore_path.exists() {
builder.add(&gitignore_path);
}
let gitignore = builder.build().map_err(|e| CpdError::InvalidIgnorePattern {
pattern: e.to_string(),
})?;
Ok(Self {
gitignore,
project_root: project_root.to_path_buf(),
})
}
fn add_default_patterns(builder: &mut GitignoreBuilder) -> Result<()> {
let default_patterns = [
".git",
".gitignore",
".cpdignore",
"target",
"node_modules",
".env",
".env.local",
"*.tmp",
"*.temp",
".DS_Store",
"Thumbs.db",
"*.pyc",
"__pycache__",
".pytest_cache",
".coverage",
".vscode",
".idea",
"*.swp",
"*.swo",
"*~",
];
for pattern in &default_patterns {
builder.add_line(None, pattern).map_err(|e| CpdError::InvalidIgnorePattern {
pattern: format!("Default pattern '{}': {}", pattern, e),
})?;
}
Ok(())
}
pub fn should_include(&self, path: &Path) -> bool {
let relative_path = if path.is_absolute() {
match path.strip_prefix(&self.project_root) {
Ok(rel) => rel,
Err(_) => return true, }
} else {
path
};
match self.gitignore.matched(relative_path, path.is_dir()) {
Match::None | Match::Whitelist(_) => true,
Match::Ignore(_) => false,
}
}
pub fn filter_fn(&self) -> impl Fn(&Path) -> bool + '_ {
move |path: &Path| self.should_include(path)
}
#[allow(dead_code)]
pub fn list_patterns(&self) -> Vec<String> {
vec![
".git/".to_string(),
"target/".to_string(),
"node_modules/".to_string(),
"*.pyc".to_string(),
"__pycache__/".to_string(),
]
}
}
#[allow(dead_code)]
pub fn create_simple_filter(patterns: &[&str]) -> Result<impl Fn(&Path) -> bool> {
use std::path::Path;
let temp_dir = std::env::temp_dir();
let mut builder = GitignoreBuilder::new(&temp_dir);
for pattern in patterns {
builder.add_line(None, pattern).map_err(|e| CpdError::InvalidIgnorePattern {
pattern: format!("Pattern '{}': {}", pattern, e),
})?;
}
let gitignore = builder.build().map_err(|e| CpdError::InvalidIgnorePattern {
pattern: e.to_string(),
})?;
Ok(move |path: &Path| {
let relative_path = if path.is_absolute() {
path.file_name().map(Path::new).unwrap_or(path)
} else {
path
};
match gitignore.matched(relative_path, false) {
Match::None | Match::Whitelist(_) => true,
Match::Ignore(_) => false,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_default_ignores() {
let temp_dir = TempDir::new().unwrap();
let filter = IgnoreFilter::new(temp_dir.path()).unwrap();
let project_root = temp_dir.path();
assert!(!filter.should_include(&project_root.join(".git")));
assert!(!filter.should_include(&project_root.join("target")));
assert!(filter.should_include(&project_root.join("main.py")));
assert!(filter.should_include(&project_root.join("code.py")));
assert!(!filter.should_include(&project_root.join("test.pyc")));
assert!(!filter.should_include(&project_root.join("__pycache__")));
}
#[test]
fn test_custom_cpdignore() {
let temp_dir = TempDir::new().unwrap();
let cpdignore_path = temp_dir.path().join(".cpdignore");
fs::write(&cpdignore_path, "custom_ignore\n*.log\ntemp_*").unwrap();
let filter = IgnoreFilter::new(temp_dir.path()).unwrap();
let project_root = temp_dir.path();
assert!(!filter.should_include(&project_root.join("custom_ignore")));
assert!(!filter.should_include(&project_root.join("debug.log")));
assert!(!filter.should_include(&project_root.join("temp_file.txt")));
assert!(filter.should_include(&project_root.join("main.py")));
}
#[test]
fn test_simple_filter() {
let filter = create_simple_filter(&["*.txt", "temp"]).unwrap();
assert!(!filter(&PathBuf::from("readme.txt")));
assert!(!filter(&PathBuf::from("temp")));
assert!(filter(&PathBuf::from("main.py")));
}
}