compare_changes/
lib.rs

1mod path;
2
3pub fn path_matches(path: &str, files: &[&str]) -> Option<usize> {
4    if files.is_empty() {
5        return None;
6    }
7
8    // Parse the single path
9    let parsed_path = path::parse(path);
10
11    // If the path is negated, immediately return None
12    if matches!(parsed_path.segments.first(), Some(path::Segment::Negation)) {
13        return None;
14    }
15
16    // Check if any file matches the path
17    files
18        .iter()
19        .enumerate()
20        .find(|(_, file)| match_path(&parsed_path.segments, file))
21        .map(|(i, _)| i)
22}
23
24fn match_path(segments: &[path::Segment], text: &str) -> bool {
25    match_path_recursive(segments, text, 0, 0, false)
26}
27
28fn match_path_recursive(segments: &[path::Segment], text: &str, seg_idx: usize, t_idx: usize, is_after_zero_double_star: bool) -> bool {
29    if seg_idx >= segments.len() && t_idx >= text.len() {
30        return true;
31    }
32    if seg_idx >= segments.len() {
33        return false;
34    }
35
36    match &segments[seg_idx] {
37        path::Segment::Literal(lit) => {
38            if lit.starts_with("/") && is_after_zero_double_star {
39                // Try without the leading /
40                let without_slash = &lit[1..];
41                if t_idx + without_slash.len() <= text.len()
42                    && &text[t_idx..t_idx + without_slash.len()] == without_slash
43                    && match_path_recursive(segments, text, seg_idx + 1, t_idx + without_slash.len(), false)
44                {
45                    return true;
46                }
47            }
48            // Normal match
49            if t_idx + lit.len() <= text.len() && &text[t_idx..t_idx + lit.len()] == lit {
50                match_path_recursive(segments, text, seg_idx + 1, t_idx + lit.len(), false)
51            } else {
52                false
53            }
54        }
55        path::Segment::SingleStar => {
56            let mut end = text.len();
57            if let Some(pos) = text[t_idx..].find('/') {
58                end = t_idx + pos;
59            }
60            for i in t_idx..=end {
61                if match_path_recursive(segments, text, seg_idx + 1, i, false) {
62                    return true;
63                }
64            }
65            false
66        }
67        path::Segment::DoubleStar => {
68            for i in t_idx..=text.len() {
69                let after_zero = i == t_idx;
70                if match_path_recursive(segments, text, seg_idx + 1, i, after_zero) {
71                    return true;
72                }
73            }
74            false
75        }
76        path::Segment::QuestionMark(c) => {
77            // Try without the optional character
78            if match_path_recursive(segments, text, seg_idx + 1, t_idx, false) {
79                return true;
80            }
81            // Try with the optional character if it matches
82            if t_idx < text.len() && text.chars().nth(t_idx) == Some(*c) {
83                return match_path_recursive(segments, text, seg_idx + 1, t_idx + 1, false);
84            }
85            false
86        }
87        path::Segment::Plus(c) => {
88            let mut count = 0;
89            let mut curr_t_idx = t_idx;
90            while curr_t_idx < text.len() && text.chars().nth(curr_t_idx) == Some(*c) {
91                count += 1;
92                curr_t_idx += 1;
93            }
94            if count == 0 {
95                return false;
96            }
97            match_path_recursive(segments, text, seg_idx + 1, curr_t_idx, false)
98        }
99        path::Segment::Bracket(b) => {
100            if t_idx >= text.len() {
101                return false;
102            }
103            let ch = text.chars().nth(t_idx).unwrap();
104            if b.singles.contains(&ch) || b.ranges.iter().any(|(start, end)| ch >= *start && ch <= *end) {
105                match_path_recursive(segments, text, seg_idx + 1, t_idx + 1, false)
106            } else {
107                false
108            }
109        }
110        path::Segment::Negation => false,
111    }
112}