create src

This commit is contained in:
awfixer
2026-03-11 02:04:19 -07:00
commit 52f7a22bf2
2595 changed files with 402870 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
use crate::hex_to_id;
fn oid() -> gix_hash::ObjectId {
hex_to_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
}
fn oid2() -> gix_hash::ObjectId {
hex_to_id("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
}
#[test]
fn include() {
assert_eq!(
gix_revision::Spec::Include(oid()).to_string(),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
}
#[test]
fn exclude() {
assert_eq!(
gix_revision::Spec::Exclude(oid()).to_string(),
"^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
}
#[test]
fn range() {
assert_eq!(
gix_revision::Spec::Range {
from: oid(),
to: oid2()
}
.to_string(),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
);
}
#[test]
fn merge() {
assert_eq!(
gix_revision::Spec::Merge {
theirs: oid(),
ours: oid2()
}
.to_string(),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
);
}
#[test]
fn include_parents() {
assert_eq!(
gix_revision::Spec::IncludeOnlyParents(oid()).to_string(),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^@"
);
}
#[test]
fn exclude_parents() {
assert_eq!(
gix_revision::Spec::ExcludeParents(oid()).to_string(),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^!"
);
}

View File

@@ -0,0 +1,2 @@
mod display;
mod parse;

View File

@@ -0,0 +1,226 @@
use crate::spec::parse::{parse, try_parse};
#[test]
fn braces_must_be_closed() {
for unclosed_spec in ["@{something", "@{", "@{..@"] {
let err = try_parse(unclosed_spec).unwrap_err();
assert_eq!(
err.input.as_ref().map(std::convert::AsRef::as_ref),
Some(&unclosed_spec.as_bytes()[1..])
);
}
}
#[test]
#[cfg(target_pointer_width = "64")] // Only works this way on 64-bit systems.
fn fuzzed() {
let rec = parse("@{-9223372036854775808}");
assert_eq!(rec.nth_checked_out_branch, [Some(9223372036854775808), None]);
}
#[test]
fn reflog_by_entry_for_current_branch() {
for (spec, expected_entry) in [("@{0}", 0), ("@{42}", 42), ("@{00100}", 100)] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None);
assert_eq!(
rec.prefix[0], None,
"neither ref nor prefixes are set, straight to navigation"
);
assert_eq!(rec.current_branch_reflog_entry[0], Some(expected_entry.to_string()));
assert_eq!(rec.calls, 1);
}
}
#[test]
fn reflog_by_date_for_current_branch() {
let rec = parse("@{42 +0030}");
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None);
assert_eq!(
rec.prefix[0], None,
"neither ref nor prefixes are set, straight to navigation"
);
assert_eq!(rec.current_branch_reflog_entry[0], Some("42 +0030".to_string()));
assert_eq!(rec.calls, 1);
}
#[test]
fn reflog_by_unix_timestamp_for_current_branch() {
let rec = parse("@{100000000}");
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None);
assert_eq!(
rec.prefix[0], None,
"neither ref nor prefixes are set, straight to navigation"
);
assert_eq!(
rec.current_branch_reflog_entry[0],
Some("100000000 +0000".to_string()),
"This number is the first to count as date"
);
assert_eq!(rec.calls, 1);
let rec = parse("@{99999999}");
assert_eq!(
rec.current_branch_reflog_entry[0],
Some("99999999".to_string()),
"one less is an offset though"
);
}
#[test]
fn reflog_by_date_with_date_parse_failure() {
let err = try_parse("@{foo}").unwrap_err();
insta::assert_snapshot!(err, @"could not parse time for reflog lookup: foo");
}
#[test]
fn reflog_by_date_for_hash_is_invalid() {
for (spec, full_name) in [
("1234@{42 +0030}", "1234"),
("abcd-dirty@{42 +0030}", "abcd-dirty"),
("v1.2.3-0-g1234@{42 +0030}", "v1.2.3-0-g1234"),
] {
let err = try_parse(spec).unwrap_err();
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(full_name.as_bytes()));
assert!(err.message.contains("reflog entries require a ref name"));
}
}
#[test]
fn reflog_by_date_for_given_ref_name() {
for (spec, expected_ref) in [
("main@{42 +0030}", "main"),
("refs/heads/other@{42 +0030}", "refs/heads/other"),
("refs/worktree/feature/a@{42 +0030}", "refs/worktree/feature/a"),
] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), expected_ref);
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.current_branch_reflog_entry[0], Some("42 +0030".to_string()));
assert_eq!(rec.calls, 2, "first the ref, then the reflog entry");
}
}
#[test]
fn reflog_by_entry_for_given_ref_name() {
for (spec, expected_ref, expected_entry) in [
("main@{0}", "main", 0),
("refs/heads/other@{42}", "refs/heads/other", 42),
("refs/worktree/feature/a@{00100}", "refs/worktree/feature/a", 100),
] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), expected_ref);
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.current_branch_reflog_entry[0], Some(expected_entry.to_string()));
assert_eq!(rec.calls, 2, "first the ref, then the reflog entry");
}
}
#[test]
fn reflog_by_entry_for_hash_is_invalid() {
for (spec, full_name) in [
("1234@{0}", "1234"),
("abcd-dirty@{1}", "abcd-dirty"),
("v1.2.3-0-g1234@{2}", "v1.2.3-0-g1234"),
] {
let err = try_parse(spec).unwrap_err();
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(full_name.as_bytes()));
assert!(err.message.contains("reflog entries require a ref name"));
}
}
#[test]
fn sibling_branch_current_branch() {
for (spec, kind_name) in [("@{u}", "Upstream"), ("@{push}", "Push"), ("@{UPSTREAM}", "Upstream")] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None);
assert_eq!(rec.prefix[0], None, "neither ref nor prefix are explicitly set");
assert_eq!(rec.sibling_branch[0].as_deref(), Some(kind_name));
assert_eq!(rec.calls, 1);
}
}
#[test]
fn sibling_branch_for_branch_name() {
for (spec, ref_name, kind_name) in [
("r1@{U}", "r1", "Upstream"),
("refs/heads/main@{Push}", "refs/heads/main", "Push"),
("refs/worktree/private@{UpStreaM}", "refs/worktree/private", "Upstream"),
] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), ref_name);
assert_eq!(rec.prefix[0], None, "neither ref nor prefix are explicitly set");
assert_eq!(
rec.sibling_branch[0].as_deref(),
Some(kind_name),
"note that we do not know if something is a branch or not and make the call even if it would not be allowed. Configuration decides"
);
assert_eq!(rec.calls, 2);
}
}
#[test]
fn sibling_branch_for_hash_is_invalid() {
for (spec, full_name) in [
("1234@{u}", "1234"),
("abcd-dirty@{push}", "abcd-dirty"),
("v1.2.3-0-g1234@{upstream}", "v1.2.3-0-g1234"),
] {
let err = try_parse(spec).unwrap_err();
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(full_name.as_bytes()));
assert!(err.message.contains("sibling branches"));
}
}
#[test]
fn nth_checked_out_branch_for_refname_is_invalid() {
let err = try_parse("r1@{-1}").unwrap_err();
// its undefined how to handle negative numbers and specified ref names
insta::assert_snapshot!(err, @"reference name must be followed by positive numbers in @{n}: -1");
}
#[test]
fn nth_checked_out_branch() {
for (spec, expected_branch) in [("@{-1}", 1), ("@{-42}", 42), ("@{-00100}", 100)] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None);
assert_eq!(
rec.prefix[0], None,
"neither ref nor prefixes are set, straight to navigation"
);
assert_eq!(rec.nth_checked_out_branch[0], Some(expected_branch));
assert_eq!(rec.calls, 1);
}
}
#[test]
fn numbers_within_braces_cannot_be_negative_zero() {
let err = try_parse("@{-0}").unwrap_err();
// negative zero is not accepted, even though it could easily be defaulted to 0 which is a valid value
insta::assert_snapshot!(err, @"negative zero is invalid - remove the minus sign: -0");
}
#[test]
fn numbers_within_braces_can_be_positive_zero() {
assert_eq!(
parse("@{+0}"),
parse("@{0}"),
"+ prefixes are allowed though and the same as without it"
);
}

