History

half-decent parsing of ref-names with preparation for parenthesis handling (#427)

It needs cleanup for sure.

Sebastian Thiel committed 4 years ago

Commit 98669861 parent 13c07f42 files · +56 −14Browse files

Unified · Split

2 files
  1. git-revision/src/spec.rs +41 −5
  2. git-revision/tests/spec/parse.rs +15 −9
git-revision/src/spec.rs+41−5
@@ -22,6 +22,8 @@ pub mod parse { · 21 unmodified lines
2222
2323 #[derive(Debug, thiserror::Error)]
2424 pub enum Error {
25+ #[error("The @ character is either standing alone or followed by `{{<content>}}`, got {:?}", .input)]
26+ AtNeedsCurlyBrackets { input: BString },
2527 #[error("A portion of the input could not be parsed: {:?}", .input)]
2628 UnconsumedInput { input: BString },
2729 #[error("The delegate didn't indicate success - check delegate for more information")]
@@ -57,11 +59,46 @@ pub mod parse { · 29 unmodified lines
5759 use crate::spec::parse::{Delegate, Error};
5860 use bstr::{BStr, ByteSlice};
5961
60− pub fn revision<'a>(mut input: &'a BStr, delegate: &mut impl Delegate) -> Result<&'a BStr, Error> {
61− if let Some(rest) = input.strip_prefix(b"@").or_else(|| input.strip_prefix(b"HEAD")) {
62+ fn parse_parens(_input: &[u8]) -> Option<(&BStr, &BStr)> {
63+ None
64+ }
65+
66+ fn revision<'a>(mut input: &'a BStr, delegate: &mut impl Delegate) -> Result<&'a BStr, Error> {
67+ let mut cursor = input;
68+ let mut sep = None;
69+ while let Some((pos, b)) = cursor.bytes().enumerate().find(|(_, b)| b"@~^:.".contains(b)) {
70+ if b != b'.' || cursor.get(pos + 1) == Some(&b'.') {
71+ sep = Some((pos, b));
72+ break;
73+ }
74+ cursor = &input[pos + 1..];
75+ }
76+
77+ let name = &input[..sep.map(|(pos, _)| pos).unwrap_or_else(|| input.len())].as_bstr();
78+ if name.is_empty() && sep.map(|(_, b)| b) == Some(b'@') {
6279 delegate.resolve_ref("HEAD".into()).ok_or(Error::Delegate)?;
63− input = rest.as_bstr();
80+ } else {
81+ delegate.resolve_ref(name).ok_or(Error::Delegate)?;
6482 }
83+
84+ let past_sep = input[sep.map(|(pos, _)| pos + 1).unwrap_or(input.len())..].as_bstr();
85+ let sep_pos = sep.map(|(pos, _)| pos);
86+ let sep = sep.map(|(_, b)| b);
87+ input = match sep {
88+ Some(b'@') => {
89+ match parse_parens(past_sep).ok_or_else(|| Error::AtNeedsCurlyBrackets { input: past_sep.into() }) {
90+ Ok((_spec, rest)) => rest,
91+ Err(_) if name.is_empty() => past_sep,
92+ Err(err) => return Err(err),
93+ }
94+ }
95+ Some(b'~') => todo!("~"),
96+ Some(b'^') => todo!("^"),
97+ Some(b':') => todo!(":"),
98+ Some(b'.') => input[sep_pos.unwrap_or(input.len())..].as_bstr(),
99+ None => past_sep,
100+ Some(unknown) => unreachable!("BUG: found unknown separation character {:?}", unknown),
101+ };
65102 Ok(input)
66103 }
67104
@@ -75,9 +112,8 @@ pub mod parse { · 7 unmodified lines
75112 if let Some((rest, kind)) = try_range(input) {
76113 // TODO: protect against double-kind calls, invalid for git
77114 delegate.kind(kind).ok_or(Error::Delegate)?;
78− input = rest.as_bstr();
115+ input = revision(rest.as_bstr(), delegate)?;
79116 }
80− input = revision(input, delegate)?;
81117
82118 if input.is_empty() {
83119 Ok(())
git-revision/tests/spec/parse.rs+15−9
@@ -63,6 +63,10 @@ fn parse(spec: &str) -> Recorder { · 62 unmodified lines
6363 try_parse_opts(spec, Options::default()).unwrap()
6464 }
6565
66+fn try_parse(spec: &str) -> Result<Recorder, spec::parse::Error> {
67+ try_parse_opts(spec, Default::default())
68+}
69+
6670 fn try_parse_opts(spec: &str, options: Options) -> Result<Recorder, spec::parse::Error> {
6771 let mut rec = Recorder::with(options);
6872 spec::parse(spec.into(), &mut rec)?;
@@ -70,18 +74,16 @@ fn try_parse_opts(spec: &str, options: Options) -> Result<Recorder, spec::parse: · 1 unmodified line
7074 }
7175
7276 #[test]
73−#[ignore]
7477 fn empty_specs_are_valid() {
7578 // they should of course be invalid for the delegate. CLIs may pre-process the input as well if they wish
7679 // but git itself doesn't do that.
7780 for spec in ["", " ", "\n\t"] {
7881 let rec = parse(spec);
79− assert_eq!(rec.calls, 0);
82+ assert_eq!(rec.calls, 1);
8083 }
8184 }
8285
8386 #[test]
84−#[ignore]
8587 fn all_characters_are_taken_verbatim_which_includes_whitespace() {
8688 let spec = " HEAD \n";
8789 let rec = parse(spec);
@@ -90,7 +92,8 @@ fn all_characters_are_taken_verbatim_which_includes_whitespace() { · 2 unmodified lines
9092 }
9193
9294 mod revision {
93− use crate::spec::parse::{parse, try_parse_opts};
95+ use crate::spec::parse::{parse, try_parse};
96+ use git_revision::spec;
9497
9598 #[test]
9699 fn at_by_iteself_is_shortcut_for_head() {
@@ -100,10 +103,15 @@ mod revision { · 3 unmodified lines
100103 }
101104
102105 #[test]
103− #[ignore]
106+ fn multiple_ats_are_invalid_but_may_cause_callbacks() {
107+ let err = try_parse("@@").unwrap_err();
108+ assert!(matches!(err, spec::parse::Error::UnconsumedInput {input} if input == "@"));
109+ }
110+
111+ #[test]
104112 fn lonely_at_after_ref_is_invalid() {
105− let _err = try_parse_opts("HEAD@", Default::default()).unwrap_err();
106− // TODO: assertion
113+ let err = try_parse("HEAD@").unwrap_err();
114+ assert!(matches!(err, spec::parse::Error::AtNeedsCurlyBrackets {input} if input == ""));
107115 }
108116
109117 #[test]
@@ -114,7 +122,6 @@ mod revision { · 4 unmodified lines
114122 }
115123
116124 #[test]
117− #[ignore]
118125 fn refname_with_head_prefix() {
119126 let rec = parse("HEADfake");
120127 assert!(rec.kind.is_none());
@@ -122,7 +129,6 @@ mod revision { · 1 unmodified line
122129 }
123130
124131 #[test]
125− #[ignore]
126132 fn full_head_ref_name() {
127133 let rec = parse("refs/heads/main");
128134 assert!(rec.kind.is_none());