Files
src/src-refspec/tests/refspec/match_group.rs
2026-03-11 02:04:19 -07:00

344 lines
14 KiB
Rust

mod single {
use crate::matching::baseline;
#[test]
fn fetch_only() {
baseline::agrees_with_fetch_specs(Some("refs/heads/main"));
baseline::agrees_with_fetch_specs(Some("heads/main"));
baseline::agrees_with_fetch_specs(Some("main"));
baseline::agrees_with_fetch_specs(Some("v0.0-f1"));
baseline::agrees_with_fetch_specs(Some("tags/v0.0-f2"));
baseline::of_objects_always_matches_if_the_server_has_the_object(Some(
"78b1c1be9421b33a49a7a8176d93eeeafa112da1",
));
baseline::of_objects_always_matches_if_the_server_has_the_object(Some(
"9d2fab1a0ba3585d0bc50922bfdd04ebb59361df",
));
}
#[test]
fn fetch_and_update() {
baseline::of_objects_with_destinations_are_written_into_given_local_branches(
Some("78b1c1be9421b33a49a7a8176d93eeeafa112da1:special"),
["78b1c1be9421b33a49a7a8176d93eeeafa112da1:refs/heads/special"],
);
baseline::of_objects_with_destinations_are_written_into_given_local_branches(
Some("78b1c1be9421b33a49a7a8176d93eeeafa112da1:1111111111111111111111111111111111111111"),
["78b1c1be9421b33a49a7a8176d93eeeafa112da1:refs/heads/1111111111111111111111111111111111111111"],
);
baseline::of_objects_with_destinations_are_written_into_given_local_branches(
Some("9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:tags/special"),
["9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:refs/tags/special"],
);
baseline::of_objects_with_destinations_are_written_into_given_local_branches(
Some("9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:refs/tags/special"),
["9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:refs/tags/special"],
);
baseline::agrees_but_observable_refs_are_vague(Some("f1:origin/f1"), ["refs/heads/f1:refs/heads/origin/f1"]);
baseline::agrees_but_observable_refs_are_vague(
Some("f1:remotes/origin/f1"),
["refs/heads/f1:refs/remotes/origin/f1"],
);
baseline::agrees_but_observable_refs_are_vague(Some("f1:notes/f1"), ["refs/heads/f1:refs/heads/notes/f1"]);
baseline::agrees_with_fetch_specs(Some("+refs/heads/*:refs/remotes/origin/*"));
baseline::agrees_with_fetch_specs(Some("refs/heads/f*:refs/remotes/origin/a*"));
baseline::agrees_with_fetch_specs(Some("refs/heads/*1:refs/remotes/origin/*1"));
}
}
mod multiple {
use gix_refspec::{
match_group::validate::Fix,
parse::{Error, Operation},
};
use crate::matching::baseline;
#[test]
fn fetch_only() {
baseline::agrees_with_fetch_specs(["main", "f1"]);
baseline::agrees_with_fetch_specs(["heads/main", "heads/f1"]);
baseline::agrees_with_fetch_specs(["refs/heads/main", "refs/heads/f1"]);
baseline::agrees_with_fetch_specs(["heads/f1", "f2", "refs/heads/f3", "heads/main"]);
baseline::agrees_with_fetch_specs(["f*:a*", "refs/heads/main"]);
baseline::agrees_with_fetch_specs([
"refs/tags/*:refs/remotes/origin/*",
"refs/heads/*:refs/remotes/origin/*",
]);
baseline::agrees_with_fetch_specs(["refs/tags/*:refs/tags/*"]);
}
#[test]
fn fetch_and_update_and_negations() {
baseline::invalid_specs_fail_to_parse_where_git_shows_surprising_behaviour(
["refs/heads/f*:refs/remotes/origin/a*", "^f1"],
Error::NegativePartialName,
);
baseline::invalid_specs_fail_to_parse_where_git_shows_surprising_behaviour(
["heads/f2", "^refs/heads/f*:refs/remotes/origin/a*"],
Error::NegativeWithDestination,
);
baseline::agrees_with_fetch_specs(["refs/heads/f*:refs/remotes/origin/a*", "^refs/heads/f1"]);
baseline::invalid_specs_fail_to_parse_where_git_shows_surprising_behaviour(
["^heads/f2", "refs/heads/f*:refs/remotes/origin/a*"],
Error::NegativePartialName,
);
baseline::agrees_with_fetch_specs(["^refs/heads/f2", "refs/heads/f*:refs/remotes/origin/a*"]);
baseline::invalid_specs_fail_to_parse_where_git_shows_surprising_behaviour(
["^main", "refs/heads/*:refs/remotes/origin/*"],
Error::NegativePartialName,
);
baseline::agrees_with_fetch_specs(["^refs/heads/main", "refs/heads/*:refs/remotes/origin/*"]);
baseline::agrees_with_fetch_specs(["refs/heads/*:refs/remotes/origin/*", "^refs/heads/main"]);
}
#[test]
fn fetch_and_update_with_empty_lhs() {
baseline::agrees_but_observable_refs_are_vague([":refs/heads/f1"], ["HEAD:refs/heads/f1"]);
baseline::agrees_but_observable_refs_are_vague([":f1"], ["HEAD:refs/heads/f1"]);
baseline::agrees_but_observable_refs_are_vague(["@:f1"], ["HEAD:refs/heads/f1"]);
}
#[test]
fn fetch_and_update_head_to_head_never_updates_actual_head_ref() {
baseline::agrees_but_observable_refs_are_vague(["@:HEAD"], ["HEAD:refs/heads/HEAD"]);
}
#[test]
fn fetch_and_update_head_with_empty_rhs() {
baseline::agrees_but_observable_refs_are_vague([":"], ["HEAD:"]);
baseline::agrees_but_observable_refs_are_vague(["HEAD:"], ["HEAD:"]);
baseline::agrees_but_observable_refs_are_vague(["@:"], ["HEAD:"]);
}
#[test]
fn fetch_and_update_multiple_destinations() {
baseline::agrees_with_fetch_specs([
"refs/heads/*:refs/remotes/origin/*",
"refs/heads/main:refs/remotes/new-origin/main",
]);
baseline::agrees_with_fetch_specs([
"refs/heads/*:refs/remotes/origin/*",
"refs/heads/main:refs/remotes/origin/main", // duplicates are removed immediately.
]);
}
#[test]
fn fetch_and_update_with_conflicts() {
baseline::agrees_with_fetch_specs_validation_error(
[
"refs/heads/f1:refs/remotes/origin/conflict",
"refs/heads/f2:refs/remotes/origin/conflict",
],
"Found 1 issue that prevents the refspec mapping to be used: \n\tConflicting destination \"refs/remotes/origin/conflict\" would be written by refs/heads/f1 (\"refs/heads/f1:refs/remotes/origin/conflict\"), refs/heads/f2 (\"refs/heads/f2:refs/remotes/origin/conflict\")",
);
baseline::agrees_with_fetch_specs_validation_error(
[
"refs/heads/f1:refs/remotes/origin/conflict2",
"refs/heads/f2:refs/remotes/origin/conflict2",
"refs/heads/f1:refs/remotes/origin/conflict",
"refs/heads/f2:refs/remotes/origin/conflict",
"refs/heads/f3:refs/remotes/origin/conflict",
],
"Found 2 issues that prevent the refspec mapping to be used: \n\tConflicting destination \"refs/remotes/origin/conflict\" would be written by refs/heads/f1 (\"refs/heads/f1:refs/remotes/origin/conflict\"), refs/heads/f2 (\"refs/heads/f2:refs/remotes/origin/conflict\"), refs/heads/f3 (\"refs/heads/f3:refs/remotes/origin/conflict\")\n\tConflicting destination \"refs/remotes/origin/conflict2\" would be written by refs/heads/f1 (\"refs/heads/f1:refs/remotes/origin/conflict2\"), refs/heads/f2 (\"refs/heads/f2:refs/remotes/origin/conflict2\")",
);
baseline::agrees_with_fetch_specs_validation_error(
[
"refs/heads/f1:refs/remotes/origin/same",
"refs/tags/v0.0-f1:refs/remotes/origin/same",
],
"Found 1 issue that prevents the refspec mapping to be used: \n\tConflicting destination \"refs/remotes/origin/same\" would be written by refs/heads/f1 (\"refs/heads/f1:refs/remotes/origin/same\"), refs/tags/v0.0-f1 (\"refs/tags/v0.0-f1:refs/remotes/origin/same\")",
);
baseline::agrees_with_fetch_specs_validation_error(
[
"+refs/heads/*:refs/remotes/origin/*",
"refs/heads/f1:refs/remotes/origin/f2",
"refs/heads/f2:refs/remotes/origin/f1",
],
"Found 2 issues that prevent the refspec mapping to be used: \n\tConflicting destination \"refs/remotes/origin/f1\" would be written by refs/heads/f1 (\"+refs/heads/*:refs/remotes/origin/*\"), refs/heads/f2 (\"refs/heads/f2:refs/remotes/origin/f1\")\n\tConflicting destination \"refs/remotes/origin/f2\" would be written by refs/heads/f2 (\"+refs/heads/*:refs/remotes/origin/*\"), refs/heads/f1 (\"refs/heads/f1:refs/remotes/origin/f2\")",
);
}
#[test]
fn fetch_and_update_with_fixes() {
let glob_spec = "refs/heads/f*:foo/f*";
let glob_spec_ref = gix_refspec::parse(glob_spec.into(), Operation::Fetch).unwrap();
baseline::agrees_and_applies_fixes(
[glob_spec, "f1:f1"],
[
Fix::MappingWithPartialDestinationRemoved {
name: "foo/f1".into(),
spec: glob_spec_ref.to_owned(),
},
Fix::MappingWithPartialDestinationRemoved {
name: "foo/f2".into(),
spec: glob_spec_ref.to_owned(),
},
Fix::MappingWithPartialDestinationRemoved {
name: "foo/f3".into(),
spec: glob_spec_ref.to_owned(),
},
],
["refs/heads/f1:refs/heads/f1"],
);
}
}
mod complex_globs {
use bstr::BString;
use gix_hash::ObjectId;
use gix_refspec::{parse::Operation, MatchGroup};
#[test]
fn one_sided_complex_glob_patterns_can_be_parsed() {
// The key change is that complex glob patterns with multiple asterisks
// can now be parsed for one-sided refspecs
let spec = gix_refspec::parse("refs/*/foo/*".into(), Operation::Fetch);
assert!(spec.is_ok(), "Should parse complex glob pattern for one-sided refspec");
let spec = gix_refspec::parse("refs/*/*/bar".into(), Operation::Fetch);
assert!(
spec.is_ok(),
"Should parse complex glob pattern with multiple asterisks"
);
let spec = gix_refspec::parse("refs/heads/[a-z.]/release/*".into(), Operation::Fetch);
assert!(spec.is_ok(), "Should parse complex glob pattern");
// Two-sided refspecs with multiple asterisks should still fail
let spec = gix_refspec::parse("refs/*/foo/*:refs/remotes/*".into(), Operation::Fetch);
assert!(spec.is_err(), "Two-sided refspecs with multiple asterisks should fail");
}
#[test]
fn one_sided_simple_glob_patterns_match() {
// Test that simple glob patterns (one asterisk) work correctly with matching
let refs = [
new_ref("refs/heads/feature/foo", "1111111111111111111111111111111111111111"),
new_ref("refs/heads/bugfix/bar", "2222222222222222222222222222222222222222"),
new_ref("refs/tags/v1.0", "3333333333333333333333333333333333333333"),
new_ref("refs/pull/123", "4444444444444444444444444444444444444444"),
];
let items: Vec<_> = refs.iter().map(|r| r.to_item()).collect();
// Test: refs/heads/* should match all refs under refs/heads/
let spec = gix_refspec::parse("refs/heads/*".into(), Operation::Fetch).unwrap();
let group = MatchGroup::from_fetch_specs([spec]);
let outcome = group.match_lhs(items.iter().copied());
insta::assert_debug_snapshot!(outcome.mappings, @r#"
[
Mapping {
item_index: Some(
0,
),
lhs: FullName(
"refs/heads/feature/foo",
),
rhs: None,
spec_index: 0,
},
Mapping {
item_index: Some(
1,
),
lhs: FullName(
"refs/heads/bugfix/bar",
),
rhs: None,
spec_index: 0,
},
]
"#);
// Test: refs/tags/* should match all refs under refs/tags/
let items: Vec<_> = refs.iter().map(|r| r.to_item()).collect();
let spec = gix_refspec::parse("refs/tags/v[0-9]*".into(), Operation::Fetch).unwrap();
let group = MatchGroup::from_fetch_specs([spec]);
let outcome = group.match_lhs(items.iter().copied());
insta::assert_debug_snapshot!(outcome.mappings, @r#"
[
Mapping {
item_index: Some(
2,
),
lhs: FullName(
"refs/tags/v1.0",
),
rhs: None,
spec_index: 0,
},
]
"#);
}
#[test]
fn one_sided_glob_with_suffix_matches() {
// Test that glob patterns with suffix work correctly
let refs = [
new_ref("refs/heads/feature", "1111111111111111111111111111111111111111"),
new_ref("refs/heads/feat", "2222222222222222222222222222222222222222"),
new_ref("refs/heads/main", "3333333333333333333333333333333333333333"),
];
let items: Vec<_> = refs.iter().map(|r| r.to_item()).collect();
// Test: refs/heads/feat* should match refs/heads/feature and refs/heads/feat
let spec = gix_refspec::parse("refs/heads/feat*".into(), Operation::Fetch).unwrap();
let group = MatchGroup::from_fetch_specs([spec]);
let outcome = group.match_lhs(items.iter().copied());
let mappings = outcome.mappings;
insta::assert_debug_snapshot!(mappings, @r#"
[
Mapping {
item_index: Some(
0,
),
lhs: FullName(
"refs/heads/feature",
),
rhs: None,
spec_index: 0,
},
Mapping {
item_index: Some(
1,
),
lhs: FullName(
"refs/heads/feat",
),
rhs: None,
spec_index: 0,
},
]
"#);
}
fn new_ref(name: &str, id_hex: &str) -> Ref {
Ref {
name: name.into(),
target: ObjectId::from_hex(id_hex.as_bytes()).unwrap(),
object: None,
}
}
#[derive(Debug, Clone)]
struct Ref {
name: BString,
target: ObjectId,
object: Option<ObjectId>,
}
impl Ref {
fn to_item(&self) -> gix_refspec::match_group::Item<'_> {
gix_refspec::match_group::Item {
full_ref_name: self.name.as_ref(),
target: &self.target,
object: self.object.as_deref(),
}
}
}
}