radicle/git/canonical: Fix glob rewrite rule for trailing asterisk

Previously a rule like `refs/heads/releases-*` would be rewritten
incorrectly to `refs/heads/releases-**/*` matching the 'trailing
asterisk' rule.

Simplify the rewrite rule so that a trailing `*` becomes a prefix
matching rule. Add tests.
This commit is contained in:
Adrian Duke 2026-04-30 17:31:11 +01:00 committed by Fintan Halpenny
parent 455138f4a7
commit fb40bdc0ac
3 changed files with 86 additions and 13 deletions

View File

@ -60,6 +60,13 @@ COB type names and payload IDs remain unchanged for backwards compatibility.
- Delete invalid node addresses, of the IPv6 type, from the SQLite address book.
Entries deleted include ones that have an address of the form `[]:port`, or
that have an invalid address within the `[]`.
- Correctly handle the canonical reference rules that have a glob star suffix,
e.g., `refs/heads/main*`, `refs/heads/main-*`, etc.
Previously, these would be expanded into sub-component searches. For example,
in the previous examples, the search would be expanded to
`refs/heads/main**/*` and `refs/heads/main-**/*`.
Now, the rule will match by prefix, e.g. `refs/heads/main-*` will match
`refs/heads/main-a`, `refs/heads/main-b`, `refs/heads/main-deadbeef`, etc.
## 1.8.0

View File

@ -54,26 +54,19 @@ fn matches(pattern: &RawPattern, refname: &Qualified) -> bool {
// that namespace, even if they are further down the hierarchy.
// Thus, the following rules are applied:
//
// - a trailing `*` changes to `**/*`
// - a trailing `*` matches anything that starts with the prefix
// - a `*` in between path components changes to `**`
let spec = match pattern.as_str().split_once(ASTERISK) {
None => pattern.to_string(),
// Expand `refs/tags/*` to `refs/tags/**/*`
Some((prefix, "")) => {
let mut spec = prefix.to_string();
spec.push_str("**/*");
spec
}
// Expand `refs/tags/*/v1.0` to `refs/tags/**/v1.0`
match pattern.as_str().split_once(ASTERISK) {
None => pattern.as_str() == refname.as_str(),
Some((prefix, "")) => refname.as_str().starts_with(prefix),
Some((prefix, suffix)) => {
let mut spec = prefix.to_string();
spec.push_str("**");
spec.push_str(suffix);
spec
}
};
fast_glob::glob_match(&spec, refname.as_str())
}
}
}
/// Patterns are ordered by their specificity.
///

View File

@ -477,3 +477,76 @@ fn special_branches() {
assert!(Pattern::new((*SIGREFS_PARENT).clone().into()).is_err());
assert!(Pattern::new((*IDENTITY_ROOT).clone().into()).is_err());
}
#[test]
fn matches_expands_globs_appropriately() {
let exact = qualified_pattern!("refs/heads/main");
assert!(matches(&exact, &git::fmt::qualified!("refs/heads/main")));
assert!(!matches(&exact, &git::fmt::qualified!("refs/heads/main-2")));
assert!(!matches(&exact, &git::fmt::qualified!("refs/heads/other")));
let trailing_slash = qualified_pattern!("refs/heads/*");
assert!(matches(
&trailing_slash,
&git::fmt::qualified!("refs/heads/main")
));
assert!(matches(
&trailing_slash,
&git::fmt::qualified!("refs/heads/feature/1")
));
assert!(!matches(
&trailing_slash,
&git::fmt::qualified!("refs/tags/main")
));
let trailing_text = qualified_pattern!("refs/heads/feature-*");
assert!(matches(
&trailing_text,
&git::fmt::qualified!("refs/heads/feature-")
));
assert!(matches(
&trailing_text,
&git::fmt::qualified!("refs/heads/feature-1")
));
assert!(matches(
&trailing_text,
&git::fmt::qualified!("refs/heads/feature-1/sub")
));
assert!(!matches(
&trailing_text,
&git::fmt::qualified!("refs/heads/feature")
));
// Because `*` expands to `**`, it matches zero or more path components.
let middle = qualified_pattern!("refs/heads/*/main");
assert!(matches(
&middle,
&git::fmt::qualified!("refs/heads/alice/main")
));
assert!(matches(
&middle,
&git::fmt::qualified!("refs/heads/alice/bob/main")
));
//
// NOTE(Ade): In git this won't match as glob '*' isn't inclusive of zero matches like `**`
// See: https://git.kernel.org/pub/scm/git/git.git/tree/refspec.c?h=v2.54.0&id=94f057755b7941b321fd11fec1b2e3ca5313a4e0#n298
//
assert!(matches(&middle, &git::fmt::qualified!("refs/heads/main")));
assert!(!matches(
&middle,
&git::fmt::qualified!("refs/heads/alice/dev")
));
// HardenedBSD glob expansion issue
let hbsd = qualified_pattern!("refs/heads/quarterly/hardened/15-stable/main*");
assert!(matches(
&hbsd,
&git::fmt::qualified!("refs/heads/quarterly/hardened/15-stable/main-2026q2")
));
assert!(matches(
&hbsd,
&git::fmt::qualified!("refs/heads/quarterly/hardened/15-stable/main/2026q2")
));
}