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