LeVCS/crates/levcs-core/src/ignore.rs

124 lines
3.3 KiB
Rust

//! Minimal `.levcsignore` support: a list of glob patterns, evaluated against
//! repository-relative paths. Patterns starting with `/` are anchored to the
//! repo root; bare patterns match anywhere. Patterns prefixed with `!` are
//! negations (re-include).
use std::path::Path;
use glob::Pattern;
#[derive(Clone, Debug, Default)]
pub struct Ignore {
rules: Vec<Rule>,
}
#[derive(Clone, Debug)]
struct Rule {
pattern: Pattern,
negate: bool,
/// Anchored to repo root (true) or matches any directory (false).
anchored: bool,
}
impl Ignore {
pub fn empty() -> Self {
Self::default()
}
/// Parse a `.levcsignore` file's contents.
pub fn parse(text: &str) -> Self {
let mut rules = Vec::new();
for line in text.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let (negate, body) = if let Some(rest) = trimmed.strip_prefix('!') {
(true, rest)
} else {
(false, trimmed)
};
let (anchored, body) = if let Some(rest) = body.strip_prefix('/') {
(true, rest)
} else {
(false, body)
};
// Always include `.levcs/` itself in the ignored set.
if let Ok(pattern) = Pattern::new(body) {
rules.push(Rule {
pattern,
negate,
anchored,
});
}
}
// Always ignore `.levcs/`
if let Ok(pattern) = Pattern::new(".levcs") {
rules.insert(
0,
Rule {
pattern,
negate: false,
anchored: true,
},
);
}
if let Ok(pattern) = Pattern::new(".levcs/**") {
rules.insert(
0,
Rule {
pattern,
negate: false,
anchored: true,
},
);
}
Self { rules }
}
pub fn is_ignored(&self, rel_path: &str) -> bool {
let mut ignored = false;
for r in &self.rules {
let matched = if r.anchored {
r.pattern.matches(rel_path)
} else {
// Match against any suffix path component sequence.
r.pattern.matches(rel_path) || rel_path.split('/').any(|c| r.pattern.matches(c))
};
if matched {
ignored = !r.negate;
}
}
ignored
}
}
/// Always-ignored paths regardless of `.levcsignore`.
pub fn always_ignored(rel: &Path) -> bool {
rel.components()
.next()
.map(|c| c.as_os_str() == ".levcs")
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dotlevcs_always_ignored() {
let ig = Ignore::empty();
assert!(always_ignored(Path::new(".levcs")));
assert!(always_ignored(Path::new(".levcs/objects")));
assert!(!always_ignored(Path::new("src/main.rs")));
let _ = ig;
}
#[test]
fn negation() {
let ig = Ignore::parse("*.log\n!keep.log\n");
assert!(ig.is_ignored("a.log"));
assert!(!ig.is_ignored("keep.log"));
}
}