gix_refspec/
parse.rs

1/// The error returned by the [`parse()`][crate::parse()] function.
2#[derive(Debug, thiserror::Error)]
3#[allow(missing_docs)]
4pub enum Error {
5    #[error("Empty refspecs are invalid")]
6    Empty,
7    #[error("Negative refspecs cannot have destinations as they exclude sources")]
8    NegativeWithDestination,
9    #[error("Negative specs must not be empty")]
10    NegativeEmpty,
11    #[error("Negative specs are only supported when fetching")]
12    NegativeUnsupported,
13    #[error("Negative specs must be object hashes")]
14    NegativeObjectHash,
15    #[error("Negative specs must be full ref names, starting with \"refs/\"")]
16    NegativePartialName,
17    #[error("Negative glob patterns are not allowed")]
18    NegativeGlobPattern,
19    #[error("Fetch destinations must be ref-names, like 'HEAD:refs/heads/branch'")]
20    InvalidFetchDestination,
21    #[error("Cannot push into an empty destination")]
22    PushToEmpty,
23    #[error("glob patterns may only involved a single '*' character, found {pattern:?}")]
24    PatternUnsupported { pattern: bstr::BString },
25    #[error("Both sides of the specification need a pattern, like 'a/*:b/*'")]
26    PatternUnbalanced,
27    #[error(transparent)]
28    ReferenceName(#[from] gix_validate::reference::name::Error),
29    #[error(transparent)]
30    RevSpec(#[from] gix_revision::spec::parse::Error),
31}
32
33/// Define how the parsed refspec should be used.
34#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)]
35pub enum Operation {
36    /// The `src` side is local and the `dst` side is remote.
37    Push,
38    /// The `src` side is remote and the `dst` side is local.
39    Fetch,
40}
41
42pub(crate) mod function {
43    use bstr::{BStr, ByteSlice};
44
45    use crate::{
46        parse::{Error, Operation},
47        types::Mode,
48        RefSpecRef,
49    };
50
51    /// Parse `spec` for use in `operation` and return it if it is valid.
52    pub fn parse(mut spec: &BStr, operation: Operation) -> Result<RefSpecRef<'_>, Error> {
53        fn fetch_head_only(mode: Mode) -> RefSpecRef<'static> {
54            RefSpecRef {
55                mode,
56                op: Operation::Fetch,
57                src: Some("HEAD".into()),
58                dst: None,
59            }
60        }
61
62        let mode = match spec.first() {
63            Some(&b'^') => {
64                spec = &spec[1..];
65                if operation == Operation::Push {
66                    return Err(Error::NegativeUnsupported);
67                }
68                Mode::Negative
69            }
70            Some(&b'+') => {
71                spec = &spec[1..];
72                Mode::Force
73            }
74            Some(_) => Mode::Normal,
75            None => {
76                return match operation {
77                    Operation::Push => Err(Error::Empty),
78                    Operation::Fetch => Ok(fetch_head_only(Mode::Normal)),
79                }
80            }
81        };
82
83        let (mut src, dst) = match spec.find_byte(b':') {
84            Some(pos) => {
85                if mode == Mode::Negative {
86                    return Err(Error::NegativeWithDestination);
87                }
88
89                let (src, dst) = spec.split_at(pos);
90                let dst = &dst[1..];
91                let src = (!src.is_empty()).then(|| src.as_bstr());
92                let dst = (!dst.is_empty()).then(|| dst.as_bstr());
93                match (src, dst) {
94                    (None, None) => match operation {
95                        Operation::Push => (None, None),
96                        Operation::Fetch => (Some("HEAD".into()), None),
97                    },
98                    (None, Some(dst)) => match operation {
99                        Operation::Push => (None, Some(dst)),
100                        Operation::Fetch => (Some("HEAD".into()), Some(dst)),
101                    },
102                    (Some(src), None) => match operation {
103                        Operation::Push => return Err(Error::PushToEmpty),
104                        Operation::Fetch => (Some(src), None),
105                    },
106                    (Some(src), Some(dst)) => (Some(src), Some(dst)),
107                }
108            }
109            None => {
110                let src = (!spec.is_empty()).then_some(spec);
111                if Operation::Fetch == operation && mode != Mode::Negative && src.is_none() {
112                    return Ok(fetch_head_only(mode));
113                } else {
114                    (src, None)
115                }
116            }
117        };
118
119        if let Some(spec) = src.as_mut() {
120            if *spec == "@" {
121                *spec = "HEAD".into();
122            }
123        }
124        let (src, src_had_pattern) = validated(src, operation == Operation::Push && dst.is_some())?;
125        let (dst, dst_had_pattern) = validated(dst, false)?;
126        if mode != Mode::Negative && src_had_pattern != dst_had_pattern {
127            return Err(Error::PatternUnbalanced);
128        }
129
130        if mode == Mode::Negative {
131            match src {
132                Some(spec) => {
133                    if src_had_pattern {
134                        return Err(Error::NegativeGlobPattern);
135                    } else if looks_like_object_hash(spec) {
136                        return Err(Error::NegativeObjectHash);
137                    } else if !spec.starts_with(b"refs/") && spec != "HEAD" {
138                        return Err(Error::NegativePartialName);
139                    }
140                }
141                None => return Err(Error::NegativeEmpty),
142            }
143        }
144
145        Ok(RefSpecRef {
146            op: operation,
147            mode,
148            src,
149            dst,
150        })
151    }
152
153    fn looks_like_object_hash(spec: &BStr) -> bool {
154        spec.len() >= gix_hash::Kind::shortest().len_in_hex() && spec.iter().all(u8::is_ascii_hexdigit)
155    }
156
157    fn validated(spec: Option<&BStr>, allow_revspecs: bool) -> Result<(Option<&BStr>, bool), Error> {
158        match spec {
159            Some(spec) => {
160                let glob_count = spec.iter().filter(|b| **b == b'*').take(2).count();
161                if glob_count > 1 {
162                    return Err(Error::PatternUnsupported { pattern: spec.into() });
163                }
164                let has_globs = glob_count == 1;
165                if has_globs {
166                    let mut buf = smallvec::SmallVec::<[u8; 256]>::with_capacity(spec.len());
167                    buf.extend_from_slice(spec);
168                    let glob_pos = buf.find_byte(b'*').expect("glob present");
169                    buf[glob_pos] = b'a';
170                    gix_validate::reference::name_partial(buf.as_bstr())?;
171                } else {
172                    gix_validate::reference::name_partial(spec)
173                        .map_err(Error::from)
174                        .or_else(|err| {
175                            if allow_revspecs {
176                                gix_revision::spec::parse(spec, &mut super::revparse::Noop)?;
177                                Ok(spec)
178                            } else {
179                                Err(err)
180                            }
181                        })?;
182                }
183                Ok((Some(spec), has_globs))
184            }
185            None => Ok((None, false)),
186        }
187    }
188}
189
190mod revparse {
191    use bstr::BStr;
192    use gix_revision::spec::parse::delegate::{
193        Kind, Navigate, PeelTo, PrefixHint, ReflogLookup, Revision, SiblingBranch, Traversal,
194    };
195
196    pub(crate) struct Noop;
197
198    impl Revision for Noop {
199        fn find_ref(&mut self, _name: &BStr) -> Option<()> {
200            Some(())
201        }
202
203        fn disambiguate_prefix(&mut self, _prefix: gix_hash::Prefix, _hint: Option<PrefixHint<'_>>) -> Option<()> {
204            Some(())
205        }
206
207        fn reflog(&mut self, _query: ReflogLookup) -> Option<()> {
208            Some(())
209        }
210
211        fn nth_checked_out_branch(&mut self, _branch_no: usize) -> Option<()> {
212            Some(())
213        }
214
215        fn sibling_branch(&mut self, _kind: SiblingBranch) -> Option<()> {
216            Some(())
217        }
218    }
219
220    impl Navigate for Noop {
221        fn traverse(&mut self, _kind: Traversal) -> Option<()> {
222            Some(())
223        }
224
225        fn peel_until(&mut self, _kind: PeelTo<'_>) -> Option<()> {
226            Some(())
227        }
228
229        fn find(&mut self, _regex: &BStr, _negated: bool) -> Option<()> {
230            Some(())
231        }
232
233        fn index_lookup(&mut self, _path: &BStr, _stage: u8) -> Option<()> {
234            Some(())
235        }
236    }
237
238    impl Kind for Noop {
239        fn kind(&mut self, _kind: gix_revision::spec::Kind) -> Option<()> {
240            Some(())
241        }
242    }
243
244    impl gix_revision::spec::parse::Delegate for Noop {
245        fn done(&mut self) {}
246    }
247}