#[derive(Debug, Clone)]
pub struct ParsedRef {
pub original_ref: String,
pub normalized_for_semver: String,
pub previous_ref: String,
pub has_refs_tags_prefix: bool,
}
pub(crate) fn normalize_semver(tag: &str) -> String {
let (core, suffix) = tag
.find(|c| ['-', '+'].contains(&c))
.map(|idx| (&tag[..idx], &tag[idx..]))
.unwrap_or((tag, ""));
if core.is_empty() {
return tag.to_string();
}
let dot_count = core.matches('.').count();
let normalized_core = match dot_count {
0 => format!("{core}.0.0"),
1 => format!("{core}.0"),
_ => core.to_string(),
};
format!("{normalized_core}{suffix}")
}
pub fn is_downgrade(current: &str, proposed: &str) -> bool {
let cur = parse_ref(current, false);
let prop = parse_ref(proposed, false);
match (
semver::Version::parse(&cur.normalized_for_semver),
semver::Version::parse(&prop.normalized_for_semver),
) {
(Ok(c), Ok(p)) => p.cmp_precedence(&c) == std::cmp::Ordering::Less,
_ => false,
}
}
pub fn parse_ref(raw: &str, default_refs_tags_prefix: bool) -> ParsedRef {
let mut maybe_version = raw.to_string();
let mut previous_ref = String::new();
let mut has_refs_tags_prefix = default_refs_tags_prefix;
if let Some(stripped) = maybe_version.strip_prefix("refs/tags/") {
has_refs_tags_prefix = true;
previous_ref = maybe_version.clone();
maybe_version = stripped.to_string();
}
if let Some(digit_idx) = maybe_version.find(|c: char| c.is_ascii_digit())
&& digit_idx > 0
{
previous_ref = maybe_version.clone();
maybe_version = maybe_version[digit_idx..].to_string();
}
if previous_ref.is_empty() {
previous_ref = maybe_version.clone();
}
ParsedRef {
original_ref: raw.to_string(),
normalized_for_semver: normalize_semver(&maybe_version),
previous_ref,
has_refs_tags_prefix,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn check(
raw: &str,
default_refs_tags_prefix: bool,
expected_normalized: &str,
expected_previous: &str,
expected_has_prefix: bool,
) {
let parsed = parse_ref(raw, default_refs_tags_prefix);
assert_eq!(parsed.original_ref, raw, "original_ref for {raw:?}");
assert_eq!(
parsed.normalized_for_semver, expected_normalized,
"normalized_for_semver for {raw:?}"
);
assert_eq!(
parsed.previous_ref, expected_previous,
"previous_ref for {raw:?}"
);
assert_eq!(
parsed.has_refs_tags_prefix, expected_has_prefix,
"has_refs_tags_prefix for {raw:?}"
);
}
#[test]
fn bare_three_segment_passes_through() {
check("1.2.3", false, "1.2.3", "1.2.3", false);
}
#[test]
fn bare_two_segment_pads_patch() {
check("1.0", false, "1.0.0", "1.0", false);
}
#[test]
fn v_prefix_normalizes_to_semver() {
check("v1.2.3", false, "1.2.3", "v1.2.3", false);
}
#[test]
fn v_prefix_with_prerelease_keeps_semver_core() {
check("v1.2.3-rc1", false, "1.2.3-rc1", "v1.2.3-rc1", false);
}
#[test]
fn bare_prerelease_long_passes_through_as_semver() {
check(
"1.0.0-alpha.1",
false,
"1.0.0-alpha.1",
"1.0.0-alpha.1",
false,
);
}
#[test]
fn bare_prerelease_short_passes_through_as_semver() {
check("2.0.0-beta", false, "2.0.0-beta", "2.0.0-beta", false);
}
#[test]
fn hl_prefixed_tag_keeps_version_core_as_prerelease() {
check("hl0.47.0-1", false, "0.47.0-1", "hl0.47.0-1", false);
}
#[test]
fn plus_metadata_passes_through() {
check("1.2.3+gitea", false, "1.2.3+gitea", "1.2.3+gitea", false);
}
#[test]
fn plus_metadata_dotted_passes_through() {
check("1.2.3+meta.1", false, "1.2.3+meta.1", "1.2.3+meta.1", false);
}
#[test]
fn release_channel_strips_first_dash() {
check("release-24.05", false, "24.05.0", "release-24.05", false);
}
#[test]
fn nix_darwin_channel_strips_full_prefix() {
check(
"nix-darwin-24.05",
false,
"24.05.0",
"nix-darwin-24.05",
false,
);
}
#[test]
fn refs_tags_v_prefix_records_prefix_and_strips_v() {
check("refs/tags/v1.0.0", false, "1.0.0", "v1.0.0", true);
}
#[test]
fn refs_tags_bare_keeps_full_previous_ref() {
check("refs/tags/1.2.3", false, "1.2.3", "refs/tags/1.2.3", true);
}
#[test]
fn refs_tags_v_prerelease_keeps_semver_core() {
check(
"refs/tags/v1.2.3-rc1",
false,
"1.2.3-rc1",
"v1.2.3-rc1",
true,
);
}
#[test]
fn iso_date_pads_year_into_semver_with_date_prerelease() {
check("2024-05-01", false, "2024.0.0-05-01", "2024-05-01", false);
}
#[test]
fn empty_input_returns_empty_normalized() {
check("", false, "", "", false);
}
#[test]
fn lone_v_dash_has_no_digit_to_anchor_strip() {
check("v-", false, "v.0.0-", "v-", false);
}
#[test]
fn default_refs_tags_prefix_persists_without_refs_tags_string() {
check("1.2.3", true, "1.2.3", "1.2.3", true);
}
#[test]
fn is_downgrade_flags_lower_hl_prefixed_proposal() {
assert!(is_downgrade("hl0.47.0-1", "hl0.33.0-1"));
}
#[test]
fn is_downgrade_allows_strictly_greater_proposal() {
assert!(!is_downgrade("hl0.33.0-1", "hl0.47.0-1"));
assert!(!is_downgrade("v1.0.0", "v2.0.0"));
}
#[test]
fn is_downgrade_allows_equal_versions() {
assert!(!is_downgrade("v1.2.3", "v1.2.3"));
assert!(!is_downgrade("1.0.0", "v1.0.0"));
}
#[test]
fn is_downgrade_returns_false_when_either_side_unparseable() {
assert!(!is_downgrade("not-a-version", "1.2.3"));
assert!(!is_downgrade("1.2.3", "not-a-version"));
assert!(!is_downgrade("", "1.2.3"));
}
}