mirror of
https://github.com/awfixers-stuff/src.git
synced 2026-03-24 03:25:59 +00:00
create src
This commit is contained in:
26
src-refspec/tests/refspec/impls.rs
Normal file
26
src-refspec/tests/refspec/impls.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
|
||||
use gix_refspec::{parse::Operation, RefSpec};
|
||||
|
||||
fn pair() -> Vec<RefSpec> {
|
||||
let lhs = gix_refspec::parse("refs/heads/foo".into(), Operation::Push).unwrap();
|
||||
let rhs = gix_refspec::parse("refs/heads/foo:refs/heads/foo".into(), Operation::Push).unwrap();
|
||||
vec![lhs.to_owned(), rhs.to_owned()]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp() {
|
||||
assert_eq!(BTreeSet::from_iter(pair()).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash() {
|
||||
let set: HashSet<_> = pair().into_iter().collect();
|
||||
assert_eq!(set.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq() {
|
||||
let specs = pair();
|
||||
assert_eq!(&specs[0], &specs[1]);
|
||||
}
|
||||
10
src-refspec/tests/refspec/main.rs
Normal file
10
src-refspec/tests/refspec/main.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
extern crate core;
|
||||
|
||||
use gix_testtools::Result;
|
||||
|
||||
mod impls;
|
||||
mod match_group;
|
||||
mod matching;
|
||||
mod parse;
|
||||
mod spec;
|
||||
mod write;
|
||||
343
src-refspec/tests/refspec/match_group.rs
Normal file
343
src-refspec/tests/refspec/match_group.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
310
src-refspec/tests/refspec/matching.rs
Normal file
310
src-refspec/tests/refspec/matching.rs
Normal file
@@ -0,0 +1,310 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static BASELINE: LazyLock<baseline::Baseline> = LazyLock::new(|| baseline::parse().unwrap());
|
||||
|
||||
pub mod baseline {
|
||||
use std::{borrow::Borrow, collections::HashMap};
|
||||
|
||||
use bstr::{BString, ByteSlice, ByteVec};
|
||||
use gix_hash::ObjectId;
|
||||
use gix_refspec::{
|
||||
match_group::{validate::Fix, SourceRef},
|
||||
parse::Operation,
|
||||
MatchGroup,
|
||||
};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::matching::BASELINE;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Ref {
|
||||
pub name: BString,
|
||||
pub target: ObjectId,
|
||||
/// Set if `target` is an annotated tag, this being the object it points to.
|
||||
pub object: Option<ObjectId>,
|
||||
}
|
||||
|
||||
impl Ref {
|
||||
pub fn to_item(&self) -> gix_refspec::match_group::Item<'_> {
|
||||
gix_refspec::match_group::Item {
|
||||
full_ref_name: self.name.borrow(),
|
||||
target: &self.target,
|
||||
object: self.object.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static INPUT: LazyLock<Vec<Ref>> = LazyLock::new(|| parse_input().unwrap());
|
||||
|
||||
pub type Baseline = HashMap<Vec<BString>, Result<Vec<Mapping>, BString>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Mapping {
|
||||
pub remote: BString,
|
||||
/// `None` if there is no destination/tracking branch
|
||||
pub local: Option<BString>,
|
||||
}
|
||||
|
||||
pub fn input() -> impl ExactSizeIterator<Item = gix_refspec::match_group::Item<'static>> + Clone {
|
||||
INPUT.iter().map(Ref::to_item)
|
||||
}
|
||||
|
||||
pub fn of_objects_with_destinations_are_written_into_given_local_branches<'a, 'b>(
|
||||
specs: impl IntoIterator<Item = &'a str> + Clone,
|
||||
expected: impl IntoIterator<Item = &'b str>,
|
||||
) {
|
||||
agrees_and_applies_fixes(specs, Vec::new(), expected);
|
||||
}
|
||||
|
||||
pub fn agrees_and_applies_fixes<'a, 'b>(
|
||||
specs: impl IntoIterator<Item = &'a str> + Clone,
|
||||
fixes: impl IntoIterator<Item = Fix>,
|
||||
expected: impl IntoIterator<Item = &'b str>,
|
||||
) {
|
||||
check_fetch_remote(
|
||||
specs,
|
||||
Mode::Custom {
|
||||
expected: expected
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let spec = gix_refspec::parse(s.into(), Operation::Fetch).expect("valid spec");
|
||||
Mapping {
|
||||
remote: spec.source().unwrap().into(),
|
||||
local: spec.destination().map(ToOwned::to_owned),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
fixes: fixes.into_iter().collect(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn of_objects_always_matches_if_the_server_has_the_object<'a, 'b>(
|
||||
specs: impl IntoIterator<Item = &'a str> + Clone,
|
||||
) {
|
||||
check_fetch_remote(specs, Mode::Normal { validate_err: None });
|
||||
}
|
||||
|
||||
pub fn agrees_with_fetch_specs<'a>(specs: impl IntoIterator<Item = &'a str> + Clone) {
|
||||
check_fetch_remote(specs, Mode::Normal { validate_err: None });
|
||||
}
|
||||
|
||||
pub fn agrees_with_fetch_specs_validation_error<'a>(
|
||||
specs: impl IntoIterator<Item = &'a str> + Clone,
|
||||
validate_err: impl Into<String>,
|
||||
) {
|
||||
check_fetch_remote(
|
||||
specs,
|
||||
Mode::Normal {
|
||||
validate_err: Some(validate_err.into()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn invalid_specs_fail_to_parse_where_git_shows_surprising_behaviour<'a>(
|
||||
specs: impl IntoIterator<Item = &'a str>,
|
||||
err: gix_refspec::parse::Error,
|
||||
) {
|
||||
let err = err.to_string();
|
||||
let mut saw_err = false;
|
||||
for spec in specs {
|
||||
match gix_refspec::parse(spec.into(), Operation::Fetch) {
|
||||
Ok(_) => {}
|
||||
Err(e) if e.to_string() == err => {
|
||||
saw_err = true;
|
||||
}
|
||||
Err(err) => panic!("Unexpected parse error: {err:?}"),
|
||||
}
|
||||
}
|
||||
assert!(saw_err, "Failed to see error when parsing specs: {err:?}");
|
||||
}
|
||||
|
||||
/// Here we checked by hand which refs are actually written with a particular refspec
|
||||
pub fn agrees_but_observable_refs_are_vague<'a, 'b>(
|
||||
specs: impl IntoIterator<Item = &'a str> + Clone,
|
||||
expected: impl IntoIterator<Item = &'b str>,
|
||||
) {
|
||||
of_objects_with_destinations_are_written_into_given_local_branches(specs, expected);
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
Normal { validate_err: Option<String> },
|
||||
Custom { expected: Vec<Mapping>, fixes: Vec<Fix> },
|
||||
}
|
||||
|
||||
fn check_fetch_remote<'a>(specs: impl IntoIterator<Item = &'a str> + Clone, mode: Mode) {
|
||||
let match_group = MatchGroup::from_fetch_specs(
|
||||
specs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|spec| gix_refspec::parse(spec.into(), Operation::Fetch).unwrap()),
|
||||
);
|
||||
|
||||
let key: Vec<_> = specs.into_iter().map(BString::from).collect();
|
||||
let expected = BASELINE
|
||||
.get(&key)
|
||||
.unwrap_or_else(|| panic!("BUG: Need {key:?} added to the baseline"))
|
||||
.as_ref();
|
||||
|
||||
let actual = match_group.match_lhs(input()).validated();
|
||||
let (actual, expected) = match &mode {
|
||||
Mode::Normal { validate_err } => match validate_err {
|
||||
Some(err_message) => {
|
||||
assert_eq!(actual.unwrap_err().to_string(), *err_message);
|
||||
return;
|
||||
}
|
||||
None => {
|
||||
let (actual, fixed) = actual.unwrap();
|
||||
assert_eq!(
|
||||
fixed,
|
||||
Vec::<gix_refspec::match_group::validate::Fix>::new(),
|
||||
"we don't expect any issues to be fixed here"
|
||||
);
|
||||
(actual.mappings, expected.expect("no error"))
|
||||
}
|
||||
},
|
||||
Mode::Custom {
|
||||
expected,
|
||||
fixes: expected_fixes,
|
||||
} => {
|
||||
let (actual, actual_fixes) = actual.unwrap();
|
||||
assert_eq!(&actual_fixes, expected_fixes);
|
||||
(actual.mappings, expected)
|
||||
}
|
||||
};
|
||||
assert_eq!(
|
||||
actual.len(),
|
||||
expected.len(),
|
||||
"got a different amount of mappings: {actual:?} != {expected:?}"
|
||||
);
|
||||
|
||||
for (idx, (actual, expected)) in actual.iter().zip(expected).enumerate() {
|
||||
assert_eq!(
|
||||
source_to_bstring(&actual.lhs),
|
||||
expected.remote,
|
||||
"{idx}: remote mismatch"
|
||||
);
|
||||
if let Some(expected) = expected.local.as_ref() {
|
||||
match actual.rhs.as_ref() {
|
||||
None => panic!("{idx}: Expected local ref to be {expected}, got none"),
|
||||
Some(actual) => assert_eq!(actual.as_ref(), expected, "{idx}: mismatched local ref"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn source_to_bstring(source: &SourceRef) -> BString {
|
||||
match source {
|
||||
SourceRef::FullName(name) => name.as_ref().into(),
|
||||
SourceRef::ObjectId(id) => id.to_string().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_input() -> crate::Result<Vec<Ref>> {
|
||||
let dir = gix_testtools::scripted_fixture_read_only("match_baseline.sh")?;
|
||||
let refs_buf = std::fs::read(dir.join("clone").join("remote-refs.list"))?;
|
||||
let mut out = Vec::new();
|
||||
for line in refs_buf.lines() {
|
||||
if line.starts_with(b"From ") {
|
||||
continue;
|
||||
}
|
||||
let mut tokens = line.splitn(2, |b| *b == b'\t');
|
||||
let target = ObjectId::from_hex(tokens.next().expect("hex-sha"))?;
|
||||
let name = tokens.next().expect("name");
|
||||
if !name.ends_with(b"^{}") {
|
||||
out.push(Ref {
|
||||
name: name.into(),
|
||||
target,
|
||||
object: None,
|
||||
});
|
||||
} else {
|
||||
out.last_mut().unwrap().object = Some(target);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn parse() -> crate::Result<Baseline> {
|
||||
let dir = gix_testtools::scripted_fixture_read_only("match_baseline.sh")?;
|
||||
let buf = std::fs::read(dir.join("clone").join("baseline.git"))?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
let mut mappings = Vec::new();
|
||||
let mut fatal = None;
|
||||
for line in buf.lines() {
|
||||
if line.starts_with(b"From ") {
|
||||
continue;
|
||||
}
|
||||
match line.strip_prefix(b"specs: ") {
|
||||
Some(specs) => {
|
||||
let key: Vec<_> = specs.split(|b| *b == b' ').map(BString::from).collect();
|
||||
let value = match fatal.take() {
|
||||
Some(message) => Err(message),
|
||||
None => Ok(std::mem::take(&mut mappings)),
|
||||
};
|
||||
map.insert(key, value);
|
||||
}
|
||||
None => match line.strip_prefix(b"fatal: ") {
|
||||
Some(message) => {
|
||||
fatal = Some(message.into());
|
||||
}
|
||||
None => match line.strip_prefix(b"error: * Ignoring funny ref") {
|
||||
Some(_) => continue,
|
||||
None => {
|
||||
let past_note = line
|
||||
.splitn(2, |b| *b == b']')
|
||||
.nth(1)
|
||||
.or_else(|| line.strip_prefix(b" * branch "))
|
||||
.or_else(|| line.strip_prefix(b" * tag "))
|
||||
.unwrap_or_else(|| panic!("line unhandled: {:?}", line.as_bstr()));
|
||||
let mut tokens = past_note.split(|b| *b == b' ').filter(|t| !t.is_empty());
|
||||
|
||||
let mut lhs = tokens.next().unwrap().trim();
|
||||
if lhs.as_bstr() == "->" {
|
||||
lhs = "HEAD".as_bytes();
|
||||
} else {
|
||||
tokens.next();
|
||||
}
|
||||
let rhs = tokens.next().unwrap().trim();
|
||||
let local = (rhs != b"FETCH_HEAD").then(|| full_tracking_ref(rhs.into()));
|
||||
if !(lhs.as_bstr() == "HEAD" && local.is_none()) {
|
||||
mappings.push(Mapping {
|
||||
remote: full_remote_ref(lhs.into()),
|
||||
local,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn looks_like_tag(name: &BString) -> bool {
|
||||
name.starts_with(b"v0.") || name.starts_with(b"annotated-v0.")
|
||||
}
|
||||
|
||||
fn full_remote_ref(mut name: BString) -> BString {
|
||||
if !name.contains(&b'/') || name.starts_with(b"sub/") || name.starts_with(b"suub/") {
|
||||
if looks_like_tag(&name) {
|
||||
name.insert_str(0, b"refs/tags/");
|
||||
} else if let Ok(_id) = gix_hash::ObjectId::from_hex(name.as_ref()) {
|
||||
// keep as is
|
||||
} else if name != "HEAD" {
|
||||
name.insert_str(0, b"refs/heads/");
|
||||
}
|
||||
}
|
||||
name
|
||||
}
|
||||
|
||||
fn full_tracking_ref(mut name: BString) -> BString {
|
||||
if name.starts_with_str(b"origin/") || name.starts_with_str("new-origin/") {
|
||||
name.insert_str(0, b"refs/remotes/");
|
||||
} else if looks_like_tag(&name) {
|
||||
name.insert_str(0, b"refs/tags/");
|
||||
}
|
||||
name
|
||||
}
|
||||
}
|
||||
215
src-refspec/tests/refspec/parse/fetch.rs
Normal file
215
src-refspec/tests/refspec/parse/fetch.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use gix_refspec::{
|
||||
instruction::Fetch,
|
||||
parse::{Error, Operation},
|
||||
Instruction,
|
||||
};
|
||||
|
||||
use crate::parse::{assert_parse, b, try_parse};
|
||||
|
||||
#[test]
|
||||
fn revspecs_are_disallowed() {
|
||||
for spec in ["main~1", "^@^{}", "HEAD:main~1"] {
|
||||
assert!(matches!(
|
||||
try_parse(spec, Operation::Fetch).unwrap_err(),
|
||||
Error::ReferenceName(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_hash_as_source() {
|
||||
assert_parse(
|
||||
"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391:",
|
||||
Instruction::Fetch(Fetch::Only {
|
||||
src: b("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_hash_destination_are_valid_as_they_might_be_a_strange_partial_branch_name() {
|
||||
assert_parse(
|
||||
"a:e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
|
||||
Instruction::Fetch(Fetch::AndUpdate {
|
||||
src: b("a"),
|
||||
dst: b("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
|
||||
allow_non_fast_forward: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_must_not_be_empty() {
|
||||
assert!(matches!(
|
||||
try_parse("^", Operation::Fetch).unwrap_err(),
|
||||
Error::NegativeEmpty
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_must_not_be_object_hash() {
|
||||
assert!(matches!(
|
||||
try_parse("^e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", Operation::Fetch).unwrap_err(),
|
||||
Error::NegativeObjectHash
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_with_destination() {
|
||||
for spec in ["^a:b", "^a:", "^:", "^:b"] {
|
||||
assert!(matches!(
|
||||
try_parse(spec, Operation::Fetch).unwrap_err(),
|
||||
Error::NegativeWithDestination
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclude() {
|
||||
assert!(matches!(
|
||||
try_parse("^a", Operation::Fetch).unwrap_err(),
|
||||
Error::NegativePartialName
|
||||
));
|
||||
assert!(matches!(
|
||||
try_parse("^a*", Operation::Fetch).unwrap_err(),
|
||||
Error::NegativeGlobPattern
|
||||
));
|
||||
assert_parse(
|
||||
"^refs/heads/a",
|
||||
Instruction::Fetch(Fetch::Exclude { src: b("refs/heads/a") }),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ampersand_is_resolved_to_head() {
|
||||
assert_parse("@", Instruction::Fetch(Fetch::Only { src: b("HEAD") }));
|
||||
assert_parse("+@", Instruction::Fetch(Fetch::Only { src: b("HEAD") }));
|
||||
assert_parse("^@", Instruction::Fetch(Fetch::Exclude { src: b("HEAD") }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lhs_colon_empty_fetches_only() {
|
||||
assert_parse("src:", Instruction::Fetch(Fetch::Only { src: b("src") }));
|
||||
assert_parse("+src:", Instruction::Fetch(Fetch::Only { src: b("src") }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lhs_colon_rhs_updates_single_ref() {
|
||||
assert_parse(
|
||||
"a:b",
|
||||
Instruction::Fetch(Fetch::AndUpdate {
|
||||
src: b("a"),
|
||||
dst: b("b"),
|
||||
allow_non_fast_forward: false,
|
||||
}),
|
||||
);
|
||||
assert_parse(
|
||||
"+a:b",
|
||||
Instruction::Fetch(Fetch::AndUpdate {
|
||||
src: b("a"),
|
||||
dst: b("b"),
|
||||
allow_non_fast_forward: true,
|
||||
}),
|
||||
);
|
||||
|
||||
assert_parse(
|
||||
"a/*:b/*",
|
||||
Instruction::Fetch(Fetch::AndUpdate {
|
||||
src: b("a/*"),
|
||||
dst: b("b/*"),
|
||||
allow_non_fast_forward: false,
|
||||
}),
|
||||
);
|
||||
assert_parse(
|
||||
"+a/*:b/*",
|
||||
Instruction::Fetch(Fetch::AndUpdate {
|
||||
src: b("a/*"),
|
||||
dst: b("b/*"),
|
||||
allow_non_fast_forward: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_lhs_colon_rhs_fetches_head_to_destination() {
|
||||
assert_parse(
|
||||
":a",
|
||||
Instruction::Fetch(Fetch::AndUpdate {
|
||||
src: b("HEAD"),
|
||||
dst: b("a"),
|
||||
allow_non_fast_forward: false,
|
||||
}),
|
||||
);
|
||||
|
||||
assert_parse(
|
||||
"+:a",
|
||||
Instruction::Fetch(Fetch::AndUpdate {
|
||||
src: b("HEAD"),
|
||||
dst: b("a"),
|
||||
allow_non_fast_forward: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colon_alone_is_for_fetching_head_into_fetchhead() {
|
||||
assert_parse(":", Instruction::Fetch(Fetch::Only { src: b("HEAD") }));
|
||||
assert_parse("+:", Instruction::Fetch(Fetch::Only { src: b("HEAD") }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ampersand_on_left_hand_side_is_head() {
|
||||
assert_parse("@:", Instruction::Fetch(Fetch::Only { src: b("HEAD") }));
|
||||
assert_parse(
|
||||
"@:HEAD",
|
||||
Instruction::Fetch(Fetch::AndUpdate {
|
||||
src: b("HEAD"),
|
||||
dst: b("HEAD"),
|
||||
allow_non_fast_forward: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_refspec_is_enough_for_fetching_head_into_fetchhead() {
|
||||
assert_parse("", Instruction::Fetch(Fetch::Only { src: b("HEAD") }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_glob_patterns_are_allowed_in_one_sided_refspecs() {
|
||||
// Complex patterns with multiple asterisks should work for one-sided refspecs
|
||||
assert_parse(
|
||||
"refs/*/foo/*",
|
||||
Instruction::Fetch(Fetch::Only { src: b("refs/*/foo/*") }),
|
||||
);
|
||||
|
||||
assert_parse(
|
||||
"+refs/heads/*/release/*",
|
||||
Instruction::Fetch(Fetch::Only {
|
||||
src: b("refs/heads/*/release/*"),
|
||||
}),
|
||||
);
|
||||
|
||||
// Even more complex patterns
|
||||
assert_parse(
|
||||
"refs/*/*/branch",
|
||||
Instruction::Fetch(Fetch::Only {
|
||||
src: b("refs/*/*/branch"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_glob_patterns_still_fail_for_two_sided_refspecs() {
|
||||
// Two-sided refspecs with complex patterns (multiple asterisks) should still fail
|
||||
for spec in [
|
||||
"refs/*/foo/*:refs/remotes/origin/*",
|
||||
"refs/*/*:refs/remotes/*",
|
||||
"a/*/c/*:b/*",
|
||||
] {
|
||||
assert!(matches!(
|
||||
try_parse(spec, Operation::Fetch).unwrap_err(),
|
||||
Error::PatternUnsupported { .. }
|
||||
));
|
||||
}
|
||||
}
|
||||
83
src-refspec/tests/refspec/parse/invalid.rs
Normal file
83
src-refspec/tests/refspec/parse/invalid.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use gix_refspec::parse::{Error, Operation};
|
||||
|
||||
use crate::parse::try_parse;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert!(matches!(try_parse("", Operation::Push).unwrap_err(), Error::Empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_component() {
|
||||
assert!(matches!(
|
||||
try_parse("refs/heads/test:refs/remotes//test", Operation::Fetch).unwrap_err(),
|
||||
Error::ReferenceName(gix_validate::reference::name::Error::RepeatedSlash)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace() {
|
||||
assert!(matches!(
|
||||
try_parse("refs/heads/test:refs/remotes/ /test", Operation::Fetch).unwrap_err(),
|
||||
Error::ReferenceName(gix_validate::reference::name::Error::InvalidByte { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_patterns_with_more_than_one_asterisk() {
|
||||
// For one-sided refspecs, complex patterns are now allowed
|
||||
for op in [Operation::Fetch, Operation::Push] {
|
||||
assert!(try_parse("a/*/c/*", op).is_ok());
|
||||
}
|
||||
|
||||
// For two-sided refspecs, complex patterns should still fail
|
||||
for op in [Operation::Fetch, Operation::Push] {
|
||||
for spec in ["a/*/c/*:x/*/y/*", "a**:**b", "+:**/"] {
|
||||
assert!(matches!(
|
||||
try_parse(spec, op).unwrap_err(),
|
||||
Error::PatternUnsupported { .. }
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Negative specs with multiple patterns still fail
|
||||
assert!(matches!(
|
||||
try_parse("^*/*", Operation::Fetch).unwrap_err(),
|
||||
Error::NegativeGlobPattern
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_sides_need_pattern_if_one_uses_it() {
|
||||
// For two-sided refspecs, both sides still need patterns if one uses it
|
||||
for op in [Operation::Fetch, Operation::Push] {
|
||||
for spec in [":a/*", "+:a/*", "a*:b/c", "a:b/*"] {
|
||||
assert!(
|
||||
matches!(try_parse(spec, op).unwrap_err(), Error::PatternUnbalanced),
|
||||
"{}",
|
||||
spec
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// One-sided refspecs with patterns are now allowed
|
||||
for op in [Operation::Fetch, Operation::Push] {
|
||||
assert!(try_parse("refs/*/a", op).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_to_empty() {
|
||||
assert!(matches!(
|
||||
try_parse("HEAD:", Operation::Push).unwrap_err(),
|
||||
Error::PushToEmpty
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzzed() {
|
||||
let input =
|
||||
include_bytes!("../../fixtures/fuzzed/clusterfuzz-testcase-minimized-src-refspec-parse-4658733962887168");
|
||||
drop(gix_refspec::parse(input.into(), gix_refspec::parse::Operation::Fetch).unwrap_err());
|
||||
drop(gix_refspec::parse(input.into(), gix_refspec::parse::Operation::Push).unwrap_err());
|
||||
}
|
||||
117
src-refspec/tests/refspec/parse/mod.rs
Normal file
117
src-refspec/tests/refspec/parse/mod.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use std::panic::catch_unwind;
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use gix_refspec::parse::Operation;
|
||||
use gix_testtools::scripted_fixture_read_only;
|
||||
|
||||
#[test]
|
||||
fn baseline() {
|
||||
let dir = scripted_fixture_read_only("parse_baseline.sh").unwrap();
|
||||
let baseline = std::fs::read(dir.join("baseline.git")).unwrap();
|
||||
let mut lines = baseline.lines();
|
||||
let mut panics = 0;
|
||||
let mut mismatch = 0;
|
||||
let mut count = 0;
|
||||
while let Some(kind_spec) = lines.next() {
|
||||
count += 1;
|
||||
let (kind, spec) = kind_spec.split_at(kind_spec.find_byte(b' ').expect("space between kind and spec"));
|
||||
let spec = &spec[1..];
|
||||
let err_code: usize = lines
|
||||
.next()
|
||||
.expect("err code")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("number");
|
||||
let op = match kind {
|
||||
b"fetch" => Operation::Fetch,
|
||||
b"push" => Operation::Push,
|
||||
_ => unreachable!("{} unexpected", kind.as_bstr()),
|
||||
};
|
||||
let res = catch_unwind(|| try_parse(spec.to_str().unwrap(), op));
|
||||
match &res {
|
||||
Ok(res) => match (res.is_ok(), err_code == 0) {
|
||||
(true, true) | (false, false) => {
|
||||
if let Ok(spec) = res {
|
||||
spec.instruction(); // should not panic
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
match (res.as_ref().err(), err_code == 0) {
|
||||
(
|
||||
Some(
|
||||
gix_refspec::parse::Error::NegativePartialName
|
||||
| gix_refspec::parse::Error::NegativeGlobPattern,
|
||||
),
|
||||
true,
|
||||
) => {} // we prefer failing fast, git let's it pass
|
||||
// We now allow complex glob patterns in one-sided refspecs
|
||||
(None, false) if is_one_sided_glob_pattern(spec, op) => {
|
||||
// This is an intentional behavior change: we allow complex globs in one-sided refspecs
|
||||
}
|
||||
_ => {
|
||||
eprintln!("{err_code} {res:?} {} {:?}", kind.as_bstr(), spec.as_bstr());
|
||||
mismatch += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
panics += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if panics != 0 || mismatch != 0 {
|
||||
panic!(
|
||||
"Out of {} baseline entries, got {} right, ({} mismatches and {} panics)",
|
||||
count,
|
||||
count - (mismatch + panics),
|
||||
mismatch,
|
||||
panics
|
||||
);
|
||||
}
|
||||
|
||||
fn is_one_sided_glob_pattern(spec: &[u8], op: Operation) -> bool {
|
||||
use bstr::ByteSlice;
|
||||
matches!(op, Operation::Fetch)
|
||||
&& spec
|
||||
.to_str()
|
||||
.map(|s| s.contains('*') && !s.contains(':'))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_and_remote() -> crate::Result {
|
||||
let spec = gix_refspec::parse("remote:local".into(), Operation::Fetch)?;
|
||||
assert_eq!(spec.remote(), spec.source());
|
||||
assert_eq!(spec.local(), spec.destination());
|
||||
|
||||
let spec = gix_refspec::parse("local:remote".into(), Operation::Push)?;
|
||||
assert_eq!(spec.local(), spec.source());
|
||||
assert_eq!(spec.remote(), spec.destination());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod fetch;
|
||||
mod invalid;
|
||||
mod push;
|
||||
|
||||
mod util {
|
||||
use gix_refspec::{parse::Operation, Instruction, RefSpecRef};
|
||||
|
||||
pub fn b(input: &str) -> &bstr::BStr {
|
||||
input.into()
|
||||
}
|
||||
|
||||
pub fn try_parse(spec: &str, op: Operation) -> Result<RefSpecRef<'_>, gix_refspec::parse::Error> {
|
||||
gix_refspec::parse(spec.into(), op)
|
||||
}
|
||||
|
||||
pub fn assert_parse<'a>(spec: &'a str, expected: Instruction<'_>) -> RefSpecRef<'a> {
|
||||
let spec = try_parse(spec, expected.operation()).expect("no error");
|
||||
assert_eq!(spec.instruction(), expected);
|
||||
spec
|
||||
}
|
||||
}
|
||||
pub use util::*;
|
||||
163
src-refspec/tests/refspec/parse/push.rs
Normal file
163
src-refspec/tests/refspec/parse/push.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use crate::parse::{assert_parse, b, try_parse};
|
||||
use gix_refspec::{
|
||||
instruction::Push,
|
||||
parse::{Error, Operation},
|
||||
Instruction,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn negative_must_not_be_empty() {
|
||||
assert!(matches!(
|
||||
try_parse("^", Operation::Push).unwrap_err(),
|
||||
Error::NegativeEmpty
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_must_not_be_object_hash() {
|
||||
assert!(matches!(
|
||||
try_parse("^e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", Operation::Push).unwrap_err(),
|
||||
Error::NegativeObjectHash
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_with_destination() {
|
||||
for spec in ["^a:b", "^a:", "^:", "^:b"] {
|
||||
assert!(matches!(
|
||||
try_parse(spec, Operation::Push).unwrap_err(),
|
||||
Error::NegativeWithDestination
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclude() {
|
||||
assert!(matches!(
|
||||
try_parse("^a", Operation::Push).unwrap_err(),
|
||||
Error::NegativePartialName
|
||||
));
|
||||
assert!(matches!(
|
||||
try_parse("^a*", Operation::Push).unwrap_err(),
|
||||
Error::NegativeGlobPattern
|
||||
));
|
||||
assert_parse(
|
||||
"^refs/heads/a",
|
||||
Instruction::Push(Push::Exclude { src: b("refs/heads/a") }),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revspecs_with_ref_name_destination() {
|
||||
assert_parse(
|
||||
"main~1:b",
|
||||
Instruction::Push(Push::Matching {
|
||||
src: b("main~1"),
|
||||
dst: b("b"),
|
||||
allow_non_fast_forward: false,
|
||||
}),
|
||||
);
|
||||
assert_parse(
|
||||
"+main~1:b",
|
||||
Instruction::Push(Push::Matching {
|
||||
src: b("main~1"),
|
||||
dst: b("b"),
|
||||
allow_non_fast_forward: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn destinations_must_be_ref_names() {
|
||||
assert!(matches!(
|
||||
try_parse("a~1:b~1", Operation::Push).unwrap_err(),
|
||||
Error::ReferenceName(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_refs_must_be_refnames() {
|
||||
assert!(matches!(
|
||||
try_parse("a~1", Operation::Push).unwrap_err(),
|
||||
Error::ReferenceName(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ampersand_is_resolved_to_head() {
|
||||
assert_parse(
|
||||
"@",
|
||||
Instruction::Push(Push::Matching {
|
||||
src: b("HEAD"),
|
||||
dst: b("HEAD"),
|
||||
allow_non_fast_forward: false,
|
||||
}),
|
||||
);
|
||||
|
||||
assert_parse(
|
||||
"+@",
|
||||
Instruction::Push(Push::Matching {
|
||||
src: b("HEAD"),
|
||||
dst: b("HEAD"),
|
||||
allow_non_fast_forward: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lhs_colon_rhs_pushes_single_ref() {
|
||||
assert_parse(
|
||||
"a:b",
|
||||
Instruction::Push(Push::Matching {
|
||||
src: b("a"),
|
||||
dst: b("b"),
|
||||
allow_non_fast_forward: false,
|
||||
}),
|
||||
);
|
||||
assert_parse(
|
||||
"+a:b",
|
||||
Instruction::Push(Push::Matching {
|
||||
src: b("a"),
|
||||
dst: b("b"),
|
||||
allow_non_fast_forward: true,
|
||||
}),
|
||||
);
|
||||
assert_parse(
|
||||
"a/*:b/*",
|
||||
Instruction::Push(Push::Matching {
|
||||
src: b("a/*"),
|
||||
dst: b("b/*"),
|
||||
allow_non_fast_forward: false,
|
||||
}),
|
||||
);
|
||||
assert_parse(
|
||||
"+a/*:b/*",
|
||||
Instruction::Push(Push::Matching {
|
||||
src: b("a/*"),
|
||||
dst: b("b/*"),
|
||||
allow_non_fast_forward: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colon_alone_is_for_pushing_matching_refs() {
|
||||
assert_parse(
|
||||
":",
|
||||
Instruction::Push(Push::AllMatchingBranches {
|
||||
allow_non_fast_forward: false,
|
||||
}),
|
||||
);
|
||||
assert_parse(
|
||||
"+:",
|
||||
Instruction::Push(Push::AllMatchingBranches {
|
||||
allow_non_fast_forward: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete() {
|
||||
assert_parse(":a", Instruction::Push(Push::Delete { ref_or_pattern: b("a") }));
|
||||
assert_parse("+:a", Instruction::Push(Push::Delete { ref_or_pattern: b("a") }));
|
||||
}
|
||||
131
src-refspec/tests/refspec/spec.rs
Normal file
131
src-refspec/tests/refspec/spec.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
mod prefix {
|
||||
use gix_refspec::{parse::Operation, RefSpec};
|
||||
|
||||
#[test]
|
||||
fn head_is_specifically_known() {
|
||||
assert_eq!(parse("HEAD").to_ref().prefix().unwrap(), "HEAD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_refs_have_no_prefix() {
|
||||
assert_eq!(parse("main").to_ref().prefix(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_specs_have_no_prefix() {
|
||||
assert_eq!(parse("^refs/heads/main").to_ref().prefix(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_absolute_refs_have_no_prefix() {
|
||||
assert_eq!(parse("refs/short").to_ref().prefix(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_specs_use_the_destination() {
|
||||
assert_eq!(
|
||||
gix_refspec::parse("refs/local/main:refs/remote/main".into(), Operation::Push)
|
||||
.unwrap()
|
||||
.prefix()
|
||||
.unwrap(),
|
||||
"refs/remote/"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_names_have_a_prefix() {
|
||||
assert_eq!(parse("refs/heads/main").to_ref().prefix().unwrap(), "refs/heads/");
|
||||
assert_eq!(parse("refs/foo/bar").to_ref().prefix().unwrap(), "refs/foo/");
|
||||
assert_eq!(
|
||||
parse("refs/heads/*:refs/remotes/origin/*").to_ref().prefix().unwrap(),
|
||||
"refs/heads/"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strange_glob_patterns_have_no_prefix() {
|
||||
assert_eq!(parse("refs/*/main:refs/*/main").to_ref().prefix(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_names_have_no_prefix() {
|
||||
assert_eq!(
|
||||
parse("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").to_ref().prefix(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
fn parse(spec: &str) -> RefSpec {
|
||||
gix_refspec::parse(spec.into(), Operation::Fetch).unwrap().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
mod expand_prefixes {
|
||||
use gix_refspec::parse::Operation;
|
||||
|
||||
#[test]
|
||||
fn head_is_specifically_known() {
|
||||
assert_eq!(parse("HEAD"), ["HEAD"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_refs_have_many_prefixes() {
|
||||
assert_eq!(
|
||||
parse("main"),
|
||||
[
|
||||
"main",
|
||||
"refs/main",
|
||||
"refs/tags/main",
|
||||
"refs/heads/main",
|
||||
"refs/remotes/main",
|
||||
"refs/remotes/main/HEAD"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_specs_have_no_prefix() {
|
||||
assert_eq!(parse("^refs/heads/main").len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_absolute_refs_expand_to_themselves() {
|
||||
assert_eq!(parse("refs/short"), ["refs/short"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_names_expand_to_their_prefix() {
|
||||
assert_eq!(parse("refs/heads/main"), ["refs/heads/"]);
|
||||
assert_eq!(parse("refs/foo/bar"), ["refs/foo/"]);
|
||||
assert_eq!(parse("refs/heads/*:refs/remotes/origin/*"), ["refs/heads/"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_specs_use_the_destination() {
|
||||
let mut out = Vec::new();
|
||||
gix_refspec::parse("refs/local/main:refs/remote/main".into(), Operation::Push)
|
||||
.unwrap()
|
||||
.expand_prefixes(&mut out);
|
||||
assert_eq!(out, ["refs/remote/"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strange_glob_patterns_expand_to_nothing() {
|
||||
assert_eq!(parse("refs/*/main:refs/*/main").len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_names_expand_to_nothing() {
|
||||
assert_eq!(parse("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").len(), 0);
|
||||
}
|
||||
|
||||
fn parse(spec: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
gix_refspec::parse(spec.into(), Operation::Fetch)
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
.to_ref()
|
||||
.expand_prefixes(&mut out);
|
||||
out.into_iter().map(|b| b.to_string()).collect()
|
||||
}
|
||||
}
|
||||
98
src-refspec/tests/refspec/write.rs
Normal file
98
src-refspec/tests/refspec/write.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
mod push {
|
||||
use gix_refspec::{instruction, Instruction};
|
||||
|
||||
#[test]
|
||||
fn all_matching_branches() {
|
||||
assert_eq!(
|
||||
Instruction::Push(instruction::Push::AllMatchingBranches {
|
||||
allow_non_fast_forward: false
|
||||
})
|
||||
.to_bstring(),
|
||||
":"
|
||||
);
|
||||
assert_eq!(
|
||||
Instruction::Push(instruction::Push::AllMatchingBranches {
|
||||
allow_non_fast_forward: true
|
||||
})
|
||||
.to_bstring(),
|
||||
"+:"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete() {
|
||||
assert_eq!(
|
||||
Instruction::Push(instruction::Push::Delete {
|
||||
ref_or_pattern: "for-deletion".into(),
|
||||
})
|
||||
.to_bstring(),
|
||||
":for-deletion"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matching() {
|
||||
assert_eq!(
|
||||
Instruction::Push(instruction::Push::Matching {
|
||||
src: "from".into(),
|
||||
dst: "to".into(),
|
||||
allow_non_fast_forward: false
|
||||
})
|
||||
.to_bstring(),
|
||||
"from:to"
|
||||
);
|
||||
assert_eq!(
|
||||
Instruction::Push(instruction::Push::Matching {
|
||||
src: "from".into(),
|
||||
dst: "to".into(),
|
||||
allow_non_fast_forward: true
|
||||
})
|
||||
.to_bstring(),
|
||||
"+from:to"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod fetch {
|
||||
use gix_refspec::{instruction, Instruction};
|
||||
#[test]
|
||||
fn only() {
|
||||
assert_eq!(
|
||||
Instruction::Fetch(instruction::Fetch::Only {
|
||||
src: "refs/heads/main".into(),
|
||||
})
|
||||
.to_bstring(),
|
||||
"refs/heads/main"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclude() {
|
||||
assert_eq!(
|
||||
Instruction::Fetch(instruction::Fetch::Exclude { src: "excluded".into() }).to_bstring(),
|
||||
"^excluded"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn and_update() {
|
||||
assert_eq!(
|
||||
Instruction::Fetch(instruction::Fetch::AndUpdate {
|
||||
src: "from".into(),
|
||||
dst: "to".into(),
|
||||
allow_non_fast_forward: false
|
||||
})
|
||||
.to_bstring(),
|
||||
"from:to"
|
||||
);
|
||||
assert_eq!(
|
||||
Instruction::Fetch(instruction::Fetch::AndUpdate {
|
||||
src: "from".into(),
|
||||
dst: "to".into(),
|
||||
allow_non_fast_forward: true
|
||||
})
|
||||
.to_bstring(),
|
||||
"+from:to"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user