use std::fs;
use std::path::Path;
pub struct IgnoreStorage {
rules: Vec<IgnoreRule>,
}
#[derive(Debug)]
enum IgnoreRule {
Substring(String),
Glob(String),
Tag(String),
}
impl IgnoreStorage {
pub fn load(ignore_file: &Path) -> Self {
let rules = match fs::read_to_string(ignore_file) {
Ok(content) => content
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.filter_map(Self::parse_rule)
.collect(),
Err(_) => Vec::new(),
};
Self { rules }
}
fn parse_rule(line: &str) -> Option<IgnoreRule> {
if let Some(tag) = line.strip_prefix('#') {
return Some(IgnoreRule::Tag(tag.to_lowercase()));
}
if line.contains('*') {
return Some(IgnoreRule::Glob(line.to_lowercase()));
}
Some(IgnoreRule::Substring(line.to_lowercase()))
}
pub fn is_ignored(&self, description: &str, tags: &[String]) -> bool {
let desc_lower = description.to_lowercase();
self.rules.iter().any(|rule| match rule {
IgnoreRule::Substring(pattern) => desc_lower.contains(pattern.as_str()),
IgnoreRule::Glob(pattern) => glob_match(pattern, &desc_lower),
IgnoreRule::Tag(tag) => tags.iter().any(|t| t.to_lowercase() == *tag),
})
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
let parts: Vec<&str> = pattern.split('*').collect();
if parts.len() == 1 {
return text == pattern;
}
let mut pos = 0;
for (i, part) in parts.iter().enumerate() {
if part.is_empty() {
continue;
}
match text[pos..].find(part) {
Some(idx) => {
if i == 0 && idx != 0 {
return false;
}
pos += idx + part.len();
}
None => return false,
}
}
if !pattern.ends_with('*') {
return pos == text.len();
}
true
}