From 0e3a1057a905b2b3e425ef588a0536fcec9ec97f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Myka=C3=ABl=20Lemieux-Lafontaine?= Date: Tue, 3 Feb 2026 14:53:37 -0500 Subject: [PATCH 1/3] tests: Implement TagClient.GetAsync with a query --- NGitLab.Mock.Tests/TagTests.cs | 62 ++++++++++++++++++++++++++++++- NGitLab.Mock/Clients/TagClient.cs | 34 +++++++++++++---- NGitLab.Mock/NGitLab.Mock.csproj | 1 + 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/NGitLab.Mock.Tests/TagTests.cs b/NGitLab.Mock.Tests/TagTests.cs index 54f95d515..63d960ad3 100644 --- a/NGitLab.Mock.Tests/TagTests.cs +++ b/NGitLab.Mock.Tests/TagTests.cs @@ -1,6 +1,8 @@ -using System.Net; +using System.Linq; +using System.Net; using System.Threading.Tasks; using NGitLab.Mock.Config; +using NGitLab.Models; using NUnit.Framework; namespace NGitLab.Mock.Tests; @@ -28,4 +30,62 @@ public async Task GetTagAsync() var ex = Assert.ThrowsAsync(() => tagClient.GetByNameAsync("1.0.1")); Assert.That(ex.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); } + + [Theory] + public void GetTaskAsync_CanSortByName([Values] bool useDefault) + { + // Arrange + using var server = new GitLabConfig() + .WithUser("user1", isDefault: true) + .WithProject("test-project", id: 1, addDefaultUserAsMaintainer: true, configure: project => project + .WithCommit("Initial Commit", tags: ["0.0.1"]) + .WithCommit("Second Tag", tags: ["0.0.2"]) + .WithCommit("Second Tag", tags: ["not-semver"]) + .WithCommit("Other Tag", tags: ["0.0.10"])) + .BuildServer(); + + var client = server.CreateClient(); + var tagClient = client.GetRepository(1).Tags; + + var query = new TagQuery + { + OrderBy = useDefault ? null : "name", + Sort = "asc", + }; + + // Act + var tags = tagClient.GetAsync(query); + + // Assert + Assert.That(tags.Select(t => t.Name), Is.EqualTo(["0.0.1", "0.0.10", "0.0.2", "not-semver"])); + } + + [Test] + public void GetTagAsync_CanSortByVersion() + { + // Arrange + using var server = new GitLabConfig() + .WithUser("user1", isDefault: true) + .WithProject("test-project", id: 1, addDefaultUserAsMaintainer: true, configure: project => project + .WithCommit("Initial Commit", tags: ["0.0.1"]) + .WithCommit("Second Tag", tags: ["0.0.2"]) + .WithCommit("Second Tag", tags: ["not-semver"]) + .WithCommit("Other Tag", tags: ["0.0.10"])) + .BuildServer(); + + var client = server.CreateClient(); + var tagClient = client.GetRepository(1).Tags; + + var query = new TagQuery + { + OrderBy = "version", + Sort = "asc", + }; + + // Act + var tags = tagClient.GetAsync(query); + + // Assert + Assert.That(tags.Select(t => t.Name), Is.EqualTo(["not-semver", "0.0.1", "0.0.2", "0.0.10"])); + } } diff --git a/NGitLab.Mock/Clients/TagClient.cs b/NGitLab.Mock/Clients/TagClient.cs index 30c10def7..76036e239 100644 --- a/NGitLab.Mock/Clients/TagClient.cs +++ b/NGitLab.Mock/Clients/TagClient.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using NGitLab.Mock.Internals; using NGitLab.Models; +using NuGet.Versioning; using Commit = LibGit2Sharp.Commit; namespace NGitLab.Mock.Clients; @@ -85,17 +86,36 @@ public Tag ToTagClient(LibGit2Sharp.Tag tag) { using (Context.BeginOperationScope()) { - var result = GetProject(_projectId, ProjectPermission.View).Repository.GetTags(); + IEnumerable result = GetProject(_projectId, ProjectPermission.View).Repository.GetTags(); if (query != null) { - if (!string.IsNullOrEmpty(query.Sort)) - throw new NotImplementedException(); - - if (!string.IsNullOrEmpty(query.OrderBy)) - throw new NotImplementedException(); + result = ApplyQuery(result, query.OrderBy, query.Sort); } - return GitLabCollectionResponse.Create(result.Select(tag => ToTagClient(tag)).ToArray()); + return GitLabCollectionResponse.Create(result.Select(ToTagClient).ToArray()); + } + + static IEnumerable ApplyQuery(IEnumerable tags, string? orderBy, string? direction) + { + if (string.IsNullOrEmpty(direction)) + direction = "desc"; + + // LibGitSharp does not really expose tag creation time, so hard to sort using that annotation, + // we'll skip sorting in that case (which should be functionnaly similar to "name" sorting) + tags = orderBy switch + { + "name" => tags.OrderBy(t => t.FriendlyName, StringComparer.Ordinal), + "updated" or null => tags, + "version" => tags.OrderBy(t => SemanticVersion.TryParse(t.FriendlyName, out var s) ? s : null), + _ => throw new NotImplementedException(), + }; + + return direction switch + { + null or "desc" => tags.Reverse(), + "asc" => tags, + _ => throw new NotImplementedException(), + }; } } } diff --git a/NGitLab.Mock/NGitLab.Mock.csproj b/NGitLab.Mock/NGitLab.Mock.csproj index 210f75b1c..055b8dfd2 100644 --- a/NGitLab.Mock/NGitLab.Mock.csproj +++ b/NGitLab.Mock/NGitLab.Mock.csproj @@ -13,6 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + From f94ab41a84cbdfd26f88582f526afea07eb61c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Myka=C3=ABl=20Lemieux-Lafontaine?= Date: Tue, 3 Feb 2026 15:07:29 -0500 Subject: [PATCH 2/3] Fix warnings --- NGitLab.Mock/Clients/TagClient.cs | 2 +- NGitLab.Mock/NGitLab.Mock.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NGitLab.Mock/Clients/TagClient.cs b/NGitLab.Mock/Clients/TagClient.cs index 76036e239..4e08c4245 100644 --- a/NGitLab.Mock/Clients/TagClient.cs +++ b/NGitLab.Mock/Clients/TagClient.cs @@ -95,7 +95,7 @@ public Tag ToTagClient(LibGit2Sharp.Tag tag) return GitLabCollectionResponse.Create(result.Select(ToTagClient).ToArray()); } - static IEnumerable ApplyQuery(IEnumerable tags, string? orderBy, string? direction) + static IEnumerable ApplyQuery(IEnumerable tags, string orderBy, string direction) { if (string.IsNullOrEmpty(direction)) direction = "desc"; diff --git a/NGitLab.Mock/NGitLab.Mock.csproj b/NGitLab.Mock/NGitLab.Mock.csproj index 055b8dfd2..93910a0d9 100644 --- a/NGitLab.Mock/NGitLab.Mock.csproj +++ b/NGitLab.Mock/NGitLab.Mock.csproj @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + From 953f428424509677b707e293cc49a37c51d20d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Myka=C3=ABl=20Lemieux-Lafontaine?= Date: Tue, 3 Feb 2026 15:51:01 -0500 Subject: [PATCH 3/3] PR comments --- NGitLab.Mock/Clients/TagClient.cs | 63 +++++++++++++++++++++++++------ NGitLab.Mock/NGitLab.Mock.csproj | 1 - 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/NGitLab.Mock/Clients/TagClient.cs b/NGitLab.Mock/Clients/TagClient.cs index 4e08c4245..22cbf4661 100644 --- a/NGitLab.Mock/Clients/TagClient.cs +++ b/NGitLab.Mock/Clients/TagClient.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using NGitLab.Mock.Internals; using NGitLab.Models; -using NuGet.Versioning; using Commit = LibGit2Sharp.Commit; namespace NGitLab.Mock.Clients; @@ -97,25 +96,67 @@ public Tag ToTagClient(LibGit2Sharp.Tag tag) static IEnumerable ApplyQuery(IEnumerable tags, string orderBy, string direction) { - if (string.IsNullOrEmpty(direction)) - direction = "desc"; - - // LibGitSharp does not really expose tag creation time, so hard to sort using that annotation, - // we'll skip sorting in that case (which should be functionnaly similar to "name" sorting) tags = orderBy switch { "name" => tags.OrderBy(t => t.FriendlyName, StringComparer.Ordinal), - "updated" or null => tags, - "version" => tags.OrderBy(t => SemanticVersion.TryParse(t.FriendlyName, out var s) ? s : null), - _ => throw new NotImplementedException(), + "version" => tags.OrderBy(t => t.FriendlyName, SemanticVersionComparer.Instance), + null => tags, + + // LibGitSharp does not really expose tag creation time, so hard to sort using that annotation, + "updated" => throw new NotSupportedException("Sorting by 'updated' is not supported since the info is not available in LibGit2Sharp."), + _ => throw new NotSupportedException($"Sorting by '{orderBy}' is not supported."), }; + if (string.IsNullOrEmpty(direction)) + direction = "desc"; + return direction switch { - null or "desc" => tags.Reverse(), + "desc" => tags.Reverse(), "asc" => tags, - _ => throw new NotImplementedException(), + _ => throw new NotSupportedException($"Sort direction must be 'asc' or 'desc', got '{direction}' instead"), }; } } + + private sealed class SemanticVersionComparer : IComparer + { + public static SemanticVersionComparer Instance { get; } = new(); + + public int Compare(string x, string y) + { + var versionX = ParseVersion(x); + var versionY = ParseVersion(y); + + var majorCmp = versionX.Major.CompareTo(versionY.Major); + if (majorCmp != 0) + return majorCmp; + + var minorCmp = versionX.Minor.CompareTo(versionY.Minor); + if (minorCmp != 0) + return minorCmp; + + return versionX.Patch.CompareTo(versionY.Patch); + } + + private static (int Major, int Minor, int Patch) ParseVersion(string version) + { + if (string.IsNullOrEmpty(version)) + return (0, 0, 0); + + // Strip leading 'v' or 'V' if present + if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + version = version[1..]; + + if (version.IndexOf('-') is int dashIndex and not -1) + version = version[..dashIndex]; + + var parts = version.Split('.'); + var major = parts.Length > 0 && int.TryParse(parts[0], out var m) ? m : 0; + var minor = parts.Length > 1 && int.TryParse(parts[1], out var n) ? n : 0; + var patch = parts.Length > 2 && int.TryParse(parts[2], out var p) ? p : 0; + + return (major, minor, patch); + } + } } diff --git a/NGitLab.Mock/NGitLab.Mock.csproj b/NGitLab.Mock/NGitLab.Mock.csproj index 93910a0d9..210f75b1c 100644 --- a/NGitLab.Mock/NGitLab.Mock.csproj +++ b/NGitLab.Mock/NGitLab.Mock.csproj @@ -13,7 +13,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive -