use ignore::gitignore::{Gitignore, GitignoreBuilder};
use std::path::Path;
const ALWAYS_EXCLUDED: &[&str] = &[".git", ".chkpt", "target"];
const DEPENDENCY_DIRS: &[&str] = &[
"node_modules",
".venv",
"venv",
"__pypackages__",
".tox",
".nox",
".gradle",
".m2",
];
pub struct IgnoreMatcher {
gitignore: Option<Gitignore>,
include_deps: bool,
}
impl IgnoreMatcher {
pub fn new(chkptignore_path: Option<&Path>, include_deps: bool) -> Self {
let gitignore = chkptignore_path.and_then(|path| {
if path.exists() {
let mut builder = GitignoreBuilder::new(path.parent().unwrap_or(Path::new(".")));
if let Some(err) = builder.add(path) {
tracing::warn!("Error parsing .chkptignore: {}", err);
return None;
}
match builder.build() {
Ok(gi) => Some(gi),
Err(err) => {
tracing::warn!("Error building .chkptignore matcher: {}", err);
None
}
}
} else {
None
}
});
Self {
gitignore,
include_deps,
}
}
pub fn is_ignored(&self, relative_path: &str, is_dir: bool) -> bool {
if has_excluded_directory_component(relative_path, self.include_deps) {
return true;
}
if let Some(ref gi) = self.gitignore {
let matched = gi.matched_path_or_any_parents(relative_path, is_dir);
if matched.is_ignore() {
return true;
}
}
false
}
}
fn has_excluded_directory_component(relative_path: &str, include_deps: bool) -> bool {
let mut components = relative_path.split('/').peekable();
while let Some(component) = components.next() {
if ALWAYS_EXCLUDED.contains(&component) {
return true;
}
if !include_deps && DEPENDENCY_DIRS.contains(&component) {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_builtin_exclusions() {
let matcher = IgnoreMatcher::new(None, false);
assert!(matcher.is_ignored(".git", true));
assert!(matcher.is_ignored(".git", false));
assert!(matcher.is_ignored(".git/HEAD", false));
assert!(matcher.is_ignored("node_modules", true));
assert!(matcher.is_ignored("node_modules", false));
assert!(matcher.is_ignored("node_modules/pkg/index.js", false));
assert!(matcher.is_ignored(".chkpt", true));
assert!(matcher.is_ignored(".chkpt", false));
assert!(matcher.is_ignored(".chkpt/config", false));
assert!(matcher.is_ignored("target", true));
assert!(matcher.is_ignored("target", false));
assert!(matcher.is_ignored("target/debug/main", false));
assert!(matcher.is_ignored("packages/app/node_modules", true));
assert!(matcher.is_ignored("packages/app/node_modules", false));
assert!(matcher.is_ignored("packages/app/node_modules/pkg/index.js", false));
assert!(matcher.is_ignored("services/api/.venv", true));
assert!(matcher.is_ignored("services/api/.venv", false));
assert!(matcher.is_ignored("services/api/.venv/lib/site.py", false));
assert!(matcher.is_ignored("crates/core/target", true));
assert!(matcher.is_ignored("crates/core/target", false));
assert!(matcher.is_ignored("crates/core/target/debug/app", false));
}
#[test]
fn test_non_excluded_paths() {
let matcher = IgnoreMatcher::new(None, false);
assert!(!matcher.is_ignored("src/main.rs", false));
assert!(!matcher.is_ignored("README.md", false));
assert!(!matcher.is_ignored(".gitignore", false));
assert!(!matcher.is_ignored("src/targeting.rs", false));
assert!(!matcher.is_ignored("src/venv_config.rs", false));
}
#[test]
fn test_chkptignore_patterns() {
let dir = TempDir::new().unwrap();
let ignore_path = dir.path().join(".chkptignore");
fs::write(&ignore_path, "*.log\nbuild/\n").unwrap();
let matcher = IgnoreMatcher::new(Some(&ignore_path), false);
assert!(matcher.is_ignored("debug.log", false));
assert!(matcher.is_ignored("build/out.o", false));
assert!(!matcher.is_ignored("src/main.rs", false));
}
}