use std::path::Path;
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use tracing::debug;
#[derive(Debug)]
pub struct GitignoreFilter {
gitignore: Gitignore
}
impl GitignoreFilter {
#[must_use]
pub fn new(repo_path: &Path) -> Self {
let gitignore_path = repo_path.join(".gitignore");
let mut builder = GitignoreBuilder::new(repo_path);
if gitignore_path.exists()
&& let Some(err) = builder.add(&gitignore_path)
{
debug!(error = %err, "error parsing .gitignore");
}
let gitignore = builder.build().unwrap_or_else(|_| {
GitignoreBuilder::new(repo_path)
.build()
.unwrap_or_else(|_| Gitignore::empty())
});
Self { gitignore }
}
#[must_use]
pub fn is_ignored(&self, path: &Path) -> bool {
let is_dir = path.is_dir();
self.gitignore.matched_path_or_any_parents(path, is_dir).is_ignore()
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use std::path::PathBuf;
use super::*;
#[test]
fn test_empty_filter() {
let filter = GitignoreFilter::new(Path::new("/nonexistent"));
assert!(!filter.is_ignored(&PathBuf::from("/nonexistent/file.txt")));
}
#[test]
fn test_gitignore_pattern() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path();
std::fs::write(path.join(".gitignore"), "*.log\n").expect("write gitignore");
let filter = GitignoreFilter::new(path);
assert!(
filter.is_ignored(&path.join("test.log")),
"*.log should ignore test.log"
);
}
#[test]
fn test_gitignore_allows_unmatched() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path();
std::fs::write(path.join(".gitignore"), "*.log\n").expect("write gitignore");
let filter = GitignoreFilter::new(path);
assert!(
!filter.is_ignored(&path.join("file.txt")),
"file.txt should not be ignored"
);
}
#[test]
fn test_gitignore_dir_pattern() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path();
std::fs::write(path.join(".gitignore"), "build/\n").expect("write gitignore");
std::fs::create_dir(path.join("build")).expect("mkdir build");
std::fs::write(path.join("build/output.js"), "data").expect("write file");
let filter = GitignoreFilter::new(path);
assert!(filter.is_ignored(&path.join("build")), "build/ should be ignored");
}
#[test]
fn test_macos_dotfiles_ignored() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path();
std::fs::write(path.join(".gitignore"), ".DS_Store\n._*\n").expect("write");
let filter = GitignoreFilter::new(path);
assert!(
filter.is_ignored(&path.join(".DS_Store")),
".DS_Store should be ignored"
);
assert!(
filter.is_ignored(&path.join("._test.txt")),
"._test.txt should be ignored"
);
assert!(
!filter.is_ignored(&path.join("normal.txt")),
"normal.txt should not be ignored"
);
}
#[test]
fn test_editor_temp_files_ignored() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path();
std::fs::write(path.join(".gitignore"), "*.swp\n*~\n").expect("write");
let filter = GitignoreFilter::new(path);
assert!(filter.is_ignored(&path.join(".file.swp")), ".swp should be ignored");
assert!(filter.is_ignored(&path.join("file.txt~")), "backup~ should be ignored");
assert!(
!filter.is_ignored(&path.join("file.txt")),
"file.txt should not be ignored"
);
}
#[test]
fn test_multiple_gitignore_patterns() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path();
std::fs::write(path.join(".gitignore"), "*.log\n*.tmp\ntarget/\n").expect("write");
std::fs::create_dir(path.join("target")).expect("mkdir");
let filter = GitignoreFilter::new(path);
assert!(
filter.is_ignored(&path.join("debug.log")),
"debug.log should be ignored"
);
assert!(filter.is_ignored(&path.join("temp.tmp")), "temp.tmp should be ignored");
assert!(filter.is_ignored(&path.join("target")), "target/ should be ignored");
assert!(
!filter.is_ignored(&path.join("src/main.rs")),
"main.rs should not be ignored"
);
}
#[test]
fn test_is_hidden_path() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path();
std::fs::write(path.join(".gitignore"), ".git/\n").expect("write gitignore");
std::fs::create_dir(path.join(".git")).expect("mkdir .git");
let filter = GitignoreFilter::new(path);
assert!(
filter.is_ignored(&path.join(".git")),
".git directory should be hidden (filtered)"
);
assert!(
!filter.is_ignored(&path.join(".gitignore")),
".gitignore should NOT be hidden (it is not .git/)"
);
assert!(
!filter.is_ignored(&path.join(".gitkeep")),
".gitkeep should NOT be hidden (it is not .git/)"
);
}
#[test]
fn test_gitignore_combined_patterns() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path();
std::fs::write(path.join(".gitignore"), "*.log\n*.tmp\n.DS_Store\n").expect("write");
let filter = GitignoreFilter::new(path);
assert!(
filter.is_ignored(&path.join("test.log")),
"test.log should be ignored (pattern *.log)"
);
assert!(
filter.is_ignored(&path.join("test.tmp")),
"test.tmp should be ignored (pattern *.tmp)"
);
assert!(
filter.is_ignored(&path.join(".DS_Store")),
".DS_Store should be ignored (exact match)"
);
assert!(
filter.is_ignored(&path.join("subdir/deep.log")),
"subdir/deep.log should be ignored (*.log recursive)"
);
assert!(
filter.is_ignored(&path.join("cache/session.tmp")),
"cache/session.tmp should be ignored (*.tmp recursive)"
);
assert!(
!filter.is_ignored(&path.join("README.md")),
"README.md should NOT be ignored"
);
assert!(
!filter.is_ignored(&path.join("src/main.rs")),
"src/main.rs should NOT be ignored"
);
assert!(
!filter.is_ignored(&path.join("test.txt")),
"test.txt should NOT be ignored"
);
assert!(
!filter.is_ignored(&path.join("logger.rs")),
"logger.rs should NOT be ignored (contains 'log' but not as extension)"
);
}
}