View File

@@ -0,0 +1,140 @@
use crate::spec::parse::{parse, try_parse};
#[test]
fn regex_parsing_ignores_ranges_as_opposed_to_git() {
for spec in [":/a..b", ":/a...b"] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(
rec.patterns,
vec![(spec[2..].into(), false)],
"git parses ranges but I think it's merely coincidental rather than intended, not doing so allows to use '.' more liberally"
);
}
}
#[test]
fn index_lookups_ignores_ranges_as_opposed_to_git() {
for spec in [":a..b", ":a...b"] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(
rec.index_lookups,
vec![(spec[1..].into(), 0)],
"git parses ranges but it's never useful as these specs only ever produce blob ids"
);
}
}
#[test]
fn various_forms_of_regex() {
for (spec, (regex, negated)) in [
(":/simple", ("simple", false)),
(":/!-negated", ("negated", true)),
(":/^from start", ("^from start", false)),
(":/!!leading exclamation mark", ("!leading exclamation mark", false)),
(":/with count{1}", ("with count{1}", false)),
(
":/all-consuming makes navigation impossible^5~10",
("all-consuming makes navigation impossible^5~10", false),
),
] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None);
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.patterns, vec![(regex.into(), negated)]);
assert_eq!(rec.calls, 1);
}
}
#[test]
fn regex_do_not_get_any_backslash_processing() {
for (spec, regex) in [(r#":/{"#, "{"), (r":/\{\}", r"\{\}"), (r":/\\\\\}", r"\\\\\}")] {
let rec = parse(spec);
assert_eq!(rec.patterns, vec![(regex.into(), false)]);
assert_eq!(rec.calls, 1);
}
}
#[test]
fn various_valid_index_lookups_by_path() {
for spec in [
":path",
":dir/path",
":./relative-to.cwd",
":../relative-to-cwd-too",
":navigation/is/ignored~10^^^",
] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None);
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.index_lookups, vec![(spec[1..].into(), 0)]);
assert_eq!(rec.peel_to, vec![], "peeling only works for anchors");
assert_eq!(rec.calls, 1);
}
}
#[test]
fn various_valid_index_lookups_by_path_and_stage() {
for (spec, path, stage) in [
(":0:path", "path", 0),
(":1:dir/path", "dir/path", 1),
(":2:dir/path@{part-of-path}", "dir/path@{part-of-path}", 2),
] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None);
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.index_lookups, vec![(path.into(), stage)]);
assert_eq!(rec.peel_to, vec![], "peeling only works for anchors");
assert_eq!(rec.calls, 1);
}
}
#[test]
fn empty_top_level_regex_are_invalid() {
let err = try_parse(":/").unwrap_err();
// git also can't do it, finds nothing instead. It could be the youngest commit in theory, but isn't
insta::assert_snapshot!(err, @"':/' must be followed by a regular expression");
}
#[test]
fn regex_with_empty_exclamation_mark_prefix_is_invalid() {
let err = try_parse(r#":/!hello"#).unwrap_err();
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(b"!hello".as_ref()));
insta::assert_snapshot!(err, @"need one character after /!, typically -: !hello");
}
#[test]
fn needs_suffix() {
let err = try_parse(":").unwrap_err();
// git also can't do it, finds nothing instead. It could be the youngest commit in theory, but isn't
insta::assert_snapshot!(err, @"':' must be followed by either slash and regex or path to lookup in HEAD tree");
}
#[test]
fn invalid_index_stage_is_part_of_path() {
for spec in [":4:file", ":5:file", ":01:file", ":10:file"] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None);
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.patterns, vec![]);
assert_eq!(
rec.index_lookups,
vec![(spec[1..].into(), 0)],
"these count as stage 0 lookups"
);
assert_eq!(rec.peel_to, vec![]);
assert_eq!(rec.calls, 1);
}
}

