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