Skip to main content

gix_refspec/
spec.rs

1use bstr::{BStr, BString, ByteSlice};
2
3use crate::{
4    Instruction, RefSpec, RefSpecRef,
5    instruction::{Fetch, Push},
6    parse::Operation,
7    types::Mode,
8};
9
10/// Conversion. Use the [`RefSpecRef`][RefSpec::to_ref()] type for more usage options.
11impl RefSpec {
12    /// Return ourselves as reference type.
13    pub fn to_ref(&self) -> RefSpecRef<'_> {
14        RefSpecRef {
15            mode: self.mode,
16            op: self.op,
17            src: self.src.as_ref().map(AsRef::as_ref),
18            dst: self.dst.as_ref().map(AsRef::as_ref),
19        }
20    }
21
22    /// Return true if the spec stats with a `+` and thus forces setting the reference.
23    pub fn allow_non_fast_forward(&self) -> bool {
24        matches!(self.mode, Mode::Force)
25    }
26}
27
28mod impls {
29    use std::{
30        cmp::Ordering,
31        hash::{Hash, Hasher},
32    };
33
34    use crate::{RefSpec, RefSpecRef};
35
36    impl From<RefSpecRef<'_>> for RefSpec {
37        fn from(v: RefSpecRef<'_>) -> Self {
38            v.to_owned()
39        }
40    }
41
42    impl Hash for RefSpec {
43        fn hash<H: Hasher>(&self, state: &mut H) {
44            self.to_ref().hash(state);
45        }
46    }
47
48    impl Hash for RefSpecRef<'_> {
49        fn hash<H: Hasher>(&self, state: &mut H) {
50            self.instruction().hash(state);
51        }
52    }
53
54    impl PartialEq for RefSpec {
55        fn eq(&self, other: &Self) -> bool {
56            self.to_ref().eq(&other.to_ref())
57        }
58    }
59
60    impl PartialEq for RefSpecRef<'_> {
61        fn eq(&self, other: &Self) -> bool {
62            self.instruction().eq(&other.instruction())
63        }
64    }
65
66    impl PartialOrd for RefSpecRef<'_> {
67        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
68            Some(self.cmp(other))
69        }
70    }
71
72    impl PartialOrd for RefSpec {
73        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
74            Some(self.cmp(other))
75        }
76    }
77
78    impl Ord for RefSpecRef<'_> {
79        fn cmp(&self, other: &Self) -> Ordering {
80            self.instruction().cmp(&other.instruction())
81        }
82    }
83
84    impl Ord for RefSpec {
85        fn cmp(&self, other: &Self) -> Ordering {
86            self.to_ref().cmp(&other.to_ref())
87        }
88    }
89}
90
91/// Access
92impl<'a> RefSpecRef<'a> {
93    /// Return the left-hand side of the spec, typically the source.
94    /// It takes many different forms so don't rely on this being a ref name.
95    ///
96    /// It's not present in case of deletions.
97    pub fn source(&self) -> Option<&BStr> {
98        self.src
99    }
100
101    /// Return the right-hand side of the spec, typically the destination.
102    /// It takes many different forms so don't rely on this being a ref name.
103    ///
104    /// It's not present in case of source-only specs.
105    pub fn destination(&self) -> Option<&BStr> {
106        self.dst
107    }
108
109    /// Always returns the remote side, whose actual side in the refspec depends on how it was parsed.
110    pub fn remote(&self) -> Option<&BStr> {
111        match self.op {
112            Operation::Push => self.dst,
113            Operation::Fetch => self.src,
114        }
115    }
116
117    /// Always returns the local side, whose actual side in the refspec depends on how it was parsed.
118    pub fn local(&self) -> Option<&BStr> {
119        match self.op {
120            Operation::Push => self.src,
121            Operation::Fetch => self.dst,
122        }
123    }
124
125    /// Derive the prefix from the [`source`][Self::source()] side of this spec if this is a fetch spec,
126    /// or the [`destination`][Self::destination()] side if it is a push spec, if it is possible to do so without ambiguity.
127    ///
128    /// Exact refs starting with `refs/` are returned unchanged, like `refs/heads/main`
129    /// or `refs/namespaces/foo/refs/heads/main`. Simple Git-style glob refspecs return
130    /// the portion up to their single `*`, like `refs/heads/` or `refs/namespaces/foo/refs/heads/`.
131    ///
132    /// More complex wildcard patterns don't have a Git-compatible ref-prefix and thus return `None`.
133    pub fn prefix(&self) -> Option<&BStr> {
134        if self.mode == Mode::Negative {
135            return None;
136        }
137        let source = match self.op {
138            Operation::Fetch => self.source(),
139            Operation::Push => self.destination(),
140        }?;
141        if source == "HEAD" {
142            return source.into();
143        }
144
145        let sans_refs_prefix = source.strip_prefix(b"refs/")?;
146        if let Some(star_pos) = sans_refs_prefix.find_byte(b'*') {
147            if star_pos == 0
148                || sans_refs_prefix[star_pos + 1..].contains(&b'*')
149                || sans_refs_prefix.find_byteset(b"?[]\\").is_some()
150            {
151                return None;
152            }
153            let prefix = &source[.."refs/".len() + star_pos];
154            return (!prefix.is_empty()).then_some(prefix.as_bstr());
155        }
156        Some(source)
157    }
158
159    /// As opposed to [`prefix()`][Self::prefix], if the latter is `None` it will expand to all possible prefixes and place them in `out`.
160    ///
161    /// Note that only the `source` side is considered.
162    pub fn expand_prefixes(&self, out: &mut Vec<BString>) {
163        match self.prefix() {
164            Some(prefix) => out.push(prefix.into()),
165            None => {
166                let source = match match self.op {
167                    Operation::Fetch => self.source(),
168                    Operation::Push => self.destination(),
169                } {
170                    Some(source) => source,
171                    None => return,
172                };
173                if let Some(rest) = source.strip_prefix(b"refs/") {
174                    if !rest.contains(&b'/') {
175                        out.push(source.into());
176                    }
177                    return;
178                } else if gix_hash::ObjectId::from_hex(source).is_ok() {
179                    return;
180                }
181                expand_partial_name(source, |expanded| {
182                    out.push(expanded.into());
183                    None::<()>
184                });
185            }
186        }
187    }
188
189    /// Transform the state of the refspec into an instruction making clear what to do with it.
190    pub fn instruction(&self) -> Instruction<'a> {
191        match self.op {
192            Operation::Fetch => match (self.mode, self.src, self.dst) {
193                (Mode::Normal | Mode::Force, Some(src), None) => Instruction::Fetch(Fetch::Only { src }),
194                (Mode::Normal | Mode::Force, Some(src), Some(dst)) => Instruction::Fetch(Fetch::AndUpdate {
195                    src,
196                    dst,
197                    allow_non_fast_forward: matches!(self.mode, Mode::Force),
198                }),
199                (Mode::Negative, Some(src), None) => Instruction::Fetch(Fetch::Exclude { src }),
200                (mode, src, dest) => {
201                    unreachable!(
202                        "BUG: fetch instructions with {:?} {:?} {:?} are not possible",
203                        mode, src, dest
204                    )
205                }
206            },
207            Operation::Push => match (self.mode, self.src, self.dst) {
208                (Mode::Normal | Mode::Force, Some(src), None) => Instruction::Push(Push::Matching {
209                    src,
210                    dst: src,
211                    allow_non_fast_forward: matches!(self.mode, Mode::Force),
212                }),
213                (Mode::Normal | Mode::Force, None, Some(dst)) => {
214                    Instruction::Push(Push::Delete { ref_or_pattern: dst })
215                }
216                (Mode::Normal | Mode::Force, None, None) => Instruction::Push(Push::AllMatchingBranches {
217                    allow_non_fast_forward: matches!(self.mode, Mode::Force),
218                }),
219                (Mode::Normal | Mode::Force, Some(src), Some(dst)) => Instruction::Push(Push::Matching {
220                    src,
221                    dst,
222                    allow_non_fast_forward: matches!(self.mode, Mode::Force),
223                }),
224                (Mode::Negative, Some(src), None) => Instruction::Push(Push::Exclude { src }),
225                (mode, src, dest) => {
226                    unreachable!(
227                        "BUG: push instructions with {:?} {:?} {:?} are not possible",
228                        mode, src, dest
229                    )
230                }
231            },
232        }
233    }
234}
235
236/// Conversion
237impl RefSpecRef<'_> {
238    /// Convert this ref into a standalone, owned copy.
239    pub fn to_owned(&self) -> RefSpec {
240        RefSpec {
241            mode: self.mode,
242            op: self.op,
243            src: self.src.map(ToOwned::to_owned),
244            dst: self.dst.map(ToOwned::to_owned),
245        }
246    }
247}
248
249pub(crate) fn expand_partial_name<T>(name: &BStr, mut cb: impl FnMut(&BStr) -> Option<T>) -> Option<T> {
250    use bstr::ByteVec;
251    let mut buf = BString::from(Vec::with_capacity(128));
252    for (base, append_head) in [
253        ("", false),
254        ("refs/", false),
255        ("refs/tags/", false),
256        ("refs/heads/", false),
257        ("refs/remotes/", false),
258        ("refs/remotes/", true),
259    ] {
260        buf.clear();
261        buf.push_str(base);
262        buf.push_str(name);
263        if append_head {
264            buf.push_str("/HEAD");
265        }
266        if let Some(res) = cb(buf.as_ref()) {
267            return Some(res);
268        }
269    }
270    None
271}