anodizer_core/git/
semver.rs1use anyhow::Result;
2use regex::Regex;
3use std::sync::LazyLock;
4
5#[derive(Debug, Clone)]
6pub struct SemVer {
7 pub major: u64,
8 pub minor: u64,
9 pub patch: u64,
10 pub prerelease: Option<String>,
11 pub build_metadata: Option<String>,
12}
13
14impl SemVer {
15 pub fn is_prerelease(&self) -> bool {
16 self.prerelease.is_some()
17 }
18
19 pub fn raw_version_string(&self) -> String {
22 format!("{}.{}.{}", self.major, self.minor, self.patch)
23 }
24
25 pub fn version_string(&self) -> String {
31 let mut version = self.raw_version_string();
32 if let Some(ref pre) = self.prerelease {
33 version.push('-');
34 version.push_str(pre);
35 }
36 if let Some(ref meta) = self.build_metadata {
37 version.push('+');
38 version.push_str(meta);
39 }
40 version
41 }
42}
43
44impl PartialEq for SemVer {
45 fn eq(&self, other: &Self) -> bool {
46 self.major == other.major
47 && self.minor == other.minor
48 && self.patch == other.patch
49 && self.prerelease == other.prerelease
50 }
51}
52
53impl Eq for SemVer {}
54
55impl PartialOrd for SemVer {
56 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
57 Some(self.cmp(other))
58 }
59}
60
61impl Ord for SemVer {
62 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
63 self.major
64 .cmp(&other.major)
65 .then(self.minor.cmp(&other.minor))
66 .then(self.patch.cmp(&other.patch))
67 .then(match (&self.prerelease, &other.prerelease) {
68 (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (Some(a), Some(b)) => compare_prerelease(a, b),
71 (None, None) => std::cmp::Ordering::Equal,
72 })
73 }
74}
75
76pub(super) fn compare_prerelease(a: &str, b: &str) -> std::cmp::Ordering {
84 use std::cmp::Ordering;
85
86 let a_ids: Vec<&str> = a.split('.').collect();
87 let b_ids: Vec<&str> = b.split('.').collect();
88
89 for (ai, bi) in a_ids.iter().zip(b_ids.iter()) {
90 let ord = match (ai.parse::<u64>(), bi.parse::<u64>()) {
91 (Ok(an), Ok(bn)) => an.cmp(&bn), (Ok(_), Err(_)) => Ordering::Less, (Err(_), Ok(_)) => Ordering::Greater, (Err(_), Err(_)) => ai.cmp(bi), };
96 if ord != Ordering::Equal {
97 return ord;
98 }
99 }
100 a_ids.len().cmp(&b_ids.len())
102}
103
104static SEMVER_RE: LazyLock<Regex> =
109 LazyLock::new(|| crate::util::static_regex(r"^v?(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+(.+))?$"));
110
111pub fn parse_semver(tag: &str) -> Result<SemVer> {
117 let caps = SEMVER_RE
118 .captures(tag)
119 .ok_or_else(|| anyhow::anyhow!("not a valid semver tag: {}", tag))?;
120 Ok(SemVer {
121 major: caps[1].parse()?,
122 minor: caps[2].parse()?,
123 patch: caps[3].parse()?,
124 prerelease: caps.get(4).map(|m| m.as_str().to_string()),
125 build_metadata: caps.get(5).map(|m| m.as_str().to_string()),
126 })
127}
128
129pub fn version_from_tag(tag: &str) -> Option<String> {
141 if tag.is_empty() {
142 return None;
143 }
144 parse_semver_tag(tag).ok().map(|sv| sv.version_string())
145}
146
147pub fn split_tag_family(tag: &str) -> Option<(&str, SemVer)> {
153 static FAMILY_RE: LazyLock<Regex> = LazyLock::new(|| {
154 crate::util::static_regex(r"^((?:|.*[-_/])v?)(\d+\.\d+\.\d+(?:-[^+]+)?(?:\+.+)?)$")
155 });
156 let caps = FAMILY_RE.captures(tag)?;
157 let prefix_len = caps.get(1)?.as_str().len();
158 let sv = parse_semver(caps.get(2)?.as_str()).ok()?;
159 Some((&tag[..prefix_len], sv))
160}
161
162pub fn parse_semver_tag(tag: &str) -> Result<SemVer> {
163 if let Ok(sv) = parse_semver(tag) {
165 return Ok(sv);
166 }
167 static PREFIX_RE: LazyLock<Regex> =
169 LazyLock::new(|| crate::util::static_regex(r"[-_/](v?\d+\.\d+\.\d+(?:-[^+]+)?(?:\+.+)?)$"));
170 if let Some(caps) = PREFIX_RE.captures(tag) {
171 return parse_semver(&caps[1]);
172 }
173 anyhow::bail!("not a valid semver tag: {}", tag)
174}