|
1 |
| -pub fn list(repo: gix::Repository, out: &mut dyn std::io::Write) -> anyhow::Result<()> { |
2 |
| - let platform = repo.references()?; |
| 1 | +use std::cmp::Ordering; |
| 2 | + |
| 3 | +use gix::bstr::{BStr, ByteSlice}; |
| 4 | + |
| 5 | +#[derive(Eq, PartialEq)] |
| 6 | +enum VersionPart { |
| 7 | + String(String), |
| 8 | + Number(usize), |
| 9 | +} |
| 10 | + |
| 11 | +impl Ord for VersionPart { |
| 12 | + fn cmp(&self, other: &Self) -> std::cmp::Ordering { |
| 13 | + match (self, other) { |
| 14 | + (VersionPart::String(a), VersionPart::String(b)) => a.cmp(b), |
| 15 | + (VersionPart::String(_), VersionPart::Number(_)) => Ordering::Less, |
| 16 | + (VersionPart::Number(_), VersionPart::String(_)) => Ordering::Greater, |
| 17 | + (VersionPart::Number(a), VersionPart::Number(b)) => a.cmp(b), |
| 18 | + } |
| 19 | + } |
| 20 | +} |
| 21 | + |
| 22 | +impl PartialOrd for VersionPart { |
| 23 | + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { |
| 24 | + Some(Ord::cmp(self, other)) |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +/// `Version` is used to store multi-part version numbers. It does so in a rather naive way, |
| 29 | +/// only distinguishing between parts that can be parsed as an integer and those that cannot. |
| 30 | +/// |
| 31 | +/// `Version` does not parse version numbers in any structure-aware way, so `v0.a` is parsed into |
| 32 | +/// `v`, `0`, `.a`. |
| 33 | +/// |
| 34 | +/// Comparing two `Version`s comes down to comparing their `parts`. `parts` are either compared |
| 35 | +/// numerically or lexicographically, depending on whether they are an integer or not. That way, |
| 36 | +/// `v0.9` sorts before `v0.10` as one would expect from a version number. |
| 37 | +/// |
| 38 | +/// When comparing versions of different lengths, shorter versions sort before longer ones (e.g., |
| 39 | +/// `v1.0` < `v1.0.1`). String parts always sort before numeric parts when compared directly. |
| 40 | +/// |
| 41 | +/// The sorting does not respect `versionsort.suffix` yet. |
| 42 | +#[derive(Eq, PartialEq)] |
| 43 | +struct Version { |
| 44 | + parts: Vec<VersionPart>, |
| 45 | +} |
3 | 46 |
|
4 |
| - for mut reference in platform.tags()?.flatten() { |
5 |
| - let tag = reference.peel_to_tag(); |
6 |
| - let tag_ref = tag.as_ref().map(gix::Tag::decode); |
7 |
| - |
8 |
| - // `name` is the name of the file in `refs/tags/`. |
9 |
| - // This applies to both lightweight and annotated tags. |
10 |
| - let name = reference.name().shorten(); |
11 |
| - let mut fields = Vec::new(); |
12 |
| - match tag_ref { |
13 |
| - Ok(Ok(tag_ref)) => { |
14 |
| - // `tag_name` is the name provided by the user via `git tag -a/-s/-u`. |
15 |
| - // It is only present for annotated tags. |
16 |
| - fields.push(format!( |
17 |
| - "tag name: {}", |
18 |
| - if name == tag_ref.name { "*".into() } else { tag_ref.name } |
19 |
| - )); |
20 |
| - if tag_ref.pgp_signature.is_some() { |
21 |
| - fields.push("signed".into()); |
| 47 | +impl Version { |
| 48 | + fn parse(version: &BStr) -> Self { |
| 49 | + let parts = version |
| 50 | + .chunk_by(|a, b| a.is_ascii_digit() == b.is_ascii_digit()) |
| 51 | + .map(|part| { |
| 52 | + if let Ok(part) = part.to_str() { |
| 53 | + match part.parse::<usize>() { |
| 54 | + Ok(number) => VersionPart::Number(number), |
| 55 | + Err(_) => VersionPart::String(part.to_string()), |
| 56 | + } |
| 57 | + } else { |
| 58 | + VersionPart::String(String::from_utf8_lossy(part).to_string()) |
22 | 59 | }
|
| 60 | + }) |
| 61 | + .collect(); |
23 | 62 |
|
24 |
| - writeln!(out, "{name} [{fields}]", fields = fields.join(", "))?; |
25 |
| - } |
26 |
| - _ => { |
27 |
| - writeln!(out, "{name}")?; |
| 63 | + Self { parts } |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +impl Ord for Version { |
| 68 | + fn cmp(&self, other: &Self) -> std::cmp::Ordering { |
| 69 | + let mut a_iter = self.parts.iter(); |
| 70 | + let mut b_iter = other.parts.iter(); |
| 71 | + |
| 72 | + loop { |
| 73 | + match (a_iter.next(), b_iter.next()) { |
| 74 | + (Some(a), Some(b)) => match a.cmp(b) { |
| 75 | + Ordering::Equal => continue, |
| 76 | + other => return other, |
| 77 | + }, |
| 78 | + (Some(_), None) => return Ordering::Greater, |
| 79 | + (None, Some(_)) => return Ordering::Less, |
| 80 | + (None, None) => return Ordering::Equal, |
28 | 81 | }
|
29 | 82 | }
|
30 | 83 | }
|
| 84 | +} |
| 85 | + |
| 86 | +impl PartialOrd for Version { |
| 87 | + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { |
| 88 | + Some(Ord::cmp(self, other)) |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +pub fn list(repo: gix::Repository, out: &mut dyn std::io::Write) -> anyhow::Result<()> { |
| 93 | + let platform = repo.references()?; |
| 94 | + |
| 95 | + let mut tags: Vec<_> = platform |
| 96 | + .tags()? |
| 97 | + .flatten() |
| 98 | + .map(|mut reference| { |
| 99 | + let tag = reference.peel_to_tag(); |
| 100 | + let tag_ref = tag.as_ref().map(gix::Tag::decode); |
| 101 | + |
| 102 | + // `name` is the name of the file in `refs/tags/`. |
| 103 | + // This applies to both lightweight and annotated tags. |
| 104 | + let name = reference.name().shorten(); |
| 105 | + let mut fields = Vec::new(); |
| 106 | + let version = Version::parse(name); |
| 107 | + match tag_ref { |
| 108 | + Ok(Ok(tag_ref)) => { |
| 109 | + // `tag_name` is the name provided by the user via `git tag -a/-s/-u`. |
| 110 | + // It is only present for annotated tags. |
| 111 | + fields.push(format!( |
| 112 | + "tag name: {}", |
| 113 | + if name == tag_ref.name { "*".into() } else { tag_ref.name } |
| 114 | + )); |
| 115 | + if tag_ref.pgp_signature.is_some() { |
| 116 | + fields.push("signed".into()); |
| 117 | + } |
| 118 | + |
| 119 | + (version, format!("{name} [{fields}]", fields = fields.join(", "))) |
| 120 | + } |
| 121 | + _ => (version, name.to_string()), |
| 122 | + } |
| 123 | + }) |
| 124 | + .collect(); |
| 125 | + |
| 126 | + tags.sort_by(|a, b| a.0.cmp(&b.0)); |
| 127 | + |
| 128 | + for (_, tag) in tags { |
| 129 | + writeln!(out, "{tag}")?; |
| 130 | + } |
31 | 131 |
|
32 | 132 | Ok(())
|
33 | 133 | }
|
| 134 | + |
| 135 | +#[cfg(test)] |
| 136 | +mod tests { |
| 137 | + use super::*; |
| 138 | + use gix::bstr::BStr; |
| 139 | + |
| 140 | + #[test] |
| 141 | + fn sorts_versions_correctly() { |
| 142 | + let mut actual = vec![ |
| 143 | + "v2.0.0", |
| 144 | + "v1.10.0", |
| 145 | + "v1.2.1", |
| 146 | + "v1.0.0-beta", |
| 147 | + "v1.2", |
| 148 | + "v0.10.0", |
| 149 | + "v0.9.0", |
| 150 | + "v1.2.0", |
| 151 | + "v0.1.a", |
| 152 | + "v0.1.0", |
| 153 | + "v10.0.0", |
| 154 | + "1.0.0", |
| 155 | + "v1.0.0-alpha", |
| 156 | + "v1.0.0", |
| 157 | + ]; |
| 158 | + |
| 159 | + actual.sort_by(|a, b| { |
| 160 | + let version_a = Version::parse(BStr::new(a.as_bytes())); |
| 161 | + let version_b = Version::parse(BStr::new(b.as_bytes())); |
| 162 | + version_a.cmp(&version_b) |
| 163 | + }); |
| 164 | + |
| 165 | + let expected = vec![ |
| 166 | + "v0.1.0", |
| 167 | + "v0.1.a", |
| 168 | + "v0.9.0", |
| 169 | + "v0.10.0", |
| 170 | + "v1.0.0", |
| 171 | + "v1.0.0-alpha", |
| 172 | + "v1.0.0-beta", |
| 173 | + "v1.2", |
| 174 | + "v1.2.0", |
| 175 | + "v1.2.1", |
| 176 | + "v1.10.0", |
| 177 | + "v2.0.0", |
| 178 | + "v10.0.0", |
| 179 | + "1.0.0", |
| 180 | + ]; |
| 181 | + |
| 182 | + assert_eq!(actual, expected); |
| 183 | + } |
| 184 | + |
| 185 | + #[test] |
| 186 | + fn sorts_versions_with_different_lengths_correctly() { |
| 187 | + let v1 = Version::parse(BStr::new(b"v1.0")); |
| 188 | + let v2 = Version::parse(BStr::new(b"v1.0.1")); |
| 189 | + |
| 190 | + assert_eq!(v1.cmp(&v2), Ordering::Less); |
| 191 | + assert_eq!(v2.cmp(&v1), Ordering::Greater); |
| 192 | + } |
| 193 | +} |
0 commit comments