View File

@@ -0,0 +1,132 @@
use crate::spec::parse::{parse, try_parse_opts, Options, PrefixHintOwned};
fn anchor_hint() -> Option<PrefixHintOwned> {
Some(PrefixHintOwned::DescribeAnchor {
ref_name: "cargo-smart-release".into(),
generation: 679,
})
}
#[test]
fn full_format_parses_hash_portion_as_prefix() {
let rec = parse("cargo-smart-release-679-g3bee7fb");
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None, "references are not resolved in describe output");
assert_eq!(rec.prefix[0], Some(gix_hash::Prefix::from_hex("3bee7fb").unwrap()));
assert_eq!(rec.prefix_hint[0], anchor_hint());
assert_eq!(rec.calls, 1);
let rec = parse("v1.0-0-g3bee7fb");
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None, "references are not resolved in describe output");
assert_eq!(rec.prefix[0], Some(gix_hash::Prefix::from_hex("3bee7fb").unwrap()));
assert_eq!(
rec.prefix_hint[0],
Some(PrefixHintOwned::DescribeAnchor {
ref_name: "v1.0".into(),
generation: 0,
})
);
assert_eq!(rec.calls, 1);
}
#[test]
fn full_format_lookalikes_fallback_to_ref() {
let spec = "cargo-smart-release-679-g3bee7fb";
let rec = try_parse_opts(
spec,
Options {
reject_prefix: true,
..Default::default()
},
)
.unwrap();
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), spec);
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.prefix_hint[0], None);
assert_eq!(rec.calls, 2, "call prefix, then call ref");
}
#[test]
fn any_hash_without_suffix_and_prefix_g_is_assumed_to_be_describe_output() {
let spec = "foo--bar-gabcdef1";
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None);
assert_eq!(
rec.prefix[0],
Some(gix_hash::Prefix::from_hex("abcdef1").unwrap()),
"git does not parse very precisely here"
);
assert_eq!(rec.prefix_hint[0], Some(PrefixHintOwned::MustBeCommit));
assert_eq!(rec.calls, 1);
for invalid_describe in ["-gabcdef1", "gabcdef1"] {
let rec = parse(invalid_describe);
assert!(rec.kind.is_none());
assert_eq!(
rec.get_ref(0),
invalid_describe,
"we don't consider this a prefix from a describe block"
);
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.prefix_hint[0], None);
assert_eq!(rec.calls, 1);
}
}
#[test]
fn full_format_with_dirty_suffix_is_recognized() {
let rec = parse("cargo-smart-release-679-g3bee7fb-dirty");
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None, "git does not see this as prefix, we do");
assert_eq!(rec.prefix[0], Some(gix_hash::Prefix::from_hex("3bee7fb").unwrap()));
assert_eq!(rec.prefix_hint[0], anchor_hint());
assert_eq!(rec.calls, 1);
}
#[test]
fn partial_format_with_dirty_suffix_is_recognized() {
let spec = "abcdef1-dirty";
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.find_ref[0], None);
assert_eq!(
rec.prefix[0],
Some(gix_hash::Prefix::from_hex("abcdef1").unwrap()),
"git does not see this as prefix anymore, we do"
);
assert_eq!(
rec.prefix_hint[0], None,
"This leaves room for improvement as we could assume that -dirty belongs to a revision, so this could be PrefixHint::MustBeCommit"
);
assert_eq!(rec.calls, 1);
}
#[test]
fn partial_format_lookalikes_are_never_considered() {
let spec = "abcdef1-dirty-laundry";
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), spec);
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.calls, 1, "we don't even try the prefix");
}
#[test]
fn partial_format_with_dirty_suffix_lookalikes_are_treated_as_refs() {
let spec = "abcdef1-dirty";
let rec = try_parse_opts(
spec,
Options {
reject_prefix: true,
..Default::default()
},
)
.unwrap();
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), spec);
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.calls, 2);
}

View File

@@ -0,0 +1,66 @@
use crate::spec::parse::{parse, try_parse_opts, Options};
#[test]
fn short_hex_literals_are_considered_prefixes() {
let rec = parse("abCD");
assert!(rec.kind.is_none());
assert_eq!(
rec.find_ref[0], None,
"references are not resolved if prefix lookups succeed"
);
assert_eq!(rec.prefix[0], Some(gix_hash::Prefix::from_hex("abcd").unwrap()));
assert_eq!(rec.prefix_hint[0], None);
assert_eq!(rec.calls, 1);
let rec = parse("gabcd123");
assert!(rec.kind.is_none());
assert_eq!(
rec.get_ref(0),
"gabcd123",
"ref lookups are performed if it doesn't look like a hex sha"
);
assert_eq!(
rec.prefix[0], None,
"prefix lookups are not attempted at all (and they are impossible even)"
);
assert_eq!(rec.prefix_hint[0], None);
assert_eq!(rec.calls, 1);
}
#[test]
fn unresolvable_hex_literals_are_resolved_as_refs() {
let rec = try_parse_opts(
"abCD",
Options {
reject_prefix: true,
..Default::default()
},
)
.unwrap();
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "abCD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.prefix_hint[0], None);
assert_eq!(rec.calls, 2);
}
#[test]
fn hex_literals_that_are_too_long_are_resolved_as_refs() {
let spec = "abcd123456789abcd123456789abcd123456789abcd123456789abcd123456789abcd123456789abcd123456789";
let rec = try_parse_opts(
spec,
Options {
reject_prefix: true,
..Default::default()
},
)
.unwrap();
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), spec);
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.prefix_hint[0], None);
assert_eq!(
rec.calls, 1,
"we can't create a prefix from it, hence only ref resolution is attempted"
);
}

