1use std::path::Path;
16
17use crate::error::{CoreError, Result};
18use crate::repo::Repository;
19
20pub const IGNORE_FILE: &str = ".cadvmignore";
22
23#[derive(Debug, Default, Clone)]
25pub struct IgnoreList {
26 patterns: Vec<Pattern>,
27}
28
29#[derive(Debug, Clone)]
30struct Pattern {
31 glob: String,
33 full_path: bool,
35 dir: bool,
37}
38
39impl IgnoreList {
40 pub fn load(repo: &Repository) -> Result<IgnoreList> {
42 let path = repo.workdir().join(IGNORE_FILE);
43 let text = match std::fs::read_to_string(&path) {
44 Ok(t) => t,
45 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(IgnoreList::default()),
46 Err(e) => return Err(CoreError::io(&path, e)),
47 };
48 Ok(IgnoreList::parse(&text))
49 }
50
51 pub fn parse(text: &str) -> IgnoreList {
53 let mut patterns = Vec::new();
54 for raw in text.lines() {
55 let line = raw.trim();
56 if line.is_empty() || line.starts_with('#') {
57 continue;
58 }
59 let dir = line.ends_with('/');
60 let body = line.trim_end_matches('/');
61 let body = body.strip_prefix('/').unwrap_or(body);
64 let full_path = body.contains('/');
65 patterns.push(Pattern {
66 glob: body.to_string(),
67 full_path,
68 dir,
69 });
70 }
71 IgnoreList { patterns }
72 }
73
74 pub fn is_ignored(&self, rel: &Path) -> bool {
76 let rel_str = normalize(rel);
77 let file_name = rel
78 .file_name()
79 .map(|n| n.to_string_lossy().into_owned())
80 .unwrap_or_default();
81
82 self.patterns.iter().any(|p| {
83 if p.dir {
84 rel_str == p.glob || rel_str.starts_with(&format!("{}/", p.glob))
86 } else if p.full_path {
87 glob_match(&p.glob, &rel_str)
88 } else {
89 glob_match(&p.glob, &file_name)
90 }
91 })
92 }
93}
94
95fn normalize(path: &Path) -> String {
97 path.to_string_lossy().replace('\\', "/")
98}
99
100fn glob_match(pattern: &str, text: &str) -> bool {
104 let p: Vec<char> = pattern.chars().collect();
105 let t: Vec<char> = text.chars().collect();
106 let (mut pi, mut ti) = (0usize, 0usize);
107 let (mut star, mut mark) = (None, 0usize);
108
109 while ti < t.len() {
110 if pi < p.len() && (p[pi] == '?' || p[pi] == t[ti]) {
111 pi += 1;
112 ti += 1;
113 } else if pi < p.len() && p[pi] == '*' {
114 star = Some(pi);
115 mark = ti;
116 pi += 1;
117 } else if let Some(s) = star {
118 pi = s + 1;
119 mark += 1;
120 ti = mark;
121 } else {
122 return false;
123 }
124 }
125 while pi < p.len() && p[pi] == '*' {
126 pi += 1;
127 }
128 pi == p.len()
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use std::path::PathBuf;
135
136 fn ign(s: &str) -> IgnoreList {
137 IgnoreList::parse(s)
138 }
139
140 #[test]
141 fn basename_glob() {
142 let l = ign("*.bak\ndraft_*.step\n");
143 assert!(l.is_ignored(&PathBuf::from("a/b/old.bak")));
144 assert!(l.is_ignored(&PathBuf::from("draft_v1.step")));
145 assert!(!l.is_ignored(&PathBuf::from("final.step")));
146 }
147
148 #[test]
149 fn directory_pattern() {
150 let l = ign("build/\n");
151 assert!(l.is_ignored(&PathBuf::from("build/part.step")));
152 assert!(l.is_ignored(&PathBuf::from("build")));
153 assert!(!l.is_ignored(&PathBuf::from("builder/part.step")));
154 }
155
156 #[test]
157 fn anchored_full_path() {
158 let l = ign("/secret/old.step\n");
159 assert!(l.is_ignored(&PathBuf::from("secret/old.step")));
160 assert!(!l.is_ignored(&PathBuf::from("sub/secret/old.step")));
161 }
162
163 #[test]
164 fn comments_and_blanks_skipped() {
165 let l = ign("# a comment\n\n \n*.tmp\n");
166 assert_eq!(l.patterns.len(), 1);
167 }
168}