use std::path::{Path, PathBuf};
pub fn path_is_ignored(path: &Path, patterns: &[String]) -> bool {
use glob::{MatchOptions, Pattern};
if patterns.is_empty() {
return false;
}
let normalized = path
.to_string_lossy()
.replace('\\', "/")
.trim_start_matches("./")
.to_string();
let options = MatchOptions {
case_sensitive: true,
require_literal_separator: true,
require_literal_leading_dot: false,
};
for pattern in patterns {
let mut pat = pattern.replace('\\', "/");
pat = pat.trim_start_matches("./").to_string();
if pat.ends_with('/') {
pat.push_str("**");
}
let mut candidates = vec![pat.clone()];
if !pat.starts_with("**/") {
candidates.push(format!("**/{pat}"));
}
for candidate in candidates {
if let Ok(compiled) = Pattern::new(&candidate)
&& compiled.matches_with(&normalized, options)
{
return true;
}
}
}
false
}
pub fn filter_ignored_paths(files: &mut Vec<PathBuf>, patterns: &[String]) {
if patterns.is_empty() {
return;
}
files.retain(|path| !path_is_ignored(path, patterns));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_is_ignored() {
let p = |s: &str| PathBuf::from(s);
let pats = vec!["vendor/".to_string()];
assert!(path_is_ignored(&p("vendor/skip.md"), &pats));
assert!(path_is_ignored(&p("./vendor/nested/skip.md"), &pats));
assert!(!path_is_ignored(&p("docs/keep.md"), &pats));
let pats = vec!["*.backup.md".to_string()];
assert!(path_is_ignored(&p("notes.backup.md"), &pats));
assert!(path_is_ignored(&p("a/b/notes.backup.md"), &pats));
assert!(!path_is_ignored(&p("notes.md"), &pats));
let pats = vec!["not-found.md".to_string()];
assert!(path_is_ignored(&p("not-found.md"), &pats));
assert!(path_is_ignored(&p("src/deep/not-found.md"), &pats));
let pats = vec!["**/generated.md".to_string()];
assert!(path_is_ignored(&p("x/y/generated.md"), &pats));
assert!(!path_is_ignored(&p("anything.md"), &[]));
}
#[test]
fn test_path_is_ignored_normalizes_separators() {
let pats = vec!["generated/".to_string()];
assert!(path_is_ignored(&PathBuf::from(r"generated\api.md"), &pats));
assert!(path_is_ignored(
&PathBuf::from(r"generated\deep\nested.md"),
&pats
));
assert!(!path_is_ignored(&PathBuf::from(r"guide\intro.md"), &pats));
}
#[test]
fn test_filter_ignored_paths() {
let mut files = vec![
PathBuf::from("docs/keep.md"),
PathBuf::from("vendor/skip.md"),
PathBuf::from("drafts/wip.md"),
PathBuf::from("notes.backup.md"),
];
let patterns = vec![
"vendor/".to_string(),
"drafts/".to_string(),
"*.backup.md".to_string(),
];
filter_ignored_paths(&mut files, &patterns);
assert_eq!(files, vec![PathBuf::from("docs/keep.md")]);
}
}