//! 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, } #[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")); } }