git_revision/spec/parse/
function.rs

1use std::{convert::TryInto, str::FromStr, time::SystemTime};
2
3use bstr::{BStr, BString, ByteSlice, ByteVec};
4
5use crate::{
6    spec,
7    spec::parse::{delegate, delegate::SiblingBranch, Delegate, Error},
8};
9
10/// Parse a git [`revspec`](https://git-scm.com/docs/git-rev-parse#_specifying_revisions) and call `delegate` for each token
11/// successfully parsed.
12///
13/// Note that the `delegate` is expected to maintain enough state to lookup revisions properly.
14/// Returns `Ok(())` if all of `input` was consumed, or the error if either the `revspec` syntax was incorrect or
15/// the `delegate` failed to perform the request.
16pub fn parse(mut input: &BStr, delegate: &mut impl Delegate) -> Result<(), Error> {
17    use delegate::{Kind, Revision};
18    let mut delegate = InterceptRev::new(delegate);
19    let mut prev_kind = None;
20    if let Some(b'^') = input.first() {
21        input = next(input).1;
22        let kind = spec::Kind::ExcludeReachable;
23        delegate.kind(kind).ok_or(Error::Delegate)?;
24        prev_kind = kind.into();
25    }
26
27    let mut found_revision;
28    (input, found_revision) = {
29        let rest = revision(input, &mut delegate)?;
30        (rest, rest != input)
31    };
32    if delegate.done {
33        return if input.is_empty() {
34            Ok(())
35        } else {
36            Err(Error::UnconsumedInput { input: input.into() })
37        };
38    }
39    if let Some((rest, kind)) = try_range(input) {
40        if let Some(prev_kind) = prev_kind {
41            return Err(Error::KindSetTwice { prev_kind, kind });
42        }
43        if !found_revision {
44            delegate.find_ref("HEAD".into()).ok_or(Error::Delegate)?;
45        }
46        delegate.kind(kind).ok_or(Error::Delegate)?;
47        (input, found_revision) = {
48            let remainder = revision(rest.as_bstr(), &mut delegate)?;
49            (remainder, remainder != rest)
50        };
51        if !found_revision {
52            delegate.find_ref("HEAD".into()).ok_or(Error::Delegate)?;
53        }
54    }
55
56    if input.is_empty() {
57        delegate.done();
58        Ok(())
59    } else {
60        Err(Error::UnconsumedInput { input: input.into() })
61    }
62}
63
64mod intercept {
65    use bstr::{BStr, BString};
66
67    use crate::spec::parse::{delegate, Delegate};
68
69    #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
70    pub(crate) enum PrefixHintOwned {
71        MustBeCommit,
72        DescribeAnchor { ref_name: BString, generation: usize },
73    }
74
75    impl PrefixHintOwned {
76        pub fn to_ref(&self) -> delegate::PrefixHint<'_> {
77            match self {
78                PrefixHintOwned::MustBeCommit => delegate::PrefixHint::MustBeCommit,
79                PrefixHintOwned::DescribeAnchor { ref_name, generation } => delegate::PrefixHint::DescribeAnchor {
80                    ref_name: ref_name.as_ref(),
81                    generation: *generation,
82                },
83            }
84        }
85    }
86
87    impl<'a> From<delegate::PrefixHint<'a>> for PrefixHintOwned {
88        fn from(v: delegate::PrefixHint<'a>) -> Self {
89            match v {
90                delegate::PrefixHint::MustBeCommit => PrefixHintOwned::MustBeCommit,
91                delegate::PrefixHint::DescribeAnchor { generation, ref_name } => PrefixHintOwned::DescribeAnchor {
92                    ref_name: ref_name.to_owned(),
93                    generation,
94                },
95            }
96        }
97    }
98
99    pub(crate) struct InterceptRev<'a, T> {
100        pub inner: &'a mut T,
101        pub last_ref: Option<BString>, // TODO: smallvec to save the unnecessary allocation? Can't keep ref due to lifetime constraints in traits
102        pub last_prefix: Option<(git_hash::Prefix, Option<PrefixHintOwned>)>,
103        pub done: bool,
104    }
105
106    impl<'a, T> InterceptRev<'a, T>
107    where
108        T: Delegate,
109    {
110        pub fn new(delegate: &'a mut T) -> Self {
111            InterceptRev {
112                inner: delegate,
113                last_ref: None,
114                last_prefix: None,
115                done: false,
116            }
117        }
118    }
119
120    impl<'a, T> Delegate for InterceptRev<'a, T>
121    where
122        T: Delegate,
123    {
124        fn done(&mut self) {
125            self.done = true;
126            self.inner.done()
127        }
128    }
129
130    impl<'a, T> delegate::Revision for InterceptRev<'a, T>
131    where
132        T: Delegate,
133    {
134        fn find_ref(&mut self, name: &BStr) -> Option<()> {
135            self.last_ref = name.to_owned().into();
136            self.inner.find_ref(name)
137        }
138
139        fn disambiguate_prefix(
140            &mut self,
141            prefix: git_hash::Prefix,
142            hint: Option<delegate::PrefixHint<'_>>,
143        ) -> Option<()> {
144            self.last_prefix = Some((prefix, hint.map(Into::into)));
145            self.inner.disambiguate_prefix(prefix, hint)
146        }
147
148        fn reflog(&mut self, query: delegate::ReflogLookup) -> Option<()> {
149            self.inner.reflog(query)
150        }
151
152        fn nth_checked_out_branch(&mut self, branch_no: usize) -> Option<()> {
153            self.inner.nth_checked_out_branch(branch_no)
154        }
155
156        fn sibling_branch(&mut self, kind: delegate::SiblingBranch) -> Option<()> {
157            self.inner.sibling_branch(kind)
158        }
159    }
160
161    impl<'a, T> delegate::Navigate for InterceptRev<'a, T>
162    where
163        T: Delegate,
164    {
165        fn traverse(&mut self, kind: delegate::Traversal) -> Option<()> {
166            self.inner.traverse(kind)
167        }
168
169        fn peel_until(&mut self, kind: delegate::PeelTo<'_>) -> Option<()> {
170            self.inner.peel_until(kind)
171        }
172
173        fn find(&mut self, regex: &BStr, negated: bool) -> Option<()> {
174            self.inner.find(regex, negated)
175        }
176
177        fn index_lookup(&mut self, path: &BStr, stage: u8) -> Option<()> {
178            self.inner.index_lookup(path, stage)
179        }
180    }
181
182    impl<'a, T> delegate::Kind for InterceptRev<'a, T>
183    where
184        T: Delegate,
185    {
186        fn kind(&mut self, kind: crate::spec::Kind) -> Option<()> {
187            self.inner.kind(kind)
188        }
189    }
190}
191use intercept::InterceptRev;
192
193fn try_set_prefix(delegate: &mut impl Delegate, hex_name: &BStr, hint: Option<delegate::PrefixHint<'_>>) -> Option<()> {
194    git_hash::Prefix::from_hex(hex_name.to_str().expect("hexadecimal only"))
195        .ok()
196        .and_then(|prefix| delegate.disambiguate_prefix(prefix, hint))
197}
198
199fn long_describe_prefix(name: &BStr) -> Option<(&BStr, delegate::PrefixHint<'_>)> {
200    let mut iter = name.rsplit(|b| *b == b'-');
201    let candidate = iter.by_ref().find_map(|substr| {
202        if substr.first()? != &b'g' {
203            return None;
204        };
205        let rest = substr.get(1..)?;
206        rest.iter().all(|b| b.is_ascii_hexdigit()).then(|| rest.as_bstr())
207    })?;
208
209    let candidate = iter.clone().any(|token| !token.is_empty()).then_some(candidate);
210    let hint = iter
211        .next()
212        .and_then(|gen| gen.to_str().ok().and_then(|gen| usize::from_str(gen).ok()))
213        .and_then(|generation| {
214            iter.next().map(|token| {
215                let last_token_len = token.len();
216                let first_token_ptr = iter.last().map(|token| token.as_ptr()).unwrap_or(token.as_ptr());
217                // SAFETY: both pointers are definitely part of the same object
218                #[allow(unsafe_code)]
219                let prior_tokens_len: usize = unsafe { token.as_ptr().offset_from(first_token_ptr) }
220                    .try_into()
221                    .expect("positive value");
222                delegate::PrefixHint::DescribeAnchor {
223                    ref_name: name[..prior_tokens_len + last_token_len].as_bstr(),
224                    generation,
225                }
226            })
227        })
228        .unwrap_or(delegate::PrefixHint::MustBeCommit);
229
230    candidate.map(|c| (c, hint))
231}
232
233fn short_describe_prefix(name: &BStr) -> Option<&BStr> {
234    let mut iter = name.split(|b| *b == b'-');
235    let candidate = iter
236        .next()
237        .and_then(|prefix| prefix.iter().all(|b| b.is_ascii_hexdigit()).then(|| prefix.as_bstr()));
238    (iter.count() == 1).then_some(candidate).flatten()
239}
240
241type InsideParensRestConsumed<'a> = (std::borrow::Cow<'a, BStr>, &'a BStr, usize);
242fn parens(input: &[u8]) -> Result<Option<InsideParensRestConsumed<'_>>, Error> {
243    if input.first() != Some(&b'{') {
244        return Ok(None);
245    }
246    let mut open_braces = 0;
247    let mut ignore_next = false;
248    let mut skip_list = Vec::new();
249    for (idx, b) in input.iter().enumerate() {
250        match *b {
251            b'{' => {
252                if ignore_next {
253                    ignore_next = false;
254                } else {
255                    open_braces += 1
256                }
257            }
258            b'}' => {
259                if ignore_next {
260                    ignore_next = false;
261                } else {
262                    open_braces -= 1
263                }
264            }
265            b'\\' => {
266                skip_list.push(idx);
267                if ignore_next {
268                    skip_list.pop();
269                    ignore_next = false;
270                } else {
271                    ignore_next = true;
272                }
273            }
274            _ => {
275                if ignore_next {
276                    skip_list.pop();
277                };
278                ignore_next = false
279            }
280        }
281        if open_braces == 0 {
282            let inner: std::borrow::Cow<'_, _> = if skip_list.is_empty() {
283                input[1..idx].as_bstr().into()
284            } else {
285                let mut from = 1;
286                let mut buf = BString::default();
287                for next in skip_list.into_iter() {
288                    buf.push_str(&input[from..next]);
289                    from = next + 1;
290                }
291                if let Some(rest) = input.get(from..idx) {
292                    buf.push_str(rest);
293                }
294                buf.into()
295            };
296            return Ok(Some((inner, input[idx + 1..].as_bstr(), idx + 1)));
297        }
298    }
299    Err(Error::UnclosedBracePair { input: input.into() })
300}
301
302fn try_parse<T: FromStr + PartialEq + Default>(input: &BStr) -> Result<Option<T>, Error> {
303    input
304        .to_str()
305        .ok()
306        .and_then(|n| {
307            n.parse().ok().map(|n| {
308                if n == T::default() && input[0] == b'-' {
309                    return Err(Error::NegativeZero { input: input.into() });
310                };
311                Ok(n)
312            })
313        })
314        .transpose()
315}
316
317fn revision<'a, T>(mut input: &'a BStr, delegate: &mut InterceptRev<'_, T>) -> Result<&'a BStr, Error>
318where
319    T: Delegate,
320{
321    use delegate::{Navigate, Revision};
322    fn consume_all(res: Option<()>) -> Result<&'static BStr, Error> {
323        res.ok_or(Error::Delegate).map(|_| "".into())
324    }
325    match input.as_ref() {
326        [b':'] => return Err(Error::MissingColonSuffix),
327        [b':', b'/'] => return Err(Error::EmptyTopLevelRegex),
328        [b':', b'/', regex @ ..] => {
329            let (regex, negated) = parse_regex_prefix(regex.as_bstr())?;
330            if regex.is_empty() {
331                return Err(Error::UnconsumedInput { input: input.into() });
332            }
333            return consume_all(delegate.find(regex, negated));
334        }
335        [b':', b'0', b':', path @ ..] => return consume_all(delegate.index_lookup(path.as_bstr(), 0)),
336        [b':', b'1', b':', path @ ..] => return consume_all(delegate.index_lookup(path.as_bstr(), 1)),
337        [b':', b'2', b':', path @ ..] => return consume_all(delegate.index_lookup(path.as_bstr(), 2)),
338        [b':', path @ ..] => return consume_all(delegate.index_lookup(path.as_bstr(), 0)),
339        _ => {}
340    };
341
342    let mut sep_pos = None;
343    let mut consecutive_hex_chars = Some(0);
344    {
345        let mut cursor = input;
346        let mut ofs = 0;
347        while let Some((pos, b)) = cursor.iter().enumerate().find(|(_, b)| {
348            if b"@~^:.".contains(b) {
349                true
350            } else {
351                if let Some(num) = consecutive_hex_chars.as_mut() {
352                    if b.is_ascii_hexdigit() {
353                        *num += 1;
354                    } else {
355                        consecutive_hex_chars = None;
356                    }
357                }
358                false
359            }
360        }) {
361            if *b != b'.' || cursor.get(pos + 1) == Some(&b'.') {
362                sep_pos = Some(ofs + pos);
363                break;
364            }
365            ofs += pos + 1;
366            cursor = &cursor[pos + 1..];
367        }
368    }
369
370    let name = &input[..sep_pos.unwrap_or(input.len())].as_bstr();
371    let mut sep = sep_pos.map(|pos| input[pos]);
372    let mut has_ref_or_implied_name = name.is_empty();
373    if name.is_empty() && sep == Some(b'@') && sep_pos.and_then(|pos| input.get(pos + 1)) != Some(&b'{') {
374        delegate.find_ref("HEAD".into()).ok_or(Error::Delegate)?;
375        sep_pos = sep_pos.map(|pos| pos + 1);
376        sep = match sep_pos.and_then(|pos| input.get(pos).copied()) {
377            None => return Ok("".into()),
378            Some(pos) => Some(pos),
379        };
380    } else {
381        (consecutive_hex_chars.unwrap_or(0) >= git_hash::Prefix::MIN_HEX_LEN)
382            .then(|| try_set_prefix(delegate, name, None))
383            .flatten()
384            .or_else(|| {
385                let (prefix, hint) = long_describe_prefix(name)
386                    .map(|(c, h)| (c, Some(h)))
387                    .or_else(|| short_describe_prefix(name).map(|c| (c, None)))?;
388                try_set_prefix(delegate, prefix, hint)
389            })
390            .or_else(|| {
391                name.is_empty().then_some(()).or_else(|| {
392                    #[allow(clippy::let_unit_value)]
393                    {
394                        let res = delegate.find_ref(name)?;
395                        has_ref_or_implied_name = true;
396                        res.into()
397                    }
398                })
399            })
400            .ok_or(Error::Delegate)?;
401    }
402
403    input = {
404        if let Some(b'@') = sep {
405            let past_sep = input[sep_pos.map(|pos| pos + 1).unwrap_or(input.len())..].as_bstr();
406            let (nav, rest, _consumed) = parens(past_sep)?.ok_or_else(|| Error::AtNeedsCurlyBrackets {
407                input: input[sep_pos.unwrap_or(input.len())..].into(),
408            })?;
409            let nav = nav.as_ref();
410            if let Some(n) = try_parse::<isize>(nav)? {
411                if n < 0 {
412                    if name.is_empty() {
413                        delegate
414                            .nth_checked_out_branch(n.abs().try_into().expect("non-negative isize fits usize"))
415                            .ok_or(Error::Delegate)?;
416                    } else {
417                        return Err(Error::RefnameNeedsPositiveReflogEntries { nav: nav.into() });
418                    }
419                } else if has_ref_or_implied_name {
420                    delegate
421                        .reflog(delegate::ReflogLookup::Entry(
422                            n.try_into().expect("non-negative isize fits usize"),
423                        ))
424                        .ok_or(Error::Delegate)?;
425                } else {
426                    return Err(Error::ReflogLookupNeedsRefName { name: (*name).into() });
427                }
428            } else if let Some(kind) = SiblingBranch::parse(nav) {
429                if has_ref_or_implied_name {
430                    delegate.sibling_branch(kind).ok_or(Error::Delegate)
431                } else {
432                    Err(Error::SiblingBranchNeedsBranchName { name: (*name).into() })
433                }?
434            } else if has_ref_or_implied_name {
435                let time = nav
436                    .to_str()
437                    .map_err(|_| Error::Time {
438                        input: nav.into(),
439                        source: None,
440                    })
441                    .and_then(|date| {
442                        git_date::parse(date, Some(SystemTime::now())).map_err(|err| Error::Time {
443                            input: nav.into(),
444                            source: err.into(),
445                        })
446                    })?;
447                delegate
448                    .reflog(delegate::ReflogLookup::Date(time))
449                    .ok_or(Error::Delegate)?;
450            } else {
451                return Err(Error::ReflogLookupNeedsRefName { name: (*name).into() });
452            }
453            rest
454        } else {
455            if sep_pos == Some(0) && sep == Some(b'~') {
456                return Err(Error::MissingTildeAnchor);
457            }
458            input[sep_pos.unwrap_or(input.len())..].as_bstr()
459        }
460    };
461
462    navigate(input, delegate)
463}
464
465fn navigate<'a, T>(input: &'a BStr, delegate: &mut InterceptRev<'_, T>) -> Result<&'a BStr, Error>
466where
467    T: Delegate,
468{
469    use delegate::{Kind, Navigate, Revision};
470    let mut cursor = 0;
471    while let Some(b) = input.get(cursor) {
472        cursor += 1;
473        match *b {
474            b'~' => {
475                let (number, consumed) = input
476                    .get(cursor..)
477                    .and_then(|past_sep| try_parse_usize(past_sep.as_bstr()).transpose())
478                    .transpose()?
479                    .unwrap_or((1, 0));
480                if number != 0 {
481                    delegate
482                        .traverse(delegate::Traversal::NthAncestor(number))
483                        .ok_or(Error::Delegate)?;
484                }
485                cursor += consumed;
486            }
487            b'^' => {
488                let past_sep = input.get(cursor..);
489                if let Some((number, negative, consumed)) = past_sep
490                    .and_then(|past_sep| try_parse_isize(past_sep.as_bstr()).transpose())
491                    .transpose()?
492                {
493                    if negative {
494                        delegate
495                            .traverse(delegate::Traversal::NthParent(
496                                number
497                                    .checked_mul(-1)
498                                    .ok_or_else(|| Error::InvalidNumber {
499                                        input: past_sep.expect("present").into(),
500                                    })?
501                                    .try_into()
502                                    .expect("non-negative"),
503                            ))
504                            .ok_or(Error::Delegate)?;
505                        delegate.kind(spec::Kind::RangeBetween).ok_or(Error::Delegate)?;
506                        if let Some((prefix, hint)) = delegate.last_prefix.take() {
507                            match hint {
508                                Some(hint) => delegate.disambiguate_prefix(prefix, hint.to_ref().into()),
509                                None => delegate.disambiguate_prefix(prefix, None),
510                            }
511                            .ok_or(Error::Delegate)?;
512                        } else if let Some(name) = delegate.last_ref.take() {
513                            delegate.find_ref(name.as_bstr()).ok_or(Error::Delegate)?;
514                        } else {
515                            return Err(Error::UnconsumedInput {
516                                input: input[cursor..].into(),
517                            });
518                        }
519                        delegate.done();
520                        cursor += consumed;
521                        return Ok(input[cursor..].as_bstr());
522                    } else if number == 0 {
523                        delegate.peel_until(delegate::PeelTo::ObjectKind(git_object::Kind::Commit))
524                    } else {
525                        delegate.traverse(delegate::Traversal::NthParent(
526                            number.try_into().expect("positive number"),
527                        ))
528                    }
529                    .ok_or(Error::Delegate)?;
530                    cursor += consumed;
531                } else if let Some((kind, _rest, consumed)) =
532                    past_sep.and_then(|past_sep| parens(past_sep).transpose()).transpose()?
533                {
534                    cursor += consumed;
535                    let target = match kind.as_ref().as_ref() {
536                        b"commit" => delegate::PeelTo::ObjectKind(git_object::Kind::Commit),
537                        b"tag" => delegate::PeelTo::ObjectKind(git_object::Kind::Tag),
538                        b"tree" => delegate::PeelTo::ObjectKind(git_object::Kind::Tree),
539                        b"blob" => delegate::PeelTo::ObjectKind(git_object::Kind::Blob),
540                        b"object" => delegate::PeelTo::ValidObject,
541                        b"" => delegate::PeelTo::RecursiveTagObject,
542                        regex if regex.starts_with(b"/") => {
543                            let (regex, negated) = parse_regex_prefix(regex[1..].as_bstr())?;
544                            if !regex.is_empty() {
545                                delegate.find(regex, negated).ok_or(Error::Delegate)?;
546                            }
547                            continue;
548                        }
549                        invalid => return Err(Error::InvalidObject { input: invalid.into() }),
550                    };
551                    delegate.peel_until(target).ok_or(Error::Delegate)?;
552                } else if past_sep.and_then(|i| i.first()) == Some(&b'!') {
553                    delegate
554                        .kind(spec::Kind::ExcludeReachableFromParents)
555                        .ok_or(Error::Delegate)?;
556                    delegate.done();
557                    return Ok(input[cursor + 1..].as_bstr());
558                } else if past_sep.and_then(|i| i.first()) == Some(&b'@') {
559                    delegate
560                        .kind(spec::Kind::IncludeReachableFromParents)
561                        .ok_or(Error::Delegate)?;
562                    delegate.done();
563                    return Ok(input[cursor + 1..].as_bstr());
564                } else {
565                    delegate
566                        .traverse(delegate::Traversal::NthParent(1))
567                        .ok_or(Error::Delegate)?;
568                }
569            }
570            b':' => {
571                delegate
572                    .peel_until(delegate::PeelTo::Path(input[cursor..].as_bstr()))
573                    .ok_or(Error::Delegate)?;
574                return Ok("".into());
575            }
576            _ => return Ok(input[cursor - 1..].as_bstr()),
577        }
578    }
579    Ok("".into())
580}
581
582fn parse_regex_prefix(regex: &BStr) -> Result<(&BStr, bool), Error> {
583    Ok(match regex.strip_prefix(b"!") {
584        Some(regex) if regex.first() == Some(&b'!') => (regex.as_bstr(), false),
585        Some(regex) if regex.first() == Some(&b'-') => (regex[1..].as_bstr(), true),
586        Some(_regex) => return Err(Error::UnspecifiedRegexModifier { regex: regex.into() }),
587        None => (regex, false),
588    })
589}
590
591fn try_parse_usize(input: &BStr) -> Result<Option<(usize, usize)>, Error> {
592    let mut bytes = input.iter().peekable();
593    if bytes.peek().filter(|&&&b| b == b'-' || b == b'+').is_some() {
594        return Err(Error::SignedNumber { input: input.into() });
595    }
596    let num_digits = bytes.take_while(|b| b.is_ascii_digit()).count();
597    if num_digits == 0 {
598        return Ok(None);
599    }
600    let input = &input[..num_digits];
601    let number = try_parse(input)?.ok_or_else(|| Error::InvalidNumber { input: input.into() })?;
602    Ok(Some((number, num_digits)))
603}
604
605fn try_parse_isize(input: &BStr) -> Result<Option<(isize, bool, usize)>, Error> {
606    let mut bytes = input.iter().peekable();
607    if bytes.peek().filter(|&&&b| b == b'+').is_some() {
608        return Err(Error::SignedNumber { input: input.into() });
609    }
610    let negative = bytes.peek() == Some(&&b'-');
611    let num_digits = bytes.take_while(|b| b.is_ascii_digit() || *b == &b'-').count();
612    if num_digits == 0 {
613        return Ok(None);
614    } else if num_digits == 1 && negative {
615        return Ok(Some((-1, negative, num_digits)));
616    }
617    let input = &input[..num_digits];
618    let number = try_parse(input)?.ok_or_else(|| Error::InvalidNumber { input: input.into() })?;
619    Ok(Some((number, negative, num_digits)))
620}
621
622fn try_range(input: &BStr) -> Option<(&[u8], spec::Kind)> {
623    input
624        .strip_prefix(b"...")
625        .map(|rest| (rest, spec::Kind::ReachableToMergeBase))
626        .or_else(|| input.strip_prefix(b"..").map(|rest| (rest, spec::Kind::RangeBetween)))
627}
628
629fn next(i: &BStr) -> (u8, &BStr) {
630    let b = i[0];
631    (b, i[1..].as_bstr())
632}