View File

@@ -0,0 +1,5 @@
mod at_symbol;
mod colon_symbol;
mod describe;
mod hash;
mod refnames;

View File

@@ -0,0 +1,68 @@
use crate::spec::parse::{parse, try_parse};
#[test]
fn at_by_itself_is_shortcut_for_head() {
let rec = parse("@");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEAD");
}
#[test]
fn at_is_allowed() {
for name in ["a@b", "@branch", "branch@", "@@", "@inner@"] {
let rec = parse(name);
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), name);
assert_eq!(rec.find_ref[1], None);
}
}
#[test]
fn at_in_ranges_is_allowed() {
let input = "@@@..";
let rec = parse(input);
assert_eq!(rec.kind, Some(gix_revision::spec::Kind::RangeBetween));
assert_eq!(rec.get_ref(0), "@@@");
assert_eq!(rec.get_ref(1), "HEAD");
let input = "@@...@@";
let rec = parse(input);
assert_eq!(rec.kind, Some(gix_revision::spec::Kind::ReachableToMergeBase));
assert_eq!(rec.get_ref(0), "@@");
assert_eq!(rec.get_ref(1), "@@");
}
#[test]
fn strange_revspecs_do_not_panic() {
let err = try_parse(".@.").unwrap_err();
insta::assert_snapshot!(err, @"@ character must be standalone or followed by {<content>}: @.");
}
#[test]
fn refname_head() {
let rec = parse("HEAD");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEAD");
}
#[test]
fn refname_tag() {
let spec = "v1.2.3.4-beta.1";
let rec = parse(spec);
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), spec);
}
#[test]
fn refname_with_head_prefix() {
let rec = parse("HEADfake");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEADfake");
}
#[test]
fn full_head_ref_name() {
let rec = parse("refs/heads/main");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "refs/heads/main");
}

View File

