Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion NGitLab.Mock.Tests/TagTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -28,4 +30,62 @@ public async Task GetTagAsync()
var ex = Assert.ThrowsAsync<GitLabException>(() => 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"]));
}
}
75 changes: 68 additions & 7 deletions NGitLab.Mock/Clients/TagClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,78 @@ public Tag ToTagClient(LibGit2Sharp.Tag tag)
{
using (Context.BeginOperationScope())
{
var result = GetProject(_projectId, ProjectPermission.View).Repository.GetTags();
IEnumerable<LibGit2Sharp.Tag> 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<LibGit2Sharp.Tag> ApplyQuery(IEnumerable<LibGit2Sharp.Tag> 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<string>
{
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);
}
}
}
Loading