use git_object::bstr::{BStr, BString}; use git_revision::{ spec, spec::parse::{delegate, Delegate}, }; #[derive(Default, Debug, Eq, PartialEq, Ord, PartialOrd)] struct Options { reject_kind: bool, reject_prefix: bool, no_internal_assertions: bool, } #[derive(Default, Debug, Eq, PartialEq, Ord, PartialOrd)] struct Recorder { // anchors find_ref: [Option; 2], prefix: [Option; 2], prefix_hint: [Option; 2], current_branch_reflog_entry: [Option; 2], nth_checked_out_branch: [Option; 2], sibling_branch: [Option; 2], index_lookups: Vec<(BString, u8)>, // navigation traversal: Vec, peel_to: Vec, patterns: Vec<(BString, bool)>, // range kind: Option, order: Vec, calls: usize, opts: Options, done: bool, } #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] pub enum Call { FindRef, DisambiguatePrefix, Reflog, NthCheckedOutBranch, SiblingBranch, Traverse, PeelUntil, Find, IndexLookup, Kind, } #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] pub enum PeelToOwned { ObjectKind(git_object::Kind), ExistingObject, RecursiveTagObject, Path(BString), } #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] pub enum PrefixHintOwned { MustBeCommit, DescribeAnchor { ref_name: BString, generation: usize }, } impl Recorder { fn with(options: Options) -> Self { Recorder { opts: options, ..Default::default() } } fn get_ref(&self, idx: usize) -> &BStr { self.find_ref[idx].as_ref().map(|b| b.as_ref()).unwrap() } fn called(&mut self, f: Call) { self.calls += 1; self.order.push(f) } } fn set_val(fn_name: &str, store: &mut [Option; 2], val: T) -> Option<()> { for entry in store.iter_mut() { if entry.is_none() { *entry = Some(val); return Some(()); } } panic!("called {}() more than twice with '{:?}'", fn_name, val); } impl delegate::Revision for Recorder { fn find_ref(&mut self, input: &BStr) -> Option<()> { self.called(Call::FindRef); set_val("find_ref", &mut self.find_ref, input.into()) } fn disambiguate_prefix(&mut self, input: git_hash::Prefix, hint: Option>) -> Option<()> { self.called(Call::DisambiguatePrefix); if self.opts.reject_prefix { return None; } set_val("disambiguate_prefix", &mut self.prefix, input)?; if let Some(hint) = hint { set_val( "disambiguate_prefix", &mut self.prefix_hint, match hint { delegate::PrefixHint::DescribeAnchor { ref_name, generation } => PrefixHintOwned::DescribeAnchor { ref_name: ref_name.into(), generation, }, delegate::PrefixHint::MustBeCommit => PrefixHintOwned::MustBeCommit, }, )?; } Some(()) } fn reflog(&mut self, entry: delegate::ReflogLookup) -> Option<()> { self.called(Call::Reflog); set_val( "current_branch_reflog", &mut self.current_branch_reflog_entry, match entry { delegate::ReflogLookup::Entry(no) => no.to_string(), delegate::ReflogLookup::Date(time) => { let mut buf = Vec::new(); time.write_to(&mut buf).unwrap(); BString::from(buf).to_string() } }, ) } fn nth_checked_out_branch(&mut self, branch: usize) -> Option<()> { assert_ne!(branch, 0); self.called(Call::NthCheckedOutBranch); set_val("nth_checked_out_branch", &mut self.nth_checked_out_branch, branch) } fn sibling_branch(&mut self, kind: delegate::SiblingBranch) -> Option<()> { self.called(Call::SiblingBranch); set_val("sibling_branch", &mut self.sibling_branch, format!("{:?}", kind)) } } impl delegate::Navigate for Recorder { fn traverse(&mut self, kind: delegate::Traversal) -> Option<()> { self.called(Call::Traverse); self.traversal.push(kind); Some(()) } fn peel_until(&mut self, kind: delegate::PeelTo) -> Option<()> { self.called(Call::PeelUntil); self.peel_to.push(match kind { delegate::PeelTo::ObjectKind(kind) => PeelToOwned::ObjectKind(kind), delegate::PeelTo::ValidObject => PeelToOwned::ExistingObject, delegate::PeelTo::Path(path) => PeelToOwned::Path(path.into()), delegate::PeelTo::RecursiveTagObject => PeelToOwned::RecursiveTagObject, }); Some(()) } fn find(&mut self, regex: &BStr, negated: bool) -> Option<()> { self.called(Call::Find); self.patterns.push((regex.into(), negated)); Some(()) } fn index_lookup(&mut self, path: &BStr, stage: u8) -> Option<()> { self.called(Call::IndexLookup); self.index_lookups.push((path.into(), stage)); Some(()) } } impl delegate::Kind for Recorder { fn kind(&mut self, kind: spec::Kind) -> Option<()> { self.called(Call::Kind); if self.opts.reject_kind { return None; } if self.kind.is_none() { self.kind = Some(kind); } else if !self.opts.no_internal_assertions { panic!("called kind more than once with '{:?}'", kind); } Some(()) } } impl Delegate for Recorder { fn done(&mut self) { self.done = true; } } fn parse(spec: &str) -> Recorder { try_parse_opts(spec, Options::default()).unwrap() } fn try_parse(spec: &str) -> Result { try_parse_opts(spec, Default::default()) } fn try_parse_opts(spec: &str, options: Options) -> Result { let mut rec = Recorder::with(options); spec::parse(spec.into(), &mut rec)?; Ok(rec) } #[test] fn empty_specs_are_valid() { // they should of course be invalid for the delegate. CLIs may pre-process the input as well if they wish // but git itself doesn't do that. for spec in [" ", "\n\t"] { let rec = parse(spec); assert_eq!(rec.calls, 1); } let rec = parse(""); assert_eq!(rec.calls, 0, "but we do not bother to call the delegate with nothing"); assert!(rec.done); } #[test] fn all_characters_are_taken_verbatim_which_includes_whitespace() { let spec = " HEAD \n"; let rec = parse(spec); assert!(rec.kind.is_none()); assert_eq!(rec.get_ref(0), spec); } mod fuzz { use crate::spec::parse::{try_parse_opts, Options}; #[test] fn failures() { for spec in [ "@{6255520 day ago}: ", "|^--", "^^-^", "^^-", ":/!-", "A6a^-09223372036854775808", "^^^^^^-(", ] { drop( try_parse_opts( spec, Options { no_internal_assertions: true, ..Default::default() }, ) .unwrap_err(), ); } } } mod anchor; mod kind; mod navigate;