flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/crates/scan/src/version.rs

123 lines4,382 bytesCodeBlame
1//! Ordering versions well enough to pick an upgrade: semver, Cargo, Go's
2//! `v1.2.3`, Python's `1.2.post1` and the like.
3
4use std::cmp::Ordering;
5
6#[derive(Debug, PartialEq, Eq)]
7enum Part {
8 Number(u64),
9 Text(String),
10}
11
12/// The release and the pre-release of a version, each split into numbers
13/// and words. `+build` metadata is ignored, as semver says.
14fn parts(version: &str) -> (Vec<Part>, Vec<Part>) {
15 let version = version.trim().trim_start_matches(['v', 'V', '=']);
16 let version = version.split('+').next().unwrap_or(version);
17 let (release, pre) = match version.split_once('-') {
18 Some((release, pre)) => (release, Some(pre)),
19 None => (version, None),
20 };
21 let split = |text: &str| -> Vec<Part> {
22 let mut out = Vec::new();
23 let mut current = String::new();
24 let mut digits = false;
25 let flush = |current: &mut String, digits: bool, out: &mut Vec<Part>| {
26 if current.is_empty() {
27 return;
28 }
29 out.push(if digits {
30 Part::Number(current.parse().unwrap_or(u64::MAX))
31 } else {
32 Part::Text(current.to_lowercase())
33 });
34 current.clear();
35 };
36 for c in text.chars() {
37 if c.is_ascii_alphanumeric() {
38 let is_digit = c.is_ascii_digit();
39 if !current.is_empty() && is_digit != digits {
40 flush(&mut current, digits, &mut out);
41 }
42 digits = is_digit;
43 current.push(c);
44 } else {
45 flush(&mut current, digits, &mut out);
46 }
47 }
48 flush(&mut current, digits, &mut out);
49 out
50 };
51 let mut release = split(release);
52 // A Python pre-release is written into the release: `2.0.0rc1`.
53 let mut pre_parts = pre.map(split).unwrap_or_default();
54 if let Some(at) = release.iter().position(|part| matches!(part, Part::Text(text) if is_pre_word(text))) {
55 let mut tail = release.split_off(at);
56 tail.append(&mut pre_parts);
57 pre_parts = tail;
58 }
59 (release, pre_parts)
60}
61
62fn is_pre_word(text: &str) -> bool {
63 matches!(text, "a" | "b" | "c" | "rc" | "alpha" | "beta" | "pre" | "dev" | "preview")
64}
65
66fn compare_parts(a: &[Part], b: &[Part]) -> Ordering {
67 let length = a.len().max(b.len());
68 for index in 0..length {
69 let ordering = match (a.get(index), b.get(index)) {
70 (Some(Part::Number(x)), Some(Part::Number(y))) => x.cmp(y),
71 (Some(Part::Text(x)), Some(Part::Text(y))) => x.cmp(y),
72 (Some(Part::Number(_)), Some(Part::Text(_))) => Ordering::Greater,
73 (Some(Part::Text(_)), Some(Part::Number(_))) => Ordering::Less,
74 // 1.2 == 1.2.0
75 (Some(Part::Number(x)), None) => x.cmp(&0),
76 (None, Some(Part::Number(y))) => 0.cmp(y),
77 // 1.2.post1 > 1.2
78 (Some(Part::Text(_)), None) => Ordering::Greater,
79 (None, Some(Part::Text(_))) => Ordering::Less,
80 (None, None) => Ordering::Equal,
81 };
82 if ordering != Ordering::Equal {
83 return ordering;
84 }
85 }
86 Ordering::Equal
87}
88
89/// Orders two versions. A pre-release comes before its release.
90pub fn compare(a: &str, b: &str) -> Ordering {
91 let (a_release, a_pre) = parts(a);
92 let (b_release, b_pre) = parts(b);
93 compare_parts(&a_release, &b_release).then_with(|| match (a_pre.is_empty(), b_pre.is_empty()) {
94 (true, true) => Ordering::Equal,
95 (true, false) => Ordering::Greater,
96 (false, true) => Ordering::Less,
97 (false, false) => compare_parts(&a_pre, &b_pre),
98 })
99}
100
101#[cfg(test)]
102mod tests {
103 use super::compare;
104 use std::cmp::Ordering::*;
105
106 #[test]
107 fn versions_are_ordered() {
108 for (a, b, expected) in [
109 ("1.2.3", "1.2.10", Less),
110 ("v1.9.0", "v1.10.0", Less),
111 ("2.0.0", "2.0.0-rc.1", Greater),
112 ("1.0.0-alpha", "1.0.0-beta", Less),
113 ("1.2", "1.2.0", Equal),
114 ("4.17.21", "4.17.21", Equal),
115 ("2.0.0rc1", "2.0.0", Less),
116 ("1.2.post1", "1.2", Greater),
117 ("0.10.0", "0.9.9", Greater),
118 ("1.0.0+build.5", "1.0.0", Equal),
119 ] {
120 assert_eq!(compare(a, b), expected, "{a} vs {b}");
121 }
122 }
123}