compare_changes/
lib.rs

1pub fn matches_any_file(path: &str, files: &[&str]) -> bool {
2    if files.is_empty() {
3        return false;
4    }
5
6    // Parse the single path, expanding optionals into multiple variants
7    let parse_paths: Vec<(Path, bool)> = parse_path(path);
8
9    // Check if any file matches the path
10    for file in files {
11        let file_segments = if file.is_empty() { vec![] } else { file.split('/').collect() };
12
13        // Process pre-parsed path variants sequentially - each variant can override previous results
14        let mut matched = false;
15
16        for (parsed_path, is_negation) in &parse_paths {
17            if match_segments(&parsed_path.segments, &file_segments, 0, 0) {
18                if *is_negation {
19                    matched = false; // Negation path matched - exclude the file
20                } else {
21                    matched = true; // Positive path matched - include the file
22                }
23            }
24        }
25
26        // If this file matched, return true immediately
27        if matched {
28            return true;
29        }
30    }
31
32    // No file matched the path
33    false
34}
35
36#[derive(Debug, Clone)]
37struct Path {
38    segments: Vec<Segment>,
39}
40
41#[derive(Debug, Clone)]
42enum Segment {
43    Literal(String),              // "docs", "file.txt"
44    Pattern(String),              // "*.js", "*.jsx+", "[CB]at", etc.
45    DoubleStar,                   // "**"
46    DoubleStarWithSuffix(String), // "**.js"
47}
48
49fn parse_path(pattern: &str) -> Vec<(Path, bool)> {
50    let (actual_pattern, is_negation) = match pattern.strip_prefix('!') {
51        Some(rest) => (rest, true),
52        None => (pattern, false),
53    };
54
55    // Expand optionals into multiple patterns
56    expand_optionals(actual_pattern)
57        .into_iter()
58        .map(|expanded_pattern| {
59            let parts: Vec<&str> = expanded_pattern.split('/').collect();
60            let mut segments = Vec::new();
61
62            for part in parts {
63                if part == "**" {
64                    segments.push(Segment::DoubleStar);
65                } else if let Some(suffix) = part.strip_prefix("**") {
66                    segments.push(Segment::DoubleStarWithSuffix(suffix.to_string()));
67                } else if part.contains('*') || part.contains('+') || part.contains('[') {
68                    segments.push(Segment::Pattern(part.to_string()));
69                } else {
70                    segments.push(Segment::Literal(part.to_string()));
71                }
72            }
73
74            (Path { segments }, is_negation)
75        })
76        .collect()
77}
78
79fn expand_optionals(pattern: &str) -> Vec<String> {
80    if let Some(question_pos) = pattern.find('?') {
81        if question_pos == 0 {
82            return vec![pattern.to_string()]; // Invalid pattern, return as-is
83        }
84
85        let optional_char = pattern.chars().nth(question_pos - 1).unwrap();
86        let before_optional = &pattern[..question_pos - 1];
87        let after_optional = &pattern[question_pos + 1..];
88
89        // Create two variants: without and with the optional character
90        let pattern_without = format!("{}{}", before_optional, after_optional);
91        let pattern_with = format!("{}{}{}", before_optional, optional_char, after_optional);
92
93        // Recursively expand any remaining optionals in both variants
94        let mut results = Vec::new();
95        results.extend(expand_optionals(&pattern_without));
96        results.extend(expand_optionals(&pattern_with));
97        results
98    } else {
99        // No more optionals, return the pattern as-is
100        vec![pattern.to_string()]
101    }
102}
103
104fn match_segments(segments: &[Segment], path_parts: &[&str], seg_idx: usize, path_idx: usize) -> bool {
105    // Base case: both exhausted
106    if seg_idx >= segments.len() && path_idx >= path_parts.len() {
107        return true;
108    }
109
110    // Pattern exhausted but path remains
111    if seg_idx >= segments.len() {
112        return false;
113    }
114
115    match &segments[seg_idx] {
116        Segment::Literal(literal) => {
117            if path_idx >= path_parts.len() || path_parts[path_idx] != literal {
118                return false;
119            }
120            match_segments(segments, path_parts, seg_idx + 1, path_idx + 1)
121        }
122
123        Segment::Pattern(pattern) => {
124            if path_idx >= path_parts.len() {
125                return false;
126            }
127
128            if glob_match(pattern, path_parts[path_idx]) {
129                match_segments(segments, path_parts, seg_idx + 1, path_idx + 1)
130            } else {
131                false
132            }
133        }
134
135        Segment::DoubleStar => {
136            // Try consuming 0 or more path segments
137            if match_segments(segments, path_parts, seg_idx + 1, path_idx) {
138                return true;
139            }
140
141            for i in (path_idx + 1)..=path_parts.len() {
142                if match_segments(segments, path_parts, seg_idx + 1, i) {
143                    return true;
144                }
145            }
146            false
147        }
148
149        Segment::DoubleStarWithSuffix(suffix) => {
150            for i in path_idx..path_parts.len() {
151                if path_parts[i].ends_with(suffix) && match_segments(segments, path_parts, seg_idx + 1, i + 1) {
152                    return true;
153                }
154            }
155            false
156        }
157    }
158}
159
160// Single function that handles all glob pattern matching within a path segment
161fn glob_match(pattern: &str, text: &str) -> bool {
162    glob_match_recursive(pattern, text, 0, 0)
163}
164
165fn glob_match_recursive(pattern: &str, text: &str, p_idx: usize, t_idx: usize) -> bool {
166    let p_chars: Vec<char> = pattern.chars().collect();
167    let t_chars: Vec<char> = text.chars().collect();
168
169    // Base cases
170    if p_idx >= p_chars.len() && t_idx >= t_chars.len() {
171        return true; // Both exhausted
172    }
173    if p_idx >= p_chars.len() {
174        return false; // Pattern exhausted but text remains
175    }
176
177    match p_chars[p_idx] {
178        '*' => {
179            // Try matching 0 or more characters
180            for i in t_idx..=t_chars.len() {
181                if glob_match_recursive(pattern, text, p_idx + 1, i) {
182                    return true;
183                }
184            }
185            false
186        }
187
188        '+' => {
189            if p_idx == 0 {
190                return false; // Invalid pattern starting with +
191            }
192
193            let char_to_repeat = p_chars[p_idx - 1];
194
195            // The plus means "one or more of the preceding character"
196            // But we need to backtrack because we already matched one instance
197            // We need to match at least one more occurrence at the current text position
198            let mut repeat_count = 0;
199            let mut curr_t_idx = t_idx;
200
201            // Count how many times the character repeats at current position
202            while curr_t_idx < t_chars.len() && t_chars[curr_t_idx] == char_to_repeat {
203                repeat_count += 1;
204                curr_t_idx += 1;
205            }
206
207            if repeat_count == 0 {
208                return false; // Need at least one occurrence
209            }
210
211            // Continue matching from where we left off
212            glob_match_recursive(pattern, text, p_idx + 1, curr_t_idx)
213        }
214
215        '[' => {
216            if t_idx >= t_chars.len() {
217                return false;
218            }
219
220            // Find the closing bracket
221            let mut bracket_end = p_idx + 1;
222            while bracket_end < p_chars.len() && p_chars[bracket_end] != ']' {
223                bracket_end += 1;
224            }
225
226            if bracket_end >= p_chars.len() {
227                return false; // No closing bracket
228            }
229
230            // Extract bracket content
231            let bracket_content: String = p_chars[(p_idx + 1)..bracket_end].iter().collect();
232
233            if matches_bracket_content(&bracket_content, t_chars[t_idx]) {
234                glob_match_recursive(pattern, text, bracket_end + 1, t_idx + 1)
235            } else {
236                false
237            }
238        }
239
240        '?' => {
241            if p_idx == 0 {
242                return false; // Invalid pattern starting with ?
243            }
244
245            // Optional character - try both with and without
246            // Without the optional character
247            if glob_match_recursive(pattern, text, p_idx + 1, t_idx) {
248                return true;
249            }
250
251            // With the optional character (if it matches the preceding character)
252            if t_idx < t_chars.len() {
253                let optional_char = p_chars[p_idx - 1];
254                if t_chars[t_idx] == optional_char {
255                    return glob_match_recursive(pattern, text, p_idx + 1, t_idx + 1);
256                }
257            }
258
259            false
260        }
261
262        c => {
263            // Literal character - but check if next character is '+'
264            if p_idx + 1 < p_chars.len() && p_chars[p_idx + 1] == '+' {
265                // This character is followed by +, so we need to match one or more
266                let mut repeat_count = 0;
267                let mut curr_t_idx = t_idx;
268
269                while curr_t_idx < t_chars.len() && t_chars[curr_t_idx] == c {
270                    repeat_count += 1;
271                    curr_t_idx += 1;
272                }
273
274                if repeat_count == 0 {
275                    return false; // Need at least one occurrence
276                }
277
278                // Skip both the character and the '+' in pattern
279                glob_match_recursive(pattern, text, p_idx + 2, curr_t_idx)
280            } else {
281                // Regular literal character
282                if t_idx >= t_chars.len() || t_chars[t_idx] != c {
283                    return false;
284                }
285                glob_match_recursive(pattern, text, p_idx + 1, t_idx + 1)
286            }
287        }
288    }
289}
290
291fn matches_bracket_content(bracket_content: &str, ch: char) -> bool {
292    let chars: Vec<char> = bracket_content.chars().collect();
293    let mut i = 0;
294
295    while i < chars.len() {
296        // Check for range (e.g., "a-z", "0-9", "A-Z")
297        if i + 2 < chars.len() && chars[i + 1] == '-' {
298            let start_char = chars[i];
299            let end_char = chars[i + 2];
300
301            if ch >= start_char && ch <= end_char {
302                return true;
303            }
304
305            i += 3; // Skip the range
306        } else {
307            // Single character match
308            if chars[i] == ch {
309                return true;
310            }
311            i += 1;
312        }
313    }
314
315    false
316}