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<BString>; 2], prefix: [Option<git_hash::Prefix>; 2], prefix_hint: [Option<PrefixHintOwned>; 2], current_branch_reflog_entry: [Option<String>; 2], nth_checked_out_branch: [Option<usize>; 2], sibling_branch: [Option<String>; 2], index_lookups: Vec<(BString, u8)>, // navigation traversal: Vec<delegate::Traversal>, peel_to: Vec<PeelToOwned>, patterns: Vec<(BString, bool)>, // range kind: Option<spec::Kind>, order: Vec<Call>, 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<T: std::fmt::Debug>(fn_name: &str, store: &mut [Option<T>; 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<delegate::PrefixHint<'_>>) -> 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<Recorder, spec::parse::Error> { try_parse_opts(spec, Default::default())} fn try_parse_opts(spec: &str, options: Options) -> Result<Recorder, spec::parse::Error> { 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;
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<BString>; 2],
prefix: [Option<git_hash::Prefix>; 2],
prefix_hint: [Option<PrefixHintOwned>; 2],
current_branch_reflog_entry: [Option<String>; 2],
nth_checked_out_branch: [Option<usize>; 2],
sibling_branch: [Option<String>; 2],
index_lookups: Vec<(BString, u8)>,
// navigation
traversal: Vec<delegate::Traversal>,
peel_to: Vec<PeelToOwned>,
patterns: Vec<(BString, bool)>,
// range
kind: Option<spec::Kind>,
order: Vec<Call>,
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<T: std::fmt::Debug>(fn_name: &str, store: &mut [Option<T>; 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<delegate::PrefixHint<'_>>) -> 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<Recorder, spec::parse::Error> {
try_parse_opts(spec, Default::default())
}
fn try_parse_opts(spec: &str, options: Options) -> Result<Recorder, spec::parse::Error> {
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;