ragit_ignore/
lib.rs

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    /// Some patterns are stronger than others. For example, you cannot `rag add .ragit/` even with `--force`.
13    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    // like `.gitignore`, `.ragignore` never fails to parse
35    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    /// It returns `Vec<(ignored: bool, file: String)>`. It only returns files, not dirs.
52    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,  // if a file is inside an ignored directory, there's no need to call `is_match` again
72    ) -> Result<(), FileError> {
73        if self.is_strong_match(root_dir, file) {
74            return Ok(());
75        }
76
77        // ragit doesn't track sym links at all
78        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    /// Some patterns are stronger than others. For example, you cannot `rag add .ragit/` even with `--force`.
112    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        // `a/b` -> `**/a/b`
133        // `/a/b` -> `a/b`
134        if !pattern.starts_with("/") {
135            pattern = format!("**/{pattern}");
136        }
137
138        else {
139            pattern = pattern.get(1..).unwrap().to_string();
140        }
141
142        // I'm not sure about this...
143        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                // `target` must match `crates/ignore/target/debug`
153                result.push(PatternUnit::DoubleAster);
154            },
155        }
156
157        Pattern(result)
158    }
159
160    // `path` must be a normalized, relative path
161    pub fn is_match(&self, path: &str) -> bool {
162        let mut path = path.to_string();
163
164        // there's no reason to treat `a/b` and `a/b/` differently
165        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    // (0, 0) means it's looking at pattern[0] and path[0].
178    // if it reaches (pattern.len(), path.len()), it matches
179    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,    // **
227    Regex(Regex),   // a*
228    Fixed(String),  // a
229}
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}