788 lines · 30.6 KB · Rust
RawDownload
use std::{str::FromStr, time::SystemTime}; use crate::{    spec,    spec::parse::{Delegate, Error, delegate, delegate::SiblingBranch},};use bstr::{BStr, BString, ByteSlice, ByteVec};use gix_error::{ErrorExt, Exn, ResultExt}; /// Parse a git [`revspec`](https://git-scm.com/docs/git-rev-parse#_specifying_revisions) and call `delegate` for each token/// successfully parsed.////// Note that the `delegate` is expected to maintain enough state to lookup revisions properly./// Returns `Ok(())` if all of `input` was consumed, or the error if either the `revspec` syntax was incorrect or/// the `delegate` failed to perform the request.pub fn parse(mut input: &BStr, delegate: &mut impl Delegate) -> Result<(), Exn<Error>> {    use delegate::{Kind, Revision};    let mut delegate = InterceptRev::new(delegate);    let mut prev_kind = None;    if let Some(b'^') = input.first() {        input = next(input).1;        let kind = spec::Kind::ExcludeReachable;        delegate            .kind(kind)            .or_raise(|| Error::new(format!("delegate.kind({kind:?}) failed")))?;        prev_kind = kind.into();    }     let mut found_revision;    (input, found_revision) = {        let rest = revision(input, &mut delegate)?;        (rest, rest != input)    };    if delegate.done {        return if input.is_empty() {            Ok(())        } else {            Err(Error::new_with_input("unconsumed input", input).raise())        };    }    if let Some((rest, kind)) = try_range(input) {        if let Some(prev_kind) = prev_kind {            return Err(Error::new(format!(                "cannot set spec kind more than once (was {prev_kind:?}, now {kind:?})"            ))            .raise());        }        if !found_revision {            delegate                .find_ref("HEAD".into())                .or_raise(|| Error::new("delegate did not find the HEAD reference"))?;        }        delegate            .kind(kind)            .or_raise(|| Error::new(format!("delegate.kind({kind:?}) failed")))?;        (input, found_revision) = {            let remainder = revision(rest.as_bstr(), &mut delegate)?;            (remainder, remainder != rest)        };        if !found_revision {            delegate                .find_ref("HEAD".into())                .or_raise(|| Error::new("delegate did not find the HEAD reference"))?;        }    }     if input.is_empty() {        delegate            .done()            .or_raise(|| Error::new("No revision was produced after all input was consumed"))    } else {        Err(Error::new_with_input("unconsumed input", input).raise())    }} mod intercept {    use crate::spec::parse::{Delegate, delegate};    use bstr::{BStr, BString};    use gix_error::Exn;     #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]    pub(crate) enum PrefixHintOwned {        MustBeCommit,        DescribeAnchor { ref_name: BString, generation: usize },    }     impl PrefixHintOwned {        pub fn to_ref(&self) -> delegate::PrefixHint<'_> {            match self {                PrefixHintOwned::MustBeCommit => delegate::PrefixHint::MustBeCommit,                PrefixHintOwned::DescribeAnchor { ref_name, generation } => delegate::PrefixHint::DescribeAnchor {                    ref_name: ref_name.as_ref(),                    generation: *generation,                },            }        }    }     impl<'a> From<delegate::PrefixHint<'a>> for PrefixHintOwned {        fn from(v: delegate::PrefixHint<'a>) -> Self {            match v {                delegate::PrefixHint::MustBeCommit => PrefixHintOwned::MustBeCommit,                delegate::PrefixHint::DescribeAnchor { generation, ref_name } => PrefixHintOwned::DescribeAnchor {                    ref_name: ref_name.to_owned(),                    generation,                },            }        }    }     pub(crate) struct InterceptRev<'a, T> {        pub inner: &'a mut T,        pub last_ref: Option<BString>, // TODO: smallvec to save the unnecessary allocation? Can't keep ref due to lifetime constraints in traits        pub last_prefix: Option<(gix_hash::Prefix, Option<PrefixHintOwned>)>,        pub done: bool,    }     impl<'a, T> InterceptRev<'a, T>    where        T: Delegate,    {        pub fn new(delegate: &'a mut T) -> Self {            InterceptRev {                inner: delegate,                last_ref: None,                last_prefix: None,                done: false,            }        }    }     impl<T> Delegate for InterceptRev<'_, T>    where        T: Delegate,    {        fn done(&mut self) -> Result<(), Exn> {            self.done = true;            self.inner.done()        }    }     impl<T> delegate::Revision for InterceptRev<'_, T>    where        T: Delegate,    {        fn find_ref(&mut self, name: &BStr) -> Result<(), Exn> {            self.last_ref = name.to_owned().into();            self.inner.find_ref(name)        }         fn disambiguate_prefix(            &mut self,            prefix: gix_hash::Prefix,            hint: Option<delegate::PrefixHint<'_>>,        ) -> Result<(), Exn> {            self.last_prefix = Some((prefix, hint.map(Into::into)));            self.inner.disambiguate_prefix(prefix, hint)        }         fn reflog(&mut self, query: delegate::ReflogLookup) -> Result<(), Exn> {            self.inner.reflog(query)        }         fn nth_checked_out_branch(&mut self, branch_no: usize) -> Result<(), Exn> {            self.inner.nth_checked_out_branch(branch_no)        }         fn sibling_branch(&mut self, kind: delegate::SiblingBranch) -> Result<(), Exn> {            self.inner.sibling_branch(kind)        }    }     impl<T> delegate::Navigate for InterceptRev<'_, T>    where        T: Delegate,    {        fn traverse(&mut self, kind: delegate::Traversal) -> Result<(), Exn> {            self.inner.traverse(kind)        }         fn peel_until(&mut self, kind: delegate::PeelTo<'_>) -> Result<(), Exn> {            self.inner.peel_until(kind)        }         fn find(&mut self, regex: &BStr, negated: bool) -> Result<(), Exn> {            self.inner.find(regex, negated)        }         fn index_lookup(&mut self, path: &BStr, stage: u8) -> Result<(), Exn> {            self.inner.index_lookup(path, stage)        }    }     impl<T> delegate::Kind for InterceptRev<'_, T>    where        T: Delegate,    {        fn kind(&mut self, kind: crate::spec::Kind) -> Result<(), Exn> {            self.inner.kind(kind)        }    }}use intercept::InterceptRev; trait ResultExt2 {    fn or_else_none<F>(self, f: F) -> Option<()>    where        F: FnOnce(Exn);} impl ResultExt2 for Result<(), Exn> {    fn or_else_none<F>(self, f: F) -> Option<()>    where        F: FnOnce(Exn),    {        match self {            Ok(()) => Some(()),            Err(err) => {                f(err);                None            }        }    }} fn try_set_prefix(    delegate: &mut impl Delegate,    hex_name: &BStr,    hint: Option<delegate::PrefixHint<'_>>,    errors: &mut Vec<Exn>,) -> Option<()> {    gix_hash::Prefix::from_hex(hex_name.to_str().expect("hexadecimal only"))        .ok()        .and_then(|prefix| {            delegate                .disambiguate_prefix(prefix, hint)                .or_else_none(|err| errors.push(err))        })} fn long_describe_prefix(name: &BStr) -> Option<(&BStr, delegate::PrefixHint<'_>)> {    let mut iter = name.rsplit(|b| *b == b'-');    let candidate = iter.by_ref().find_map(|substr| {        if substr.first()? != &b'g' {            return None;        }        let rest = substr.get(1..)?;        rest.iter().all(u8::is_ascii_hexdigit).then(|| rest.as_bstr())    })?;     let candidate = iter.clone().any(|token| !token.is_empty()).then_some(candidate);    let hint = iter        .next()        .and_then(|generation| {            generation                .to_str()                .ok()                .and_then(|generation| usize::from_str(generation).ok())        })        .and_then(|generation| {            iter.next().map(|token| {                let last_token_len = token.len();                let first_token_ptr = iter.next_back().map_or(token.as_ptr(), <[_]>::as_ptr);                // SAFETY: both pointers are definitely part of the same object                #[expect(unsafe_code)]                let prior_tokens_len: usize = unsafe { token.as_ptr().offset_from(first_token_ptr) }                    .try_into()                    .expect("positive value");                delegate::PrefixHint::DescribeAnchor {                    ref_name: name[..prior_tokens_len + last_token_len].as_bstr(),                    generation,                }            })        })        .unwrap_or(delegate::PrefixHint::MustBeCommit);     candidate.map(|c| (c, hint))} fn short_describe_prefix(name: &BStr) -> Option<&BStr> {    let mut iter = name.split(|b| *b == b'-');    let candidate = iter        .next()        .and_then(|prefix| prefix.iter().all(u8::is_ascii_hexdigit).then(|| prefix.as_bstr()));    (iter.count() == 1).then_some(candidate).flatten()} type InsideParensRestConsumed<'a> = (std::borrow::Cow<'a, BStr>, &'a BStr, usize);fn parens(input: &[u8]) -> Result<Option<InsideParensRestConsumed<'_>>, Error> {    if input.first() != Some(&b'{') {        return Ok(None);    }    let mut open_braces = 0;    let mut ignore_next = false;    let mut skip_list = Vec::new();    for (idx, b) in input.iter().enumerate() {        match *b {            b'{' => {                if ignore_next {                    ignore_next = false;                } else {                    open_braces += 1;                }            }            b'}' => {                if ignore_next {                    ignore_next = false;                } else {                    open_braces -= 1;                }            }            b'\\' => {                skip_list.push(idx);                if ignore_next {                    skip_list.pop();                    ignore_next = false;                } else {                    ignore_next = true;                }            }            _ => {                if ignore_next {                    skip_list.pop();                }                ignore_next = false;            }        }        if open_braces == 0 {            let inner: std::borrow::Cow<'_, _> = if skip_list.is_empty() {                input[1..idx].as_bstr().into()            } else {                let mut from = 1;                let mut buf = BString::default();                for next in skip_list.into_iter() {                    buf.push_str(&input[from..next]);                    from = next + 1;                }                if let Some(rest) = input.get(from..idx) {                    buf.push_str(rest);                }                buf.into()            };            return Ok(Some((inner, input[idx + 1..].as_bstr(), idx + 1)));        }    }    Err(Error::new_with_input("unclosed brace pair", input))} fn try_parse<T: FromStr + PartialEq + Default>(input: &BStr) -> Result<Option<T>, Error> {    input        .to_str()        .ok()        .and_then(|n| {            n.parse().ok().map(|n| {                if n == T::default() && input[0] == b'-' {                    return Err(Error::new_with_input(                        "negative zero is invalid - remove the minus sign",                        input,                    ));                }                Ok(n)            })        })        .transpose()} fn revision<'a, T>(mut input: &'a BStr, delegate: &mut InterceptRev<'_, T>) -> Result<&'a BStr, Exn<Error>>where    T: Delegate,{    use delegate::{Navigate, Revision};    fn consume_all(res: Result<(), Exn>, err: impl FnOnce() -> String) -> Result<&'static BStr, Exn<Error>> {        res.map(|_| "".into()).or_raise(|| Error::new(err()))    }    match input.as_bytes() {        [b':'] => {            return Err(                Error::new("':' must be followed by either slash and regex or path to lookup in HEAD tree").raise(),            );        }        [b':', b'/'] => return Err(Error::new("':/' must be followed by a regular expression").raise()),        [b':', b'/', regex @ ..] => {            let (regex, negated) = parse_regex_prefix(regex.as_bstr())?;            if regex.is_empty() {                return Err(Error::new_with_input("unconsumed input", input).raise());            }            return consume_all(delegate.find(regex, negated), || {                format!("Delegate couldn't find '{regex}' (negated: {negated})")            });        }        [b':', b'0', b':', path @ ..] => {            return consume_all(delegate.index_lookup(path.as_bstr(), 0), || {                format!("Couldn't find index '{path}' stage 0", path = path.as_bstr())            });        }        [b':', b'1', b':', path @ ..] => {            return consume_all(delegate.index_lookup(path.as_bstr(), 1), || {                format!("Couldn't find index '{path}' stage 1", path = path.as_bstr())            });        }        [b':', b'2', b':', path @ ..] => {            return consume_all(delegate.index_lookup(path.as_bstr(), 2), || {                format!("Couldn't find index '{path}' stage 2", path = path.as_bstr())            });        }        [b':', path @ ..] => {            return consume_all(delegate.index_lookup(path.as_bstr(), 0), || {                format!("Couldn't find index '{path}' stage 0 (implicit)", path = path.as_bstr())            });        }        _ => {}    }     let mut sep_pos = None;    let mut consecutive_hex_chars = Some(0);    {        let mut cursor = input;        let mut ofs = 0;        const SEPARATORS: &[u8] = b"~^:.";        while let Some((pos, b)) = cursor.iter().enumerate().find(|(pos, b)| {            if **b == b'@' {                if cursor.len() == 1 {                    return true;                }                let next = cursor.get(pos + 1);                let next_next = cursor.get(pos + 2);                if *pos != 0 && (next, next_next) == (Some(&b'.'), Some(&b'.')) {                    return false;                }                next == Some(&b'{') || next.is_some_and(|b| SEPARATORS.contains(b))            } else if SEPARATORS.contains(b) {                true            } else {                if let Some(num) = consecutive_hex_chars.as_mut() {                    if b.is_ascii_hexdigit() {                        *num += 1;                    } else {                        consecutive_hex_chars = None;                    }                }                false            }        }) {            if *b != b'.' || cursor.get(pos + 1) == Some(&b'.') {                sep_pos = Some(ofs + pos);                break;            }            ofs += pos + 1;            cursor = &cursor[pos + 1..];        }    }     let name = &input[..sep_pos.unwrap_or(input.len())].as_bstr();    let mut sep = sep_pos.map(|pos| input[pos]);    let mut has_ref_or_implied_name = name.is_empty();    if name.is_empty() && sep == Some(b'@') && sep_pos.and_then(|pos| input.get(pos + 1)) != Some(&b'{') {        delegate            .find_ref("HEAD".into())            .or_raise(|| Error::new("delegate did not find the HEAD reference"))?;        sep_pos = sep_pos.map(|pos| pos + 1);        sep = match sep_pos.and_then(|pos| input.get(pos).copied()) {            None => return Ok("".into()),            Some(pos) => Some(pos),        };    } else {        let mut errors = Vec::new();        (consecutive_hex_chars.unwrap_or(0) >= gix_hash::Prefix::MIN_HEX_LEN)            .then(|| try_set_prefix(delegate, name, None, &mut errors))            .flatten()            .or_else(|| {                let (prefix, hint) = long_describe_prefix(name)                    .map(|(c, h)| (c, Some(h)))                    .or_else(|| short_describe_prefix(name).map(|c| (c, None)))?;                try_set_prefix(delegate, prefix, hint, &mut errors)            })            .or_else(|| {                name.is_empty().then_some(()).or_else(|| {                    #[expect(                        clippy::let_unit_value,                        reason = "the binding keeps generated async and blocking code structurally consistent"                    )]                    {                        let res = delegate.find_ref(name).or_else_none(|err| {                            errors.push(err);                        })?;                        has_ref_or_implied_name = true;                        res.into()                    }                })            })            .ok_or_else(|| Error::new_with_input("couldn't parse revision", input).raise_all(errors))?;    }     input = {        if let Some(b'@') = sep {            let past_sep = input[sep_pos.map_or(input.len(), |pos| pos + 1)..].as_bstr();            let (nav, rest, _consumed) = parens(past_sep)?.ok_or_else(|| {                Error::new_with_input(                    "@ character must be standalone or followed by {<content>}",                    &input[sep_pos.unwrap_or(input.len())..],

Lines 1–500 of 788.