Skip to content

Commit fd7c029

Browse files
feat: Implement TagClient.GetAsync in NGitLab.Client.Mock (#1047)
* tests: Implement TagClient.GetAsync with a query * Fix warnings * PR comments
1 parent 3478dae commit fd7c029

File tree

2 files changed

+129
-8
lines changed

2 files changed

+129
-8
lines changed

NGitLab.Mock.Tests/TagTests.cs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
using System.Net;
1+
using System.Linq;
2+
using System.Net;
23
using System.Threading.Tasks;
34
using NGitLab.Mock.Config;
5+
using NGitLab.Models;
46
using NUnit.Framework;
57

68
namespace NGitLab.Mock.Tests;
@@ -28,4 +30,62 @@ public async Task GetTagAsync()
2830
var ex = Assert.ThrowsAsync<GitLabException>(() => tagClient.GetByNameAsync("1.0.1"));
2931
Assert.That(ex.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
3032
}
33+
34+
[Theory]
35+
public void GetTaskAsync_CanSortByName([Values] bool useDefault)
36+
{
37+
// Arrange
38+
using var server = new GitLabConfig()
39+
.WithUser("user1", isDefault: true)
40+
.WithProject("test-project", id: 1, addDefaultUserAsMaintainer: true, configure: project => project
41+
.WithCommit("Initial Commit", tags: ["0.0.1"])
42+
.WithCommit("Second Tag", tags: ["0.0.2"])
43+
.WithCommit("Second Tag", tags: ["not-semver"])
44+
.WithCommit("Other Tag", tags: ["0.0.10"]))
45+
.BuildServer();
46+
47+
var client = server.CreateClient();
48+
var tagClient = client.GetRepository(1).Tags;
49+
50+
var query = new TagQuery
51+
{
52+
OrderBy = useDefault ? null : "name",
53+
Sort = "asc",
54+
};
55+
56+
// Act
57+
var tags = tagClient.GetAsync(query);
58+
59+
// Assert
60+
Assert.That(tags.Select(t => t.Name), Is.EqualTo(["0.0.1", "0.0.10", "0.0.2", "not-semver"]));
61+
}
62+
63+
[Test]
64+
public void GetTagAsync_CanSortByVersion()
65+
{
66+
// Arrange
67+
using var server = new GitLabConfig()
68+
.WithUser("user1", isDefault: true)
69+
.WithProject("test-project", id: 1, addDefaultUserAsMaintainer: true, configure: project => project
70+
.WithCommit("Initial Commit", tags: ["0.0.1"])
71+
.WithCommit("Second Tag", tags: ["0.0.2"])
72+
.WithCommit("Second Tag", tags: ["not-semver"])
73+
.WithCommit("Other Tag", tags: ["0.0.10"]))
74+
.BuildServer();
75+
76+
var client = server.CreateClient();
77+
var tagClient = client.GetRepository(1).Tags;
78+
79+
var query = new TagQuery
80+
{
81+
OrderBy = "version",
82+
Sort = "asc",
83+
};
84+
85+
// Act
86+
var tags = tagClient.GetAsync(query);
87+
88+
// Assert
89+
Assert.That(tags.Select(t => t.Name), Is.EqualTo(["not-semver", "0.0.1", "0.0.2", "0.0.10"]));
90+
}
3191
}

NGitLab.Mock/Clients/TagClient.cs

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,78 @@ public Tag ToTagClient(LibGit2Sharp.Tag tag)
8585
{
8686
using (Context.BeginOperationScope())
8787
{
88-
var result = GetProject(_projectId, ProjectPermission.View).Repository.GetTags();
88+
IEnumerable<LibGit2Sharp.Tag> result = GetProject(_projectId, ProjectPermission.View).Repository.GetTags();
8989
if (query != null)
9090
{
91-
if (!string.IsNullOrEmpty(query.Sort))
92-
throw new NotImplementedException();
93-
94-
if (!string.IsNullOrEmpty(query.OrderBy))
95-
throw new NotImplementedException();
91+
result = ApplyQuery(result, query.OrderBy, query.Sort);
9692
}
9793

98-
return GitLabCollectionResponse.Create(result.Select(tag => ToTagClient(tag)).ToArray());
94+
return GitLabCollectionResponse.Create(result.Select(ToTagClient).ToArray());
95+
}
96+
97+
static IEnumerable<LibGit2Sharp.Tag> ApplyQuery(IEnumerable<LibGit2Sharp.Tag> tags, string orderBy, string direction)
98+
{
99+
tags = orderBy switch
100+
{
101+
"name" => tags.OrderBy(t => t.FriendlyName, StringComparer.Ordinal),
102+
"version" => tags.OrderBy(t => t.FriendlyName, SemanticVersionComparer.Instance),
103+
null => tags,
104+
105+
// LibGitSharp does not really expose tag creation time, so hard to sort using that annotation,
106+
"updated" => throw new NotSupportedException("Sorting by 'updated' is not supported since the info is not available in LibGit2Sharp."),
107+
_ => throw new NotSupportedException($"Sorting by '{orderBy}' is not supported."),
108+
};
109+
110+
if (string.IsNullOrEmpty(direction))
111+
direction = "desc";
112+
113+
return direction switch
114+
{
115+
"desc" => tags.Reverse(),
116+
"asc" => tags,
117+
_ => throw new NotSupportedException($"Sort direction must be 'asc' or 'desc', got '{direction}' instead"),
118+
};
119+
}
120+
}
121+
122+
private sealed class SemanticVersionComparer : IComparer<string>
123+
{
124+
public static SemanticVersionComparer Instance { get; } = new();
125+
126+
public int Compare(string x, string y)
127+
{
128+
var versionX = ParseVersion(x);
129+
var versionY = ParseVersion(y);
130+
131+
var majorCmp = versionX.Major.CompareTo(versionY.Major);
132+
if (majorCmp != 0)
133+
return majorCmp;
134+
135+
var minorCmp = versionX.Minor.CompareTo(versionY.Minor);
136+
if (minorCmp != 0)
137+
return minorCmp;
138+
139+
return versionX.Patch.CompareTo(versionY.Patch);
140+
}
141+
142+
private static (int Major, int Minor, int Patch) ParseVersion(string version)
143+
{
144+
if (string.IsNullOrEmpty(version))
145+
return (0, 0, 0);
146+
147+
// Strip leading 'v' or 'V' if present
148+
if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase))
149+
version = version[1..];
150+
151+
if (version.IndexOf('-') is int dashIndex and not -1)
152+
version = version[..dashIndex];
153+
154+
var parts = version.Split('.');
155+
var major = parts.Length > 0 && int.TryParse(parts[0], out var m) ? m : 0;
156+
var minor = parts.Length > 1 && int.TryParse(parts[1], out var n) ? n : 0;
157+
var patch = parts.Length > 2 && int.TryParse(parts[2], out var p) ? p : 0;
158+
159+
return (major, minor, patch);
99160
}
100161
}
101162
}

0 commit comments

Comments
 (0)