diff --git a/NGitLab.Mock.Tests/TagTests.cs b/NGitLab.Mock.Tests/TagTests.cs index 54f95d51..63d960ad 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 30c10def..22cbf466 100644 --- a/NGitLab.Mock/Clients/TagClient.cs +++ b/NGitLab.Mock/Clients/TagClient.cs @@ -85,17 +85,78 @@ 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) + { + tags = orderBy switch + { + "name" => tags.OrderBy(t => t.FriendlyName, StringComparer.Ordinal), + "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 + { + "desc" => tags.Reverse(), + "asc" => tags, + _ => 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); } } }