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 starts 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 ref name or ref pattern.
102    ///
103    /// It's not present in case of source-only specs.
104    pub fn destination(&self) -> Option<&BStr> {
105        self.dst
106    }
107
108    /// Return the explicitly stored remote side, whose position depends on how the refspec was parsed.
109    ///
110    /// A one-sided push refspec has no explicit remote side and returns `None` here, even though its source
111    /// is later also used as its destination.
112    pub fn remote(&self) -> Option<&BStr> {
113        match self.op {
114            Operation::Push => self.dst,
115            Operation::Fetch => self.src,
116        }
117    }
118
119    /// Return the explicitly stored local side, whose position depends on how the refspec was parsed.
120    pub fn local(&self) -> Option<&BStr> {
121        match self.op {
122            Operation::Push => self.src,
123            Operation::Fetch => self.dst,
124        }
125    }
126
127    /// Derive the prefix from the [`source`][Self::source()] side of this spec if this is a fetch spec,
128    /// or the [`destination`][Self::destination()] side if it is a push spec, if it is possible to do so without ambiguity.
129    ///
130    /// Exact refs starting with `refs/` are returned unchanged, like `refs/heads/main`
131    /// or `refs/namespaces/foo/refs/heads/main`. Git-style ref patterns return the fixed portion before
132    /// their single `*` when it is more specific than `refs/`: both `refs/heads/*` and
133    /// `refs/heads/*/suffix` yield `refs/heads/`.
134    /// Negative refspecs and one-sided push refspecs return `None`.
135    pub fn prefix(&self) -> Option<&BStr> {
136        if self.mode == Mode::Negative {
137            return None;
138        }
139        let source = match self.op {
140            Operation::Fetch => self.source(),
141            Operation::Push => self.destination(),
142        }?;
143        if source == "HEAD" {
144            return source.into();
145        }
146
147        let sans_refs_prefix = source.strip_prefix(b"refs/")?;
148        if let Some(star_pos) = sans_refs_prefix.find_byte(b'*') {
149            if star_pos == 0
150                || sans_refs_prefix[star_pos + 1..].contains(&b'*')
151                || sans_refs_prefix.find_byteset(b"?[]\\").is_some()
152            {
153                return None;
154            }
155            let prefix = &source[.."refs/".len() + star_pos];
156            return (!prefix.is_empty()).then_some(prefix.as_bstr());
157        }
158        Some(source)
159    }
160
161    /// Append the remote-ref prefixes represented by this refspec to `out`, suitable for limiting the refs
162    /// requested from a remote. Unlike [`prefix()`][Self::prefix], partial names without an unambiguous prefix
163    /// are expanded to all of their Git-style ref-name candidates. For example, `main` expands to `main`,
164    /// `refs/main`, `refs/tags/main`, `refs/heads/main`, `refs/remotes/main`, and `refs/remotes/main/HEAD`.
165    ///
166    /// Fetch refspecs use their source; push refspecs use their explicit destination. Negative refspecs produce
167    /// no prefixes because they only exclude refs selected by positive refspecs; they do not request remote refs.
168    pub fn expand_prefixes(&self, out: &mut Vec<BString>) {
169        if self.mode == Mode::Negative {
170            return;
171        }
172        match self.prefix() {
173            Some(prefix) => out.push(prefix.into()),
174            None => {
175                let source = match match self.op {
176                    Operation::Fetch => self.source(),
177                    Operation::Push => self.destination(),
178                } {
179                    Some(source) => source,
180                    None => return,
181                };
182                if let Some(rest) = source.strip_prefix(b"refs/") {
183                    if !rest.contains(&b'/') {
184                        out.push(source.into());
185                    }
186                    return;
187                } else if gix_hash::ObjectId::from_hex(source).is_ok() {
188                    return;
189                }
190                expand_partial_name(source, |expanded| {
191                    out.push(expanded.into());
192                    None::<()>
193                });
194            }
195        }
196    }
197
198    /// Transform the state of the refspec into an instruction making clear what to do with it.
199    pub fn instruction(&self) -> Instruction<'a> {
200        match self.op {
201            Operation::Fetch => match (self.mode, self.src, self.dst) {
202                (Mode::Normal | Mode::Force, Some(src), None) => Instruction::Fetch(Fetch::Only { src }),
203                (Mode::Normal | Mode::Force, Some(src), Some(dst)) => Instruction::Fetch(Fetch::AndUpdate {
204                    src,
205                    dst,
206                    allow_non_fast_forward: matches!(self.mode, Mode::Force),
207                }),
208                (Mode::Negative, Some(src), None) => Instruction::Fetch(Fetch::Exclude { src }),
209                (mode, src, dest) => {
210                    unreachable!(
211                        "BUG: fetch instructions with {:?} {:?} {:?} are not possible",
212                        mode, src, dest
213                    )
214                }
215            },
216            Operation::Push => match (self.mode, self.src, self.dst) {
217                (Mode::Normal | Mode::Force, Some(src), None) => Instruction::Push(Push::Matching {
218                    src,
219                    dst: src,
220                    allow_non_fast_forward: matches!(self.mode, Mode::Force),
221                }),
222                (Mode::Normal | Mode::Force, None, Some(dst)) => {
223                    Instruction::Push(Push::Delete { ref_or_pattern: dst })
224                }
225                (Mode::Normal | Mode::Force, None, None) => Instruction::Push(Push::AllMatchingBranches {
226                    allow_non_fast_forward: matches!(self.mode, Mode::Force),
227                }),
228                (Mode::Normal | Mode::Force, Some(src), Some(dst)) => Instruction::Push(Push::Matching {
229                    src,
230                    dst,
231                    allow_non_fast_forward: matches!(self.mode, Mode::Force),
232                }),
233                (Mode::Negative, Some(src), None) => Instruction::Push(Push::Exclude { src }),
234                (mode, src, dest) => {
235                    unreachable!(
236                        "BUG: push instructions with {:?} {:?} {:?} are not possible",
237                        mode, src, dest
238                    )
239                }
240            },
241        }
242    }
243}
244
245/// Conversion
246impl RefSpecRef<'_> {
247    /// Convert this ref into a standalone, owned copy.
248    pub fn to_owned(&self) -> RefSpec {
249        RefSpec {
250            mode: self.mode,
251            op: self.op,
252            src: self.src.map(ToOwned::to_owned),
253            dst: self.dst.map(ToOwned::to_owned),
254        }
255    }
256}
257
258pub(crate) fn expand_partial_name<T>(name: &BStr, mut cb: impl FnMut(&BStr) -> Option<T>) -> Option<T> {
259    use bstr::ByteVec;
260    let mut buf = BString::from(Vec::with_capacity(128));
261    for (base, append_head) in [
262        ("", false),
263        ("refs/", false),
264        ("refs/tags/", false),
265        ("refs/heads/", false),
266        ("refs/remotes/", false),
267        ("refs/remotes/", true),
268    ] {
269        buf.clear();
270        buf.push_str(base);
271        buf.push_str(name);
272        if append_head {
273            buf.push_str("/HEAD");
274        }
275        if let Some(res) = cb(buf.as_ref()) {
276            return Some(res);
277        }
278    }
279    None
280}