@@ -0,0 +1,425 @@
use crate::spec::parse::{try_parse, try_parse_opts, Options};
#[test]
fn cannot_declare_ranges_multiple_times() {
for invalid_spec in ["^HEAD..", "^HEAD..."] {
let err = try_parse(invalid_spec).unwrap_err().into_inner();
assert!(err.message.contains("cannot set spec kind more than once"));
}
}
#[test]
fn delegate_can_refuse_spec_kinds() {
let err = try_parse_opts(
"^HEAD",
Options {
reject_kind: true,
..Default::default()
},
)
.unwrap_err()
.into_inner();
// Delegates can refuse spec kind changes to abort parsing early in case they want single-specs only
insta::assert_snapshot!(err, @"delegate.kind(ExcludeReachable) failed");
}
mod include_parents {
use gix_revision::spec;
use crate::spec::parse::{kind::prefix, parse, try_parse, Call};
#[test]
fn trailing_caret_at_symbol() {
let rec = parse("HEAD^@");
assert_eq!(rec.kind.unwrap(), spec::Kind::IncludeReachableFromParents);
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.order, [Call::FindRef, Call::Kind]);
assert!(rec.done);
let rec = parse("abcd^@");
assert_eq!(rec.kind.unwrap(), spec::Kind::IncludeReachableFromParents);
assert_eq!(rec.prefix[0], prefix("abcd").into());
assert_eq!(rec.order, [Call::DisambiguatePrefix, Call::Kind]);
assert!(rec.done);
let rec = parse("r1^@");
assert_eq!(rec.kind.unwrap(), spec::Kind::IncludeReachableFromParents);
assert_eq!(rec.get_ref(0), "r1");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.order, [Call::FindRef, Call::Kind]);
assert!(rec.done);
}
#[test]
fn trailing_caret_exclamation_mark_must_end_the_input() {
let err = try_parse("r1^@~1").unwrap_err().into_inner();
assert!(err.message.contains("unconsumed input"));
}
}
mod exclude_parents {
use gix_revision::spec;
use crate::spec::parse::{kind::prefix, parse, try_parse, Call};
#[test]
fn freestanding() {
let rec = parse("^!");
assert_eq!(
rec.kind,
Some(gix_revision::spec::Kind::ExcludeReachable),
"the delegate has to be able to deal with this"
);
}
#[test]
fn trailing_caret_exclamation_mark() {
let rec = parse("HEAD^!");
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachableFromParents);
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.order, [Call::FindRef, Call::Kind]);
assert!(rec.done);
let rec = parse("abcd^!");
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachableFromParents);
assert_eq!(rec.prefix[0], prefix("abcd").into());
assert_eq!(rec.order, [Call::DisambiguatePrefix, Call::Kind]);
assert!(rec.done);
let rec = parse("r1^!");
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachableFromParents);
assert_eq!(rec.get_ref(0), "r1");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.order, [Call::FindRef, Call::Kind]);
assert!(rec.done);
}
#[test]
fn trailing_caret_exclamation_mark_must_end_the_input() {
let err = try_parse("r1^!~1").unwrap_err().into_inner();
assert!(err.message.contains("unconsumed input"));
}
}
mod exclusive {
use gix_revision::spec;
use crate::spec::parse::{kind::prefix, parse};
#[test]
fn freestanding() {
let rec = parse("^");
assert_eq!(
rec.kind,
Some(gix_revision::spec::Kind::ExcludeReachable),
"the delegate has to be able to deal with this"
);
}
#[test]
fn leading_caret() {
let rec = parse("^HEAD");
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachable);
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.calls, 2);
let rec = parse("^abcd");
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachable);
assert_eq!(rec.find_ref[0], None);
assert_eq!(rec.prefix[0], prefix("abcd").into());
assert_eq!(rec.calls, 2);
let rec = parse("^r1");
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachable);
assert_eq!(rec.get_ref(0), "r1");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.calls, 2);
let rec = parse("^hello-0-gabcd-dirty");
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachable);
assert_eq!(rec.find_ref[0], None);
assert_eq!(rec.prefix[0], prefix("abcd").into());
assert_eq!(rec.calls, 2);
}
}
mod range {
use gix_revision::{spec, spec::parse::delegate::Traversal};
use crate::spec::parse::{kind::prefix, parse, try_parse, Call};
#[test]
fn minus_with_n_omitted() {
let rec = parse("r1^-");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.get_ref(0), "r1");
assert_eq!(rec.traversal, [Traversal::NthParent(1)], "default is 1");
assert_eq!(rec.get_ref(1), "r1");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.order, [Call::FindRef, Call::Traverse, Call::Kind, Call::FindRef]);
assert!(rec.done);
let rec = parse("@^-");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.traversal, [Traversal::NthParent(1)], "default is 1");
assert_eq!(rec.get_ref(1), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.order, [Call::FindRef, Call::Traverse, Call::Kind, Call::FindRef]);
assert!(rec.done);
let rec = parse("abcd^-");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.prefix[0], prefix("abcd").into());
assert_eq!(rec.traversal, [Traversal::NthParent(1)], "default is 1");
assert_eq!(rec.prefix[1], prefix("abcd").into());
assert_eq!(
rec.order,
[
Call::DisambiguatePrefix,
Call::Traverse,
Call::Kind,
Call::DisambiguatePrefix
]
);
assert!(rec.done);
}
#[test]
fn minus_with_n() {
let rec = parse("r1^-42");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.get_ref(0), "r1");
assert_eq!(rec.traversal, [Traversal::NthParent(42)]);
assert_eq!(rec.get_ref(1), "r1");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.order, [Call::FindRef, Call::Traverse, Call::Kind, Call::FindRef]);
assert!(rec.done);
let rec = parse("@^-42");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.traversal, [Traversal::NthParent(42)]);
assert_eq!(rec.get_ref(1), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.order, [Call::FindRef, Call::Traverse, Call::Kind, Call::FindRef]);
assert!(rec.done);
let rec = parse("abcd^-42");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.prefix[0], prefix("abcd").into());
assert_eq!(rec.traversal, [Traversal::NthParent(42)]);
assert_eq!(rec.prefix[1], prefix("abcd").into());
assert_eq!(
rec.order,
[
Call::DisambiguatePrefix,
Call::Traverse,
Call::Kind,
Call::DisambiguatePrefix
]
);
assert!(rec.done);
}
#[test]
fn minus_with_n_omitted_has_to_end_there() {
let err = try_parse("r1^-^").unwrap_err().into_inner();
assert!(err.message.contains("unconsumed input"));
}
#[test]
fn minus_with_n_has_to_end_there() {
let err = try_parse("r1^-42^").unwrap_err().into_inner();
assert!(err.message.contains("unconsumed input"));
}
#[test]
fn minus_with_n_has_to_end_there_and_handle_range_suffix() {
let err = try_parse("r1^-42..").unwrap_err().into_inner();
assert!(err.message.contains("unconsumed input"));
}
#[test]
fn minus_with_n_omitted_has_to_end_there_and_handle_range_suffix() {
let err = try_parse("r1^-..").unwrap_err().into_inner();
assert!(err.message.contains("unconsumed input"));
}
#[test]
fn freestanding_dot_dot() {
let rec = parse("..");
assert_eq!(
rec.kind,
Some(gix_revision::spec::Kind::RangeBetween),
"the delegate has to be able to deal with this"
);
}
#[test]
fn trailing_dot_dot() {
let rec = parse("r1..");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.get_ref(0), "r1");
assert_eq!(rec.get_ref(1), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.order, [Call::FindRef, Call::Kind, Call::FindRef]);
}
#[test]
fn leading_dot_dot() {
let rec = parse("..r2");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.get_ref(1), "r2");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.order, [Call::FindRef, Call::Kind, Call::FindRef]);
}
#[test]
fn middle_dot_dot() {
let rec = parse("@..r2");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.get_ref(1), "r2");
assert_eq!(rec.calls, 3);
let rec = parse("r1..r2");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.get_ref(0), "r1");
assert_eq!(rec.get_ref(1), "r2");
assert_eq!(rec.calls, 3);
let rec = parse("abcd..1234");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.prefix[0], prefix("abcd").into());
assert_eq!(rec.prefix[1], prefix("1234").into());
assert_eq!(rec.calls, 3);
let rec = parse("r1..abcd");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.get_ref(0), "r1");
assert_eq!(rec.prefix[0], prefix("abcd").into());
assert_eq!(rec.calls, 3);
let rec = parse("abcd-dirty..v1.2-42-g1234");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.find_ref[0], None);
assert_eq!(rec.prefix[0], prefix("abcd").into());
assert_eq!(rec.prefix[1], prefix("1234").into());
assert_eq!(rec.calls, 3);
let rec = parse("v1.2-42-g1234..abcd-dirty");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.find_ref[0], None);
assert_eq!(rec.prefix[0], prefix("1234").into());
assert_eq!(rec.prefix[1], prefix("abcd").into());
assert_eq!(rec.calls, 3);
let rec = parse("v1.2.4@{1}~~^10..r1@{2}~10^2");
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
assert_eq!(rec.get_ref(0), "v1.2.4");
assert_eq!(rec.get_ref(1), "r1");
assert_eq!(&rec.prefix, &[None, None]);
assert_eq!(
rec.traversal,
[
Traversal::NthAncestor(1),
Traversal::NthAncestor(1),
Traversal::NthParent(10),
Traversal::NthAncestor(10),
Traversal::NthParent(2)
]
);
assert_eq!(rec.calls, 10);
assert!(rec.done);
}
}
mod mergebase {
use gix_revision::{spec, spec::parse::delegate::Traversal};
use crate::spec::parse::{kind::prefix, parse};
#[test]
fn freestanding_dot_dot_dot() {
let rec = parse("...");
assert_eq!(
rec.kind,
Some(gix_revision::spec::Kind::ReachableToMergeBase),
"the delegate has to be able to deal with this"
);
}
#[test]
fn trailing_dot_dot_dot() {
let rec = parse("HEAD...");
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.calls, 3);
}
#[test]
fn leading_dot_dot_dot() {
let rec = parse("...r2");
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.get_ref(1), "r2");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.calls, 3);
}
#[test]
fn middle_dot_dot_dot() {
let rec = parse("HEAD...@");
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.get_ref(1), "HEAD");
assert_eq!(rec.calls, 3);
let rec = parse("@...HEAD");
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.get_ref(1), "HEAD");
assert_eq!(rec.calls, 3);
let rec = parse("r1...abcd");
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
assert_eq!(rec.get_ref(0), "r1");
assert_eq!(rec.prefix[0], prefix("abcd").into());
assert_eq!(rec.calls, 3);
let rec = parse("v1.2.3-beta.1-42-g1234-dirty...abcd-dirty");
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
assert_eq!(rec.find_ref[0], None);
assert_eq!(rec.prefix[0], prefix("1234").into());
assert_eq!(rec.prefix[1], prefix("abcd").into());
assert_eq!(rec.calls, 3);
let rec = parse("r1@{1}~~^10...@{2}~10^2");
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
assert_eq!(rec.get_ref(0), "r1");
assert_eq!(rec.find_ref[1], None, "HEAD is implied");
assert_eq!(&rec.prefix, &[None, None]);
assert_eq!(
rec.traversal,
[
Traversal::NthAncestor(1),
Traversal::NthAncestor(1),
Traversal::NthParent(10),
Traversal::NthAncestor(10),
Traversal::NthParent(2)
]
);
assert_eq!(rec.calls, 9);
assert!(rec.done);
}
}
fn prefix(hex: &str) -> gix_hash::Prefix {
gix_hash::Prefix::from_hex(hex).unwrap()
}

