diff --git a/NGitLab.Mock.Tests/TagTests.cs b/NGitLab.Mock.Tests/TagTests.cs index e34f422c..ee64c5eb 100644 --- a/NGitLab.Mock.Tests/TagTests.cs +++ b/NGitLab.Mock.Tests/TagTests.cs @@ -135,4 +135,71 @@ public async Task SearchTags() Assert.That(tags.Count(), Is.EqualTo(expectedCount), $"Expected search expression '{searchExpression}' to return {expectedCount} results."); } } + + [Test] + public async Task EnumerateTags_FivePerPageAndWithStartPageSpecified() + { + // Arrange + using var server = new GitLabConfig() + .WithUser("user1", isDefault: true) + .WithProject("test-project", id: 1, addDefaultUserAsMaintainer: true, configure: project => + { + project.WithCommit("Commit with tags", tags: Enumerable.Range(0, 20).Select(i => $"1.{i}.0").ToArray()); + }) + .BuildServer(); + + var client = server.CreateClient(); + var tagClient = client.GetRepository(1).Tags; + + var perPage = 5; + + (int? StartPage, string[] ExpectedTags)[] testCases = + [ + (null, ["1.19.0", "1.18.0", "1.17.0", "1.16.0", "1.15.0"]), + (1, ["1.19.0", "1.18.0", "1.17.0", "1.16.0", "1.15.0"]), + (2, ["1.14.0", "1.13.0", "1.12.0", "1.11.0", "1.10.0"]), + (4, ["1.4.0", "1.3.0", "1.2.0", "1.1.0", "1.0.0"]), + (5, []), + ]; + + foreach (var (startPage, expectedTags) in testCases) + { + // Act + var tags = tagClient.GetAsync(new TagQuery { OrderBy = "version", PerPage = perPage, Page = startPage }).AsEnumerable().Take(perPage).ToArray(); + + // Assert + Assert.That(tags.Select(t => t.Name), Is.EqualTo(expectedTags)); + } + } + + [Test] + public async Task EnumerateTags_WithPreviousTagSpecified() + { + // Arrange + using var server = new GitLabConfig() + .WithUser("user1", isDefault: true) + .WithProject("test-project", id: 1, addDefaultUserAsMaintainer: true, configure: project => + { + project.WithCommit("Commit with tags", tags: Enumerable.Range(0, 10).Select(i => $"1.{i}.0").ToArray()); + }) + .BuildServer(); + + var client = server.CreateClient(); + var tagClient = client.GetRepository(1).Tags; + + (string PreviousTag, string[] ExpectedTags)[] testCases = + [ + (null, ["1.9.0", "1.8.0", "1.7.0", "1.6.0", "1.5.0", "1.4.0", "1.3.0", "1.2.0", "1.1.0", "1.0.0"]), + ("1.3.0", ["1.2.0", "1.1.0", "1.0.0"]), + ]; + + foreach (var (previousTag, expectedTags) in testCases) + { + // Act + var tags = tagClient.GetAsync(new TagQuery { OrderBy = "version", PageToken = previousTag }).AsEnumerable().ToArray(); + + // Assert + Assert.That(tags.Select(t => t.Name), Is.EqualTo(expectedTags)); + } + } } diff --git a/NGitLab.Mock/Clients/TagClient.cs b/NGitLab.Mock/Clients/TagClient.cs index 43d51295..42cf7714 100644 --- a/NGitLab.Mock/Clients/TagClient.cs +++ b/NGitLab.Mock/Clients/TagClient.cs @@ -141,12 +141,25 @@ public Tag ToTagClient(LibGit2Sharp.Tag tag) if (string.IsNullOrEmpty(direction)) direction = "desc"; - return direction switch + tags = direction switch { "desc" => tags.Reverse(), "asc" => tags, _ => throw new NotSupportedException($"Sort direction must be 'asc' or 'desc', got '{direction}' instead"), }; + + if (!string.IsNullOrEmpty(query.PageToken)) + { + tags = tags.SkipWhile(t => !string.Equals(t.FriendlyName, query.PageToken, StringComparison.Ordinal)).Skip(1); + } + + if (query.Page.HasValue) + { + var skip = (query.Page.Value - 1) * (query.PerPage ?? 20); + tags = tags.Skip(skip); + } + + return tags; } } diff --git a/NGitLab.Tests/TagTests.cs b/NGitLab.Tests/TagTests.cs index 2497f1c9..7811b584 100644 --- a/NGitLab.Tests/TagTests.cs +++ b/NGitLab.Tests/TagTests.cs @@ -125,4 +125,76 @@ public async Task GetTag(string tagNameSought, bool expectExistence) Assert.That(ex.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); } } + + [NGitLabRetry] + [Test] + public async Task EnumerateTags_FivePerPageAndWithStartPageSpecified() + { + // Arrange + using var context = await GitLabTestContext.CreateAsync(); + var project = context.CreateProject(initializeWithCommits: true); + var tagClient = context.Client.GetRepository(project.Id).Tags; + var perPage = 5; + + for (var i = 0; i < 20; i++) + { + tagClient.Create(new TagCreate + { + Name = $"1.{i}.0", + Ref = project.DefaultBranch, + }); + } + + (int? StartPage, string[] ExpectedTags)[] testCases = + [ + (null, ["1.19.0", "1.18.0", "1.17.0", "1.16.0", "1.15.0"]), + (1, ["1.19.0", "1.18.0", "1.17.0", "1.16.0", "1.15.0"]), + (2, ["1.14.0", "1.13.0", "1.12.0", "1.11.0", "1.10.0"]), + (4, ["1.4.0", "1.3.0", "1.2.0", "1.1.0", "1.0.0"]), + (5, []), + ]; + + foreach (var (startPage, expectedTags) in testCases) + { + // Act + var tags = tagClient.GetAsync(new TagQuery { OrderBy = "version", PerPage = perPage, Page = startPage }).AsEnumerable().Take(perPage).ToArray(); + + // Assert + Assert.That(tags.Select(t => t.Name), Is.EqualTo(expectedTags)); + } + } + + [NGitLabRetry] + [Test] + public async Task EnumerateTags_WithPreviousTagSpecified() + { + // Arrange + using var context = await GitLabTestContext.CreateAsync(); + var project = context.CreateProject(initializeWithCommits: true); + var tagClient = context.Client.GetRepository(project.Id).Tags; + + for (var i = 0; i < 10; i++) + { + tagClient.Create(new TagCreate + { + Name = $"1.{i}.0", + Ref = project.DefaultBranch, + }); + } + + (string PreviousTag, string[] ExpectedTags)[] testCases = + [ + (null, ["1.9.0", "1.8.0", "1.7.0", "1.6.0", "1.5.0", "1.4.0", "1.3.0", "1.2.0", "1.1.0", "1.0.0"]), + ("1.3.0", ["1.2.0", "1.1.0", "1.0.0"]), + ]; + + foreach (var (previousTag, expectedTags) in testCases) + { + // Act + var tags = tagClient.GetAsync(new TagQuery { OrderBy = "version", PageToken = previousTag }).AsEnumerable().ToArray(); + + // Assert + Assert.That(tags.Select(t => t.Name), Is.EqualTo(expectedTags)); + } + } } diff --git a/NGitLab/Impl/TagClient.cs b/NGitLab/Impl/TagClient.cs index be2b095c..b00a88b0 100644 --- a/NGitLab/Impl/TagClient.cs +++ b/NGitLab/Impl/TagClient.cs @@ -44,6 +44,8 @@ public GitLabCollectionResponse GetAsync(TagQuery query) url = Utils.AddParameter(url, "sort", query.Sort); url = Utils.AddParameter(url, "per_page", query.PerPage); url = Utils.AddParameter(url, "search", query.Search); + url = Utils.AddParameter(url, "page", query.Page); + url = Utils.AddParameter(url, "page_token", query.PageToken); } return _api.Get().GetAllAsync(url); diff --git a/NGitLab/Models/Tag.cs b/NGitLab/Models/Tag.cs index 237930e7..69332df6 100644 --- a/NGitLab/Models/Tag.cs +++ b/NGitLab/Models/Tag.cs @@ -1,7 +1,9 @@ -using System.Text.Json.Serialization; +using System.Diagnostics; +using System.Text.Json.Serialization; namespace NGitLab.Models; +[DebuggerDisplay("{Name,nq}")] public class Tag { [JsonPropertyName("name")] diff --git a/NGitLab/Models/TagQuery.cs b/NGitLab/Models/TagQuery.cs index 1be0fa8b..d726dafa 100644 --- a/NGitLab/Models/TagQuery.cs +++ b/NGitLab/Models/TagQuery.cs @@ -1,12 +1,38 @@ namespace NGitLab.Models; +/// +/// A filter and sort query when +/// listing all project repository tags. +/// public class TagQuery { + /// + /// Specifies how to order tags, i.e. by "name", "updated" or (semantic) "version". Default is "updated". + /// public string OrderBy { get; set; } + /// + /// Sort order, i.e. "asc" or "desc". Default is "desc". + /// public string Sort { get; set; } + /// + /// Number of results to return per page. Default is 20. + /// public int? PerPage { get; set; } + /// + /// Search criteria. You can use "^term" and "term$" to find tags that begin and end with "term". No other regular expressions are supported. + /// public string Search { get; set; } + + /// + /// Start page number. Default is 1. + /// + public int? Page { get; set; } + + /// + /// Previous tag name, i.e. tag to start the pagination from. Used to fetch the next set of results. + /// + public string PageToken { get; set; } } diff --git a/NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 7056392f..c18e7499 100644 --- a/NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -4665,6 +4665,10 @@ NGitLab.Models.TagProtect.TagProtect(string name) -> void NGitLab.Models.TagQuery NGitLab.Models.TagQuery.OrderBy.get -> string NGitLab.Models.TagQuery.OrderBy.set -> void +NGitLab.Models.TagQuery.Page.get -> int? +NGitLab.Models.TagQuery.Page.set -> void +NGitLab.Models.TagQuery.PageToken.get -> string +NGitLab.Models.TagQuery.PageToken.set -> void NGitLab.Models.TagQuery.PerPage.get -> int? NGitLab.Models.TagQuery.PerPage.set -> void NGitLab.Models.TagQuery.Search.get -> string diff --git a/NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt index 8bf1fe56..05c95fac 100644 --- a/NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -4666,6 +4666,10 @@ NGitLab.Models.TagProtect.TagProtect(string name) -> void NGitLab.Models.TagQuery NGitLab.Models.TagQuery.OrderBy.get -> string NGitLab.Models.TagQuery.OrderBy.set -> void +NGitLab.Models.TagQuery.Page.get -> int? +NGitLab.Models.TagQuery.Page.set -> void +NGitLab.Models.TagQuery.PageToken.get -> string +NGitLab.Models.TagQuery.PageToken.set -> void NGitLab.Models.TagQuery.PerPage.get -> int? NGitLab.Models.TagQuery.PerPage.set -> void NGitLab.Models.TagQuery.Search.get -> string diff --git a/NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 7056392f..c18e7499 100644 --- a/NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -4665,6 +4665,10 @@ NGitLab.Models.TagProtect.TagProtect(string name) -> void NGitLab.Models.TagQuery NGitLab.Models.TagQuery.OrderBy.get -> string NGitLab.Models.TagQuery.OrderBy.set -> void +NGitLab.Models.TagQuery.Page.get -> int? +NGitLab.Models.TagQuery.Page.set -> void +NGitLab.Models.TagQuery.PageToken.get -> string +NGitLab.Models.TagQuery.PageToken.set -> void NGitLab.Models.TagQuery.PerPage.get -> int? NGitLab.Models.TagQuery.PerPage.set -> void NGitLab.Models.TagQuery.Search.get -> string diff --git a/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 8bf1fe56..05c95fac 100644 --- a/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -4666,6 +4666,10 @@ NGitLab.Models.TagProtect.TagProtect(string name) -> void NGitLab.Models.TagQuery NGitLab.Models.TagQuery.OrderBy.get -> string NGitLab.Models.TagQuery.OrderBy.set -> void +NGitLab.Models.TagQuery.Page.get -> int? +NGitLab.Models.TagQuery.Page.set -> void +NGitLab.Models.TagQuery.PageToken.get -> string +NGitLab.Models.TagQuery.PageToken.set -> void NGitLab.Models.TagQuery.PerPage.get -> int? NGitLab.Models.TagQuery.PerPage.set -> void NGitLab.Models.TagQuery.Search.get -> string