Coverage Report

Created: 2026-01-16 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gitoxide/gix-refspec/src/match_group/util.rs
Line
Count
Source
1
use std::{borrow::Cow, ops::Range};
2
3
use bstr::{BStr, BString, ByteSlice, ByteVec};
4
use gix_hash::ObjectId;
5
6
use crate::{match_group::Item, RefSpecRef};
7
8
/// A type keeping enough information about a ref-spec to be able to efficiently match it against multiple matcher items.
9
#[derive(Debug)]
10
pub struct Matcher<'a> {
11
    pub(crate) lhs: Option<Needle<'a>>,
12
    pub(crate) rhs: Option<Needle<'a>>,
13
}
14
15
impl<'a> Matcher<'a> {
16
    /// Match the lefthand-side `item` against this spec and return `(true, Some<rhs>)` to gain the other,
17
    /// transformed righthand-side of the match as configured by the refspec.
18
    /// Or return `(true, None)` if there was no `rhs` but the `item` matched.
19
    /// Lastly, return `(false, None)` if `item` didn't match at all.
20
    ///
21
    /// This may involve resolving a glob with an allocation, as the destination is built using the matching portion of a glob.
22
0
    pub fn matches_lhs(&self, item: Item<'_>) -> (bool, Option<Cow<'a, BStr>>) {
23
0
        match (self.lhs, self.rhs) {
24
0
            (Some(lhs), None) => (lhs.matches(item).is_match(), None),
25
0
            (Some(lhs), Some(rhs)) => lhs.matches(item).into_match_outcome(rhs, item),
26
0
            (None, _) => (false, None),
27
        }
28
0
    }
29
30
    /// Match the righthand-side `item` against this spec and return `(true, Some<lhs>)` to gain the other,
31
    /// transformed lefthand-side of the match as configured by the refspec.
32
    /// Or return `(true, None)` if there was no `lhs` but the `item` matched.
33
    /// Lastly, return `(false, None)` if `item` didn't match at all.
34
    ///
35
    /// This may involve resolving a glob with an allocation, as the destination is built using the matching portion of a glob.
36
0
    pub fn matches_rhs(&self, item: Item<'_>) -> (bool, Option<Cow<'a, BStr>>) {
37
0
        match (self.lhs, self.rhs) {
38
0
            (None, Some(rhs)) => (rhs.matches(item).is_match(), None),
39
0
            (Some(lhs), Some(rhs)) => rhs.matches(item).into_match_outcome(lhs, item),
40
0
            (_, None) => (false, None),
41
        }
42
0
    }
43
}
44
45
#[derive(Debug, Copy, Clone)]
46
pub(crate) enum Needle<'a> {
47
    FullName(&'a BStr),
48
    PartialName(&'a BStr),
49
    Glob { name: &'a BStr, asterisk_pos: usize },
50
    Pattern(&'a BStr),
51
    Object(ObjectId),
52
}
53
54
enum Match {
55
    /// There was no match.
56
    None,
57
    /// No additional data is provided as part of the match.
58
    Normal,
59
    /// The range of text to copy from the originating item name
60
    GlobRange(Range<usize>),
61
}
62
63
impl Match {
64
0
    fn is_match(&self) -> bool {
65
0
        !matches!(self, Match::None)
66
0
    }
67
0
    fn into_match_outcome<'a>(self, destination: Needle<'a>, item: Item<'_>) -> (bool, Option<Cow<'a, BStr>>) {
68
0
        let arg = match self {
69
0
            Match::None => return (false, None),
70
0
            Match::Normal => None,
71
0
            Match::GlobRange(range) => Some((range, item)),
72
        };
73
0
        (true, destination.to_bstr_replace(arg).into())
74
0
    }
75
}
76
77
impl<'a> Needle<'a> {
78
    #[inline]
79
0
    fn matches(&self, item: Item<'_>) -> Match {
80
0
        match self {
81
0
            Needle::FullName(name) => {
82
0
                if *name == item.full_ref_name {
83
0
                    Match::Normal
84
                } else {
85
0
                    Match::None
86
                }
87
            }
88
0
            Needle::PartialName(name) => crate::spec::expand_partial_name(name, |expanded| {
89
0
                (expanded == item.full_ref_name).then_some(Match::Normal)
90
0
            })
91
0
            .unwrap_or(Match::None),
92
0
            Needle::Glob { name, asterisk_pos } => {
93
0
                match item.full_ref_name.get(..*asterisk_pos) {
94
0
                    Some(full_name_portion) if full_name_portion != name[..*asterisk_pos] => {
95
0
                        return Match::None;
96
                    }
97
0
                    None => return Match::None,
98
0
                    _ => {}
99
                }
100
0
                let tail = &name[*asterisk_pos + 1..];
101
0
                if !item.full_ref_name.ends_with(tail) {
102
0
                    return Match::None;
103
0
                }
104
0
                let end = item.full_ref_name.len() - tail.len();
105
0
                Match::GlobRange(*asterisk_pos..end)
106
            }
107
0
            Needle::Pattern(pattern) => {
108
0
                if gix_glob::wildmatch(
109
0
                    pattern,
110
0
                    item.full_ref_name,
111
                    gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
112
                ) {
113
0
                    Match::Normal
114
                } else {
115
0
                    Match::None
116
                }
117
            }
118
0
            Needle::Object(id) => {
119
0
                if *id == item.target {
120
0
                    return Match::Normal;
121
0
                }
122
0
                match item.object {
123
0
                    Some(object) if object == *id => Match::Normal,
124
0
                    _ => Match::None,
125
                }
126
            }
127
        }
128
0
    }
129
130
0
    fn to_bstr_replace(self, range: Option<(Range<usize>, Item<'_>)>) -> Cow<'a, BStr> {
131
0
        match (self, range) {
132
0
            (Needle::FullName(name), None) => Cow::Borrowed(name),
133
0
            (Needle::PartialName(name), None) => Cow::Owned({
134
0
                let mut base: BString = "refs/".into();
135
0
                if !(name.starts_with(b"tags/") || name.starts_with(b"remotes/")) {
136
0
                    base.push_str("heads/");
137
0
                }
138
0
                base.push_str(name);
139
0
                base
140
            }),
141
0
            (Needle::Glob { name, asterisk_pos }, Some((range, item))) => {
142
0
                let mut buf = Vec::with_capacity(name.len() + range.len() - 1);
143
0
                buf.push_str(&name[..asterisk_pos]);
144
0
                buf.push_str(&item.full_ref_name[range]);
145
0
                buf.push_str(&name[asterisk_pos + 1..]);
146
0
                Cow::Owned(buf.into())
147
            }
148
0
            (Needle::Object(id), None) => {
149
0
                let mut name = id.to_string();
150
0
                name.insert_str(0, "refs/heads/");
151
0
                Cow::Owned(name.into())
152
            }
153
0
            (Needle::Pattern(name), None) => Cow::Borrowed(name),
154
0
            (Needle::Glob { .. }, None) => unreachable!("BUG: no range provided for glob pattern"),
155
            (Needle::Pattern(_), Some(_)) => {
156
0
                unreachable!("BUG: range provided for pattern, but patterns don't use ranges")
157
            }
158
            (_, Some(_)) => {
159
0
                unreachable!("BUG: range provided even though needle wasn't a glob. Globs are symmetric.")
160
            }
161
        }
162
0
    }
163
164
0
    pub fn to_bstr(self) -> Cow<'a, BStr> {
165
0
        self.to_bstr_replace(None)
166
0
    }
167
}
168
169
impl<'a> From<&'a BStr> for Needle<'a> {
170
0
    fn from(v: &'a BStr) -> Self {
171
0
        if let Some(pos) = v.find_byte(b'*') {
172
0
            Needle::Glob {
173
0
                name: v,
174
0
                asterisk_pos: pos,
175
0
            }
176
0
        } else if v.starts_with(b"refs/") {
177
0
            Needle::FullName(v)
178
0
        } else if let Ok(id) = gix_hash::ObjectId::from_hex(v) {
179
0
            Needle::Object(id)
180
        } else {
181
0
            Needle::PartialName(v)
182
        }
183
0
    }
184
}
185
186
impl<'a> From<RefSpecRef<'a>> for Matcher<'a> {
187
0
    fn from(v: RefSpecRef<'a>) -> Self {
188
0
        let mut m = Matcher {
189
0
            lhs: v.src.map(Into::into),
190
0
            rhs: v.dst.map(Into::into),
191
0
        };
192
0
        if m.rhs.is_none() {
193
0
            if let Some(src) = v.src {
194
0
                if must_use_pattern_matching(src) {
195
0
                    m.lhs = Some(Needle::Pattern(src));
196
0
                }
197
0
            }
198
0
        }
199
0
        m
200
0
    }
201
}
202
203
/// Check if a pattern is complex enough to require wildmatch instead of simple glob matching
204
0
fn must_use_pattern_matching(pattern: &BStr) -> bool {
205
0
    let asterisk_count = pattern.iter().filter(|&&b| b == b'*').count();
206
0
    if asterisk_count > 1 {
207
0
        return true;
208
0
    }
209
0
    pattern
210
0
        .iter()
211
0
        .any(|&b| b == b'?' || b == b'[' || b == b']' || b == b'\\')
212
0
}