View File

@@ -0,0 +1,273 @@
use gix_error::{bail, message, ErrorExt, Exn};
use gix_object::bstr::{BStr, BString};
use gix_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<gix_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(gix_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(AsRef::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) -> Result<(), Exn> {
for entry in store.iter_mut() {
if entry.is_none() {
*entry = Some(val);
return Ok(());
}
}
panic!("called {fn_name}() more than twice with '{val:?}'");
}
impl delegate::Revision for Recorder {
fn find_ref(&mut self, input: &BStr) -> Result<(), Exn> {
self.called(Call::FindRef);
set_val("find_ref", &mut self.find_ref, input.into())
}
fn disambiguate_prefix(
&mut self,
input: gix_hash::Prefix,
hint: Option<delegate::PrefixHint<'_>>,
) -> Result<(), Exn> {
self.called(Call::DisambiguatePrefix);
if self.opts.reject_prefix {
bail!(message!("disambiguate_prefix rejected").raise_erased());
}
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,
},
)?;
}
Ok(())
}
fn reflog(&mut self, entry: delegate::ReflogLookup) -> Result<(), Exn> {
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) -> Result<(), Exn> {
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) -> Result<(), Exn> {
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) -> Result<(), Exn> {
self.called(Call::Traverse);
self.traversal.push(kind);
Ok(())
}
fn peel_until(&mut self, kind: delegate::PeelTo) -> Result<(), Exn> {
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,
});
Ok(())
}
fn find(&mut self, regex: &BStr, negated: bool) -> Result<(), Exn> {
self.called(Call::Find);
self.patterns.push((regex.into(), negated));
Ok(())
}
fn index_lookup(&mut self, path: &BStr, stage: u8) -> Result<(), Exn> {
self.called(Call::IndexLookup);
self.index_lookups.push((path.into(), stage));
Ok(())
}
}
impl delegate::Kind for Recorder {
fn kind(&mut self, kind: spec::Kind) -> Result<(), Exn> {
self.called(Call::Kind);
if self.opts.reject_kind {
bail!(message!("kind() was rejected").raise_erased());
}
if self.kind.is_none() {
self.kind = Some(kind);
} else if !self.opts.no_internal_assertions {
panic!("called kind more than once with '{kind:?}'");
}
Ok(())
}
}
impl Delegate for Recorder {
fn done(&mut self) -> Result<(), Exn> {
self.done = true;
Ok(())
}
}
fn parse(spec: &str) -> Recorder {
try_parse_opts(spec, Options::default()).unwrap()
}
fn try_parse(spec: &str) -> Result<Recorder, Exn<spec::parse::Error>> {
try_parse_opts(spec, Default::default())
}
fn try_parse_opts(spec: &str, options: Options) -> Result<Recorder, Exn<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;

