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
22 22
23 #[derive(Debug, thiserror::Error)]23 #[derive(Debug, thiserror::Error)]
24 pub enum Error {24 pub enum Error {
25 #[error("The @ character is either standing alone or followed by `{{<content>}}`, got {:?}", .input)]
26 AtNeedsCurlyBrackets { input: BString },
25 #[error("A portion of the input could not be parsed: {:?}", .input)]27 #[error("A portion of the input could not be parsed: {:?}", .input)]
26 UnconsumedInput { input: BString },28 UnconsumedInput { input: BString },
27 #[error("The delegate didn't indicate success - check delegate for more information")]29 #[error("The delegate didn't indicate success - check delegate for more information")]
@@ -57,11 +59,46 @@ pub mod parse { · 29 unmodified lines
57 use crate::spec::parse::{Delegate, Error};59 use crate::spec::parse::{Delegate, Error};
58 use bstr::{BStr, ByteSlice};60 use bstr::{BStr, ByteSlice};
59 61
60 pub fn revision<'a>(mut input: &'a BStr, delegate: &mut impl Delegate) -> Result<&'a BStr, Error> {62 fn parse_parens(_input: &[u8]) -> Option<(&BStr, &BStr)> {
61 if let Some(rest) = input.strip_prefix(b"@").or_else(|| input.strip_prefix(b"HEAD")) {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'@') {
62 delegate.resolve_ref("HEAD".into()).ok_or(Error::Delegate)?;79 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)?;
64 }82 }
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 };
65 Ok(input)102 Ok(input)
66 }103 }
67 104
@@ -75,9 +112,8 @@ pub mod parse { · 7 unmodified lines
75 if let Some((rest, kind)) = try_range(input) {112 if let Some((rest, kind)) = try_range(input) {
76 // TODO: protect against double-kind calls, invalid for git113 // TODO: protect against double-kind calls, invalid for git
77 delegate.kind(kind).ok_or(Error::Delegate)?;114 delegate.kind(kind).ok_or(Error::Delegate)?;
78 input = rest.as_bstr();115 input = revision(rest.as_bstr(), delegate)?;
79 }116 }
80 input = revision(input, delegate)?;
81 117
82 if input.is_empty() {118 if input.is_empty() {
83 Ok(())119 Ok(())
git-revision/tests/spec/parse.rs+15−9
@@ -63,6 +63,10 @@ fn parse(spec: &str) -> Recorder { · 62 unmodified lines
63 try_parse_opts(spec, Options::default()).unwrap()63 try_parse_opts(spec, Options::default()).unwrap()
64}64}
65 65
66fn try_parse(spec: &str) -> Result<Recorder, spec::parse::Error> {
67 try_parse_opts(spec, Default::default())
68}
69
66fn try_parse_opts(spec: &str, options: Options) -> Result<Recorder, spec::parse::Error> {70fn try_parse_opts(spec: &str, options: Options) -> Result<Recorder, spec::parse::Error> {
67 let mut rec = Recorder::with(options);71 let mut rec = Recorder::with(options);
68 spec::parse(spec.into(), &mut rec)?;72 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
70}74}
71 75
72#[test]76#[test]
73#[ignore]
74fn empty_specs_are_valid() {77fn empty_specs_are_valid() {
75 // they should of course be invalid for the delegate. CLIs may pre-process the input as well if they wish78 // they should of course be invalid for the delegate. CLIs may pre-process the input as well if they wish
76 // but git itself doesn't do that.79 // but git itself doesn't do that.
77 for spec in ["", " ", "\n\t"] {80 for spec in ["", " ", "\n\t"] {
78 let rec = parse(spec);81 let rec = parse(spec);
79 assert_eq!(rec.calls, 0);82 assert_eq!(rec.calls, 1);
80 }83 }
81}84}
82 85
83#[test]86#[test]
84#[ignore]
85fn all_characters_are_taken_verbatim_which_includes_whitespace() {87fn all_characters_are_taken_verbatim_which_includes_whitespace() {
86 let spec = " HEAD \n";88 let spec = " HEAD \n";
87 let rec = parse(spec);89 let rec = parse(spec);
@@ -90,7 +92,8 @@ fn all_characters_are_taken_verbatim_which_includes_whitespace() { · 2 unmodified lines
90}92}
91 93
92mod revision {94mod revision {
93 use crate::spec::parse::{parse, try_parse_opts};95 use crate::spec::parse::{parse, try_parse};
96 use git_revision::spec;
94 97
95 #[test]98 #[test]
96 fn at_by_iteself_is_shortcut_for_head() {99 fn at_by_iteself_is_shortcut_for_head() {
@@ -100,10 +103,15 @@ mod revision { · 3 unmodified lines
100 }103 }
101 104
102 #[test]105 #[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]
104 fn lonely_at_after_ref_is_invalid() {112 fn lonely_at_after_ref_is_invalid() {
105 let _err = try_parse_opts("HEAD@", Default::default()).unwrap_err();113 let err = try_parse("HEAD@").unwrap_err();
106 // TODO: assertion114 assert!(matches!(err, spec::parse::Error::AtNeedsCurlyBrackets {input} if input == ""));
107 }115 }
108 116
109 #[test]117 #[test]
@@ -114,7 +122,6 @@ mod revision { · 4 unmodified lines
114 }122 }
115 123
116 #[test]124 #[test]
117 #[ignore]
118 fn refname_with_head_prefix() {125 fn refname_with_head_prefix() {
119 let rec = parse("HEADfake");126 let rec = parse("HEADfake");
120 assert!(rec.kind.is_none());127 assert!(rec.kind.is_none());
@@ -122,7 +129,6 @@ mod revision { · 1 unmodified line
122 }129 }
123 130
124 #[test]131 #[test]
125 #[ignore]
126 fn full_head_ref_name() {132 fn full_head_ref_name() {
127 let rec = parse("refs/heads/main");133 let rec = parse("refs/heads/main");
128 assert!(rec.kind.is_none());134 assert!(rec.kind.is_none());