Skip to main content

gix_refspec/
spec.rs

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