View File

@@ -0,0 +1,255 @@
use gix_revision::spec::parse::delegate::Traversal;
use crate::spec::parse::{parse, try_parse, PeelToOwned as PeelTo};
#[test]
fn single_is_first_parent() {
let rec = parse("@^");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.traversal[0], Traversal::NthParent(1));
assert_eq!(rec.calls, 2);
}
#[test]
fn multiple_calls_stack() {
let rec = parse("@^^^10^0^{tag}^020");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(
rec.traversal,
vec![
Traversal::NthParent(1),
Traversal::NthParent(1),
Traversal::NthParent(10),
Traversal::NthParent(20),
]
);
assert_eq!(
rec.peel_to,
vec![
PeelTo::ObjectKind(gix_object::Kind::Commit),
PeelTo::ObjectKind(gix_object::Kind::Tag)
]
);
assert_eq!(rec.calls, 7);
}
#[test]
fn followed_by_zero_is_peeling_to_commit() {
let rec = parse("@^0");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.traversal.len(), 0, "traversals by parent are never zero");
assert_eq!(
rec.peel_to,
vec![PeelTo::ObjectKind(gix_object::Kind::Commit)],
"instead 0 serves as shortcut"
);
assert_eq!(rec.calls, 2);
}
#[test]
fn explicitly_positive_numbers_are_invalid() {
let err = try_parse("@^+1").unwrap_err().into_inner();
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(b"+1".as_ref()));
assert!(err.message.contains("positive numbers are invalid"));
}
#[test]
fn explicit_parent_number() {
for (spec, expected) in [
("HEAD^1", 1),
("abcd^10", 10),
("v1.3.4^123", 123),
("v1.3.4-12-g1234^1000", 1000),
] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert!(rec.find_ref[0].as_ref().is_some() || rec.prefix[0].is_some());
assert_eq!(rec.traversal, vec![Traversal::NthParent(expected)]);
assert_eq!(rec.calls, 2);
}
}
#[test]
fn peel_to_object_type() {
for (spec, expected) in [
("HEAD^{commit}", PeelTo::ObjectKind(gix_object::Kind::Commit)),
("abcd^{tree}", PeelTo::ObjectKind(gix_object::Kind::Tree)),
("v1.3.4^{blob}", PeelTo::ObjectKind(gix_object::Kind::Blob)),
("v1.3.4-12-g1234^{tag}", PeelTo::ObjectKind(gix_object::Kind::Tag)),
("v1.3.4-12-g1234^{object}", PeelTo::ExistingObject),
] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert!(rec.find_ref[0].as_ref().is_some() || rec.prefix[0].is_some());
assert_eq!(rec.peel_to, vec![expected]);
assert_eq!(rec.calls, 2);
}
}
#[test]
fn regex_backslash_rules() {
for (spec, regex, msg) in [
(
r#"@^{/with count{1}}"#,
r#"with count{1}"#,
"matching inner parens do not need escaping",
),
(
r"@^{/with count\{1\}}",
r#"with count{1}"#,
"escaped parens are entirely ignored",
),
(r"@^{/1\}}", r#"1}"#, "unmatched closing parens need to be escaped"),
(r"@^{/2\{}", r#"2{"#, "unmatched opening parens need to be escaped"),
(
r"@^{/3{\{}}",
r#"3{{}"#,
"unmatched nested opening parens need to be escaped",
),
(
r"@^{/4{\}}}",
r#"4{}}"#,
"unmatched nested closing parens need to be escaped",
),
(r"@^{/a\b\c}", r"a\b\c", "single backslashes do not need to be escaped"),
(
r"@^{/a\b\c\\}",
r"a\b\c\",
"single backslashes do not need to be escaped, trailing",
),
(
r"@^{/a\\b\\c\\}",
r"a\b\c\",
"backslashes can be escaped nonetheless, trailing",
),
(
r"@^{/5\\{}}",
r"5\{}",
"backslashes in front of parens must be escaped or they would unbalance the brace pair",
),
] {
let rec = try_parse(spec).expect(msg);
assert!(rec.kind.is_none());
assert!(rec.find_ref[0].as_ref().is_some() || rec.prefix[0].is_some());
assert_eq!(rec.patterns, vec![(regex.into(), false)], "{msg}");
assert_eq!(rec.calls, 2);
}
}
#[test]
fn regex_with_revision_starting_point_and_negation() {
for (spec, (regex, negated)) in [
("HEAD^{/simple}", ("simple", false)),
("abcd^{/!-negated}", ("negated", true)),
("v1.3.4^{/^from start}", ("^from start", false)),
(
"v1.3.4-12-g1234^{/!!leading exclamation mark}",
("!leading exclamation mark", false),
),
("v1.3.4-12-g1234^{/with count{1}}", ("with count{1}", false)),
] {
let rec = parse(spec);
assert!(rec.kind.is_none());
assert!(rec.find_ref[0].as_ref().is_some() || rec.prefix[0].is_some());
assert_eq!(rec.patterns, vec![(regex.into(), negated)]);
assert_eq!(rec.calls, 2);
}
}
#[test]
fn empty_braces_deref_a_tag() {
let rec = parse("v1.2^{}");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "v1.2");
assert_eq!(rec.peel_to, vec![PeelTo::RecursiveTagObject]);
assert_eq!(rec.calls, 2);
}
#[test]
fn invalid_object_type() {
let err = try_parse("@^{invalid}").unwrap_err().into_inner();
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(b"invalid".as_ref()));
assert!(err.message.contains("cannot peel"));
let err = try_parse("@^{Commit}").unwrap_err().into_inner();
assert!(
err.input.as_ref().map(AsRef::as_ref) == Some(b"Commit".as_ref()) && err.message.contains("cannot peel"),
"these types are case sensitive"
);
}
#[test]
fn invalid_caret_without_previous_refname() {
let rec = parse(r"^^");
assert_eq!(rec.calls, 2);
assert_eq!(rec.kind, Some(gix_revision::spec::Kind::ExcludeReachable));
assert_eq!(
rec.traversal,
[Traversal::NthParent(1)],
"This can trip off an implementation as it's actually invalid, but looks valid"
);
for revspec in ["^^^HEAD", "^^HEAD"] {
let err = try_parse(revspec).unwrap_err().into_inner();
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(b"HEAD".as_ref()));
assert!(err.message.contains("unconsumed input"));
}
}
#[test]
fn incomplete_escaped_braces_in_regex_are_invalid() {
let err = try_parse(r"@^{/a\{1}}").unwrap_err().into_inner();
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(b"}".as_ref()));
assert!(err.message.contains("unconsumed input"));
let err = try_parse(r"@^{/a{1\}}").unwrap_err().into_inner();
assert!(
err.input.as_ref().map(AsRef::as_ref) == Some(br"{/a{1\}}".as_ref()) && err.message.contains("unclosed brace")
);
}
#[test]
fn regex_with_empty_exclamation_mark_prefix_is_invalid() {
let err = try_parse(r#"@^{/!hello}"#).unwrap_err().into_inner();
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(b"!hello".as_ref()));
assert!(err.message.contains("need one character after"));
}
#[test]
fn bad_escapes_can_cause_brace_mismatch() {
let err = try_parse(r"@^{\}").unwrap_err().into_inner();
assert!(err.input.as_ref().map(AsRef::as_ref) == Some(br"{\}".as_ref()) && err.message.contains("unclosed brace"));
let err = try_parse(r"@^{{\}}").unwrap_err().into_inner();
// The raw string r"{{\}}" contains actual backslashes, so the input would be r"{{\}}"
assert!(
err.input.as_ref().map(AsRef::as_ref) == Some(br"{{\}}".as_ref()) && err.message.contains("unclosed brace")
);
}
#[test]
fn empty_top_revision_regex_are_skipped_as_they_match_everything() {
let rec = parse("@^{/}");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEAD");
assert!(
rec.patterns.is_empty(),
"The delegate won't be called with empty regexes"
);
assert_eq!(rec.calls, 1);
}

