1use ragit_fs::{FileError, get_relative_path, is_dir, is_symlink, read_dir};
2use regex::Regex;
3use std::str::FromStr;
4
5#[cfg(test)]
6mod tests;
7
8#[derive(Debug)]
9pub struct Ignore {
10 patterns: Vec<Pattern>,
11
12 strong_patterns: Vec<Pattern>,
14}
15
16impl Ignore {
17 pub fn new() -> Self {
18 Ignore {
19 patterns: vec![],
20 strong_patterns: vec![],
21 }
22 }
23
24 pub fn add_line(&mut self, line: &str) {
25 if !line.is_empty() && !line.starts_with("#") {
26 self.patterns.push(Pattern::parse(line));
27 }
28 }
29
30 pub fn add_strong_pattern(&mut self, pattern: &str) {
31 self.strong_patterns.push(Pattern::parse(pattern));
32 }
33
34 pub fn parse(s: &str) -> Self {
36 let mut patterns = vec![];
37
38 for line in s.lines() {
39 let t = line.trim();
40
41 if t.is_empty() || t.starts_with("#") {
42 continue;
43 }
44
45 patterns.push(Pattern::parse(t));
46 }
47
48 Ignore { patterns, strong_patterns: vec![] }
49 }
50
51 pub fn walk_tree(
53 &self,
54 root_dir: &str,
55 dir: &str,
56 follow_symlink: bool,
57 skip_ignored_dirs: bool,
58 ) -> Result<Vec<(bool, String)>, FileError> {
59 let mut result = vec![];
60 self.walk_tree_worker(root_dir, dir, &mut result, follow_symlink, skip_ignored_dirs, false)?;
61 Ok(result)
62 }
63
64 fn walk_tree_worker(
65 &self,
66 root_dir: &str,
67 file: &str,
68 buffer: &mut Vec<(bool, String)>,
69 follow_symlink: bool,
70 skip_ignored_dirs: bool,
71 already_ignored: bool, ) -> Result<(), FileError> {
73 if self.is_strong_match(root_dir, file) {
74 return Ok(());
75 }
76
77 if is_symlink(file) && !follow_symlink {
79 return Ok(());
80 }
81
82 let is_match = already_ignored || self.is_match(root_dir, file);
83
84 if is_dir(file) {
85 if !skip_ignored_dirs || !is_match {
86 for entry in read_dir(file, false)? {
87 self.walk_tree_worker(root_dir, &entry, buffer, follow_symlink, skip_ignored_dirs, is_match)?;
88 }
89 }
90 }
91
92 else {
93 buffer.push((is_match, file.to_string()));
94 }
95
96 Ok(())
97 }
98
99 pub fn is_match(&self, root_dir: &str, file: &str) -> bool {
100 let Ok(rel_path) = get_relative_path(&root_dir.to_string(), &file.to_string()) else { return false; };
101
102 for pattern in self.patterns.iter() {
103 if pattern.is_match(&rel_path) {
104 return true;
105 }
106 }
107
108 false
109 }
110
111 pub fn is_strong_match(&self, root_dir: &str, file: &str) -> bool {
113 let Ok(rel_path) = get_relative_path(&root_dir.to_string(), &file.to_string()) else { return false; };
114
115 for pattern in self.strong_patterns.iter() {
116 if pattern.is_match(&rel_path) {
117 return true;
118 }
119 }
120
121 false
122 }
123}
124
125#[derive(Clone, Debug)]
126pub struct Pattern(Vec<PatternUnit>);
127
128impl Pattern {
129 pub fn parse(pattern: &str) -> Self {
130 let mut pattern = pattern.to_string();
131
132 if !pattern.starts_with("/") {
135 pattern = format!("**/{pattern}");
136 }
137
138 else {
139 pattern = pattern.get(1..).unwrap().to_string();
140 }
141
142 if pattern.ends_with("/") {
144 pattern = pattern.get(0..(pattern.len() - 1)).unwrap().to_string();
145 }
146
147 let mut result = pattern.split("/").map(|p| p.parse::<PatternUnit>().unwrap_or_else(|_| PatternUnit::Fixed(p.to_string()))).collect::<Vec<_>>();
148
149 match result.last() {
150 Some(PatternUnit::DoubleAster) => {},
151 _ => {
152 result.push(PatternUnit::DoubleAster);
154 },
155 }
156
157 Pattern(result)
158 }
159
160 pub fn is_match(&self, path: &str) -> bool {
162 let mut path = path.to_string();
163
164 if path.len() > 1 && path.ends_with("/") {
166 path = path.get(0..(path.len() - 1)).unwrap().to_string();
167 }
168
169 match_worker(
170 self.0.clone(),
171 path.split("/").map(|p| p.to_string()).collect::<Vec<_>>(),
172 )
173 }
174}
175
176fn match_worker(pattern: Vec<PatternUnit>, path: Vec<String>) -> bool {
177 let mut cursors = vec![(0, 0)];
180
181 while let Some((pattern_cursor, path_cursor)) = cursors.pop() {
182 if pattern_cursor == pattern.len() && path_cursor == path.len() {
183 return true;
184 }
185
186 if pattern_cursor >= pattern.len() || path_cursor >= path.len() {
187 if let Some(PatternUnit::DoubleAster) = pattern.get(pattern_cursor) {
188 if !cursors.contains(&(pattern_cursor + 1, path_cursor)) {
189 cursors.push((pattern_cursor + 1, path_cursor));
190 }
191 }
192
193 continue;
194 }
195
196 if match_dir(&pattern[pattern_cursor], &path[path_cursor]) {
197 if let PatternUnit::DoubleAster = &pattern[pattern_cursor] {
198 if !cursors.contains(&(pattern_cursor, path_cursor + 1)) {
199 cursors.push((pattern_cursor, path_cursor + 1));
200 }
201
202 if !cursors.contains(&(pattern_cursor + 1, path_cursor)) {
203 cursors.push((pattern_cursor + 1, path_cursor));
204 }
205 }
206
207 if !cursors.contains(&(pattern_cursor + 1, path_cursor + 1)) {
208 cursors.push((pattern_cursor + 1, path_cursor + 1));
209 }
210 }
211 }
212
213 false
214}
215
216fn match_dir(pattern: &PatternUnit, path: &str) -> bool {
217 match pattern {
218 PatternUnit::DoubleAster => true,
219 PatternUnit::Regex(r) => r.is_match(path),
220 PatternUnit::Fixed(p) => path == p,
221 }
222}
223
224#[derive(Clone, Debug)]
225pub enum PatternUnit {
226 DoubleAster, Regex(Regex), Fixed(String), }
230
231impl FromStr for PatternUnit {
232 type Err = regex::Error;
233
234 fn from_str(s: &str) -> Result<Self, regex::Error> {
235 if s == "**" {
236 Ok(PatternUnit::DoubleAster)
237 }
238
239 else if s.contains("*") || s.contains("?") || s.contains("[") {
240 let s = s
241 .replace(".", "\\.")
242 .replace("+", "\\+")
243 .replace("(", "\\(")
244 .replace(")", "\\)")
245 .replace("{", "\\{")
246 .replace("}", "\\}")
247 .replace("*", ".*")
248 .replace("?", ".");
249
250 Ok(PatternUnit::Regex(Regex::new(&format!("^{s}$"))?))
251 }
252
253 else {
254 Ok(PatternUnit::Fixed(s.to_string()))
255 }
256 }
257}