Skip to content

Commit 1bccbfe

Browse files
authored
Merge pull request #2086 from cruessler/add-default-sorting-to-tag-list
Sort tags as versions by default
2 parents 9946611 + 2961f60 commit 1bccbfe

File tree

2 files changed

+132
-27
lines changed

2 files changed

+132
-27
lines changed

gitoxide-core/src/repository/revision/resolve.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ pub enum BlobFormat {
2525
pub(crate) mod function {
2626
use std::ffi::OsString;
2727

28-
use gix::revision::Spec;
29-
3028
use super::Options;
3129
use crate::{
3230
repository::{cat::display_object, revision, revision::resolve::BlobFormat},
@@ -97,7 +95,7 @@ pub(crate) mod function {
9795
gix::path::os_str_into_bstr(&spec)
9896
.map_err(anyhow::Error::from)
9997
.and_then(|spec| repo.rev_parse(spec).map_err(Into::into))
100-
.map(Spec::detach)
98+
.map(gix::revision::Spec::detach)
10199
})
102100
.collect::<Result<Vec<_>, _>>()?,
103101
)?;

gitoxide-core/src/repository/tag.rs

Lines changed: 131 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,140 @@
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+
144
pub fn list(repo: gix::Repository, out: &mut dyn std::io::Write) -> anyhow::Result<()> {
245
let platform = repo.references()?;
346

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);
2353

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()),
2874
}
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}")?;
3082
}
3183

3284
Ok(())
3385
}
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

Comments
 (0)