View File

@@ -0,0 +1,57 @@
use gix_revision::spec::parse::delegate::Traversal;
use crate::spec::parse::{parse, PeelToOwned as PeelTo};
#[test]
fn paths_consume_all_remaining_input_as_they_refer_to_blobs() {
let rec = parse("@:../relative/path...@^^~~");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.traversal.len(), 0);
assert_eq!(rec.peel_to, vec![PeelTo::Path("../relative/path...@^^~~".into())]);
assert_eq!(rec.calls, 2);
let rec = parse("@:absolute/path^{object}");
assert_eq!(
rec.peel_to,
vec![PeelTo::Path("absolute/path^{object}".into())],
"this includes useful navigation like object-existence, a shortcoming git shares, proper implementation needs escaping as well."
);
let rec = parse("@:absolute/path^{tree}");
assert_eq!(
rec.peel_to,
vec![PeelTo::Path("absolute/path^{tree}".into())],
"this includes useful navigation like assertion of trees/blobs, we may make this possible in future but for now are as open as git"
);
}
#[test]
fn empty_paths_refer_to_the_root_tree() {
let rec = parse("@:");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.peel_to, vec![PeelTo::Path("".into())]);
assert_eq!(rec.calls, 2);
}
#[test]
fn paths_have_to_be_last_but_stack_with_other_navigation() {
let rec = parse("HEAD@{1}~10^2^{commit}:README.md");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.current_branch_reflog_entry[0], Some("1".to_string()));
assert_eq!(rec.traversal, vec![Traversal::NthAncestor(10), Traversal::NthParent(2)]);
assert_eq!(
rec.peel_to,
vec![
PeelTo::ObjectKind(gix_object::Kind::Commit),
PeelTo::Path("README.md".into())
]
);
assert_eq!(rec.calls, 6);
}

View File

@@ -0,0 +1,3 @@
mod caret_symbol;
mod colon_symbol;
mod tilde_symbol;

View File

@@ -0,0 +1,49 @@
use gix_revision::spec::parse::delegate::Traversal;
use crate::spec::parse::{parse, try_parse};
#[test]
fn without_anchor_is_invalid() {
let err = try_parse("~").unwrap_err().into_inner();
assert!(err.message.contains("tilde needs to follow an anchor"));
}
#[test]
fn single_is_first_ancestor() {
let rec = parse("@~");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.traversal[0], Traversal::NthAncestor(1));
assert_eq!(rec.calls, 2);
}
#[test]
fn followed_by_zero_is_no_op() {
let rec = parse("@~0");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(rec.calls, 1);
}
#[test]
fn multiple_calls_stack() {
let rec = parse("@~~~10~0~020");
assert!(rec.kind.is_none());
assert_eq!(rec.get_ref(0), "HEAD");
assert_eq!(rec.prefix[0], None);
assert_eq!(
rec.traversal,
vec![
Traversal::NthAncestor(1),
Traversal::NthAncestor(1),
Traversal::NthAncestor(10),
Traversal::NthAncestor(20),
]
);
assert_eq!(rec.calls, 5);
}