It needs cleanup for sure.
Unified · Split
| @@ -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 git | 113 | // 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(()) |
| @@ -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 | ||
| 66 | fn try_parse(spec: &str) -> Result<Recorder, spec::parse::Error> { | ||
| 67 | try_parse_opts(spec, Default::default()) | ||
| 68 | } | ||
| 69 | |||
| 66 | fn try_parse_opts(spec: &str, options: Options) -> Result<Recorder, spec::parse::Error> { | 70 | fn 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] | ||
| 74 | fn empty_specs_are_valid() { | 77 | fn empty_specs_are_valid() { |
| 75 | // they should of course be invalid for the delegate. CLIs may pre-process the input as well if they wish | 78 | // 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] | ||
| 85 | fn all_characters_are_taken_verbatim_which_includes_whitespace() { | 87 | fn 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 | ||
| 92 | mod revision { | 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 | #[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: assertion | 114 | 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()); |