From 0f832d65023174edefa676389c80c3dd15f6ebda Mon Sep 17 00:00:00 2001 From: Steve Ryan Date: Thu, 11 Apr 2024 14:51:12 +1000 Subject: [PATCH 1/3] Implements SHA and Format parameters for RepositoryClient.GetArchive --- NGitLab.Mock/Clients/RepositoryClient.cs | 4 +- .../RepositoryClient/RepositoryClientTests.cs | 97 +++++++++++++++++++ NGitLab/IRepositoryClient.cs | 4 +- NGitLab/Impl/RepositoryClient.cs | 14 ++- NGitLab/PublicAPI.Unshipped.txt | 4 +- 5 files changed, 114 insertions(+), 9 deletions(-) diff --git a/NGitLab.Mock/Clients/RepositoryClient.cs b/NGitLab.Mock/Clients/RepositoryClient.cs index d0ecef6f..fca73d2e 100644 --- a/NGitLab.Mock/Clients/RepositoryClient.cs +++ b/NGitLab.Mock/Clients/RepositoryClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -80,7 +80,7 @@ public void GetRawBlob(string sha, Action parser) throw new NotImplementedException(); } - public void GetArchive(Action parser) + public void GetArchive(Action parser, string sha = null, string format = null) { throw new NotImplementedException(); } diff --git a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs index 85ee5891..b9ec4f3e 100644 --- a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs +++ b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs @@ -353,4 +353,101 @@ public async Task GetCommitRefs(CommitRefType type) Assert.That(commitRefs, Is.Not.Empty); } } + + [Test] + [NGitLabRetry] + public async Task GetArchiveWithoutOptionalParameters() + { + // Arrange + using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); + + // Act + context.RepositoryClient.GetArchive((stream) => { }); + + // Assert + var requestPathAndQuery = context.Context.LastRequest.RequestUri.PathAndQuery; + + Assert.Multiple(() => + { + Assert.That(requestPathAndQuery, Is.Not.Null); + Assert.That(requestPathAndQuery.EndsWith("/archive", StringComparison.OrdinalIgnoreCase), Is.True); + }); + } + + [Test] + [NGitLabRetry] + public async Task GetArchiveAcceptsShaParameter() + { + // Arrange + using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); + var firstCommitId = context.Commits[0].Id.ToString(); + + // Act + context.RepositoryClient.GetArchive((stream) => { }, sha: firstCommitId); + + // Assert + var requestPathAndQuery = context.Context.LastRequest.RequestUri.PathAndQuery; + + Assert.Multiple(() => + { + Assert.That(requestPathAndQuery, Is.Not.Null); + Assert.That(requestPathAndQuery.Contains($"?sha={firstCommitId}", StringComparison.OrdinalIgnoreCase), Is.True); + }); + } + + [Test] + [NGitLabRetry] + public async Task GetArchiveAcceptsFormatParameter() + { + // Arrange + using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); + var format = ".zip"; + + // Act + context.RepositoryClient.GetArchive((stream) => { }, format: format); + + // Assert + var requestPathAndQuery = context.Context.LastRequest.RequestUri.PathAndQuery; + + Assert.Multiple(() => + { + Assert.That(requestPathAndQuery, Is.Not.Null); + Assert.That(requestPathAndQuery.Contains($"/archive{format}", StringComparison.OrdinalIgnoreCase), Is.True); + }); + } + + [Test] + [NGitLabRetry] + public async Task GetArchiveAcceptsShaAndFormatParametersTogether() + { + // Arrange + using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); + var format = ".zip"; + var firstCommitId = context.Commits[0].Id.ToString(); + + // Act + context.RepositoryClient.GetArchive((stream) => { }, sha: firstCommitId, format: format); + + // Assert + var requestPathAndQuery = context.Context.LastRequest.RequestUri.PathAndQuery; + + Assert.Multiple(() => + { + Assert.That(requestPathAndQuery, Is.Not.Null); + Assert.That(requestPathAndQuery.Contains($"/archive{format}", StringComparison.OrdinalIgnoreCase), Is.True); + Assert.That(requestPathAndQuery.Contains($"?sha={firstCommitId}", StringComparison.OrdinalIgnoreCase), Is.True); + }); + } + + [Test] + [NGitLabRetry] + public async Task GetArchiveThrowsExceptionWhenFormatDoesNotStartWithDot() + { + // Arrange + using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); + var format = "zip"; + + // Act and Assert + Assert.Throws(() => context.RepositoryClient.GetArchive((stream) => { }, format: format)); + } } diff --git a/NGitLab/IRepositoryClient.cs b/NGitLab/IRepositoryClient.cs index 1734ff8a..d0dec67e 100644 --- a/NGitLab/IRepositoryClient.cs +++ b/NGitLab/IRepositoryClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using NGitLab.Models; @@ -21,7 +21,7 @@ public interface IRepositoryClient void GetRawBlob(string sha, Action parser); - void GetArchive(Action parser); + void GetArchive(Action parser, string sha = null, string format = null); IEnumerable Commits { get; } diff --git a/NGitLab/Impl/RepositoryClient.cs b/NGitLab/Impl/RepositoryClient.cs index c9b00a12..1ec785d4 100644 --- a/NGitLab/Impl/RepositoryClient.cs +++ b/NGitLab/Impl/RepositoryClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -55,9 +55,17 @@ public void GetRawBlob(string sha, Action parser) _api.Get().Stream(_repoPath + "/raw_blobs/" + sha, parser); } - public void GetArchive(Action parser) + public void GetArchive(Action parser, string sha = null, string format = null) { - _api.Get().Stream(_repoPath + "/archive", parser); + if (!string.IsNullOrEmpty(format) && !format.StartsWith(".", StringComparison.Ordinal)) + throw new ArgumentException($"Format must include the '.' as part of extension", nameof(format)); + + var relativePath = $"/archive{format}"; + + if (!string.IsNullOrEmpty(sha)) + relativePath += $"?sha={sha}"; + + _api.Get().Stream(_repoPath + relativePath, parser); } public IEnumerable Commits => _api.Get().GetAll(_repoPath + $"/commits?per_page={GetCommitsRequest.DefaultPerPage}"); diff --git a/NGitLab/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI.Unshipped.txt index d1f8634f..24d96190 100644 --- a/NGitLab/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI.Unshipped.txt @@ -873,7 +873,7 @@ NGitLab.Impl.RepositoryClient.Commits.get -> System.Collections.Generic.IEnumera NGitLab.Impl.RepositoryClient.Compare(NGitLab.Models.CompareQuery query) -> NGitLab.Models.CompareResults NGitLab.Impl.RepositoryClient.Contributors.get -> NGitLab.IContributorClient NGitLab.Impl.RepositoryClient.Files.get -> NGitLab.IFilesClient -NGitLab.Impl.RepositoryClient.GetArchive(System.Action parser) -> void +NGitLab.Impl.RepositoryClient.GetArchive(System.Action parser, string sha = null, string format = null) -> void NGitLab.Impl.RepositoryClient.GetCommit(NGitLab.Sha1 sha) -> NGitLab.Models.Commit NGitLab.Impl.RepositoryClient.GetCommitDiff(NGitLab.Sha1 sha) -> System.Collections.Generic.IEnumerable NGitLab.Impl.RepositoryClient.GetCommitRefs(NGitLab.Sha1 sha, NGitLab.Models.CommitRefType type = NGitLab.Models.CommitRefType.All) -> System.Collections.Generic.IEnumerable @@ -1094,7 +1094,7 @@ NGitLab.IRepositoryClient.Commits.get -> System.Collections.Generic.IEnumerable< NGitLab.IRepositoryClient.Compare(NGitLab.Models.CompareQuery query) -> NGitLab.Models.CompareResults NGitLab.IRepositoryClient.Contributors.get -> NGitLab.IContributorClient NGitLab.IRepositoryClient.Files.get -> NGitLab.IFilesClient -NGitLab.IRepositoryClient.GetArchive(System.Action parser) -> void +NGitLab.IRepositoryClient.GetArchive(System.Action parser, string sha = null, string format = null) -> void NGitLab.IRepositoryClient.GetCommit(NGitLab.Sha1 sha) -> NGitLab.Models.Commit NGitLab.IRepositoryClient.GetCommitDiff(NGitLab.Sha1 sha) -> System.Collections.Generic.IEnumerable NGitLab.IRepositoryClient.GetCommitRefs(NGitLab.Sha1 sha, NGitLab.Models.CommitRefType type = NGitLab.Models.CommitRefType.All) -> System.Collections.Generic.IEnumerable From faaebbfeaf5c2b1f170442eb8fe4eca3d624b145 Mon Sep 17 00:00:00 2001 From: Steve Ryan Date: Fri, 12 Apr 2024 12:44:46 +1000 Subject: [PATCH 2/3] Changes to prevent breaking change to IRepositoryClient.GetArchive Adds a new overload to IRepository.GetArchive which accepts a query object in order to allow additional parameters to be passed to the the archive endpoint without breaking the existing implementation. --- NGitLab.Mock/Clients/RepositoryClient.cs | 7 +- .../RepositoryClient/RepositoryClientTests.cs | 96 +++++++++++++++---- NGitLab/Extensions/TypeExtensions.cs | 20 ++++ NGitLab/IRepositoryClient.cs | 4 +- NGitLab/Impl/RepositoryClient.cs | 26 +++-- NGitLab/Models/FileArchiveFormat.cs | 33 +++++++ NGitLab/Models/FileArchiveQuery.cs | 12 +++ NGitLab/PublicAPI.Unshipped.txt | 24 ++++- 8 files changed, 190 insertions(+), 32 deletions(-) create mode 100644 NGitLab/Extensions/TypeExtensions.cs create mode 100644 NGitLab/Models/FileArchiveFormat.cs create mode 100644 NGitLab/Models/FileArchiveQuery.cs diff --git a/NGitLab.Mock/Clients/RepositoryClient.cs b/NGitLab.Mock/Clients/RepositoryClient.cs index fca73d2e..66faae3b 100644 --- a/NGitLab.Mock/Clients/RepositoryClient.cs +++ b/NGitLab.Mock/Clients/RepositoryClient.cs @@ -80,7 +80,12 @@ public void GetRawBlob(string sha, Action parser) throw new NotImplementedException(); } - public void GetArchive(Action parser, string sha = null, string format = null) + public void GetArchive(Action parser) + { + throw new NotImplementedException(); + } + + public void GetArchive(Action parser, FileArchiveQuery fileArchiveQuery) { throw new NotImplementedException(); } diff --git a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs index b9ec4f3e..bfe9403f 100644 --- a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs +++ b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs @@ -356,10 +356,10 @@ public async Task GetCommitRefs(CommitRefType type) [Test] [NGitLabRetry] - public async Task GetArchiveWithoutOptionalParameters() + public async Task GetArchive() { // Arrange - using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); + using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2).ConfigureAwait(false); // Act context.RepositoryClient.GetArchive((stream) => { }); @@ -376,14 +376,14 @@ public async Task GetArchiveWithoutOptionalParameters() [Test] [NGitLabRetry] - public async Task GetArchiveAcceptsShaParameter() + public async Task GetArchiveWithNullQueryPassesNoParameters() { // Arrange - using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); + using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2).ConfigureAwait(false); var firstCommitId = context.Commits[0].Id.ToString(); // Act - context.RepositoryClient.GetArchive((stream) => { }, sha: firstCommitId); + context.RepositoryClient.GetArchive((stream) => { }, fileArchiveQuery: null); // Assert var requestPathAndQuery = context.Context.LastRequest.RequestUri.PathAndQuery; @@ -391,20 +391,31 @@ public async Task GetArchiveAcceptsShaParameter() Assert.Multiple(() => { Assert.That(requestPathAndQuery, Is.Not.Null); - Assert.That(requestPathAndQuery.Contains($"?sha={firstCommitId}", StringComparison.OrdinalIgnoreCase), Is.True); + Assert.That(requestPathAndQuery.EndsWith("/archive", StringComparison.OrdinalIgnoreCase), Is.True); }); } - [Test] + [TestCase(null, "")] + [TestCase(FileArchiveFormat.Bz2, ".bz2")] + [TestCase(FileArchiveFormat.Gz, ".gz")] + [TestCase(FileArchiveFormat.Tar, ".tar")] + [TestCase(FileArchiveFormat.TarBz2, ".tar.bz2")] + [TestCase(FileArchiveFormat.TarGz, ".tar.gz")] + [TestCase(FileArchiveFormat.Tb2, ".tb2")] + [TestCase(FileArchiveFormat.Tbz2, ".tbz2")] + [TestCase(FileArchiveFormat.Zip, ".zip")] [NGitLabRetry] - public async Task GetArchiveAcceptsFormatParameter() + public async Task GetArchiveFormatValuePassedCorrectly(FileArchiveFormat? archiveFormat, string expectedExtension) { // Arrange using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); - var format = ".zip"; + var fileArchiveQuery = new FileArchiveQuery + { + Format = archiveFormat, + }; // Act - context.RepositoryClient.GetArchive((stream) => { }, format: format); + context.RepositoryClient.GetArchive((stream) => { }, fileArchiveQuery); // Assert var requestPathAndQuery = context.Context.LastRequest.RequestUri.PathAndQuery; @@ -412,21 +423,24 @@ public async Task GetArchiveAcceptsFormatParameter() Assert.Multiple(() => { Assert.That(requestPathAndQuery, Is.Not.Null); - Assert.That(requestPathAndQuery.Contains($"/archive{format}", StringComparison.OrdinalIgnoreCase), Is.True); + Assert.That(requestPathAndQuery.EndsWith($"/archive{expectedExtension}", StringComparison.OrdinalIgnoreCase), Is.True); }); } [Test] [NGitLabRetry] - public async Task GetArchiveAcceptsShaAndFormatParametersTogether() + public async Task GetArchiveShaValuePassedCorrectly() { // Arrange using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); - var format = ".zip"; var firstCommitId = context.Commits[0].Id.ToString(); + var fileArchiveQuery = new FileArchiveQuery + { + Ref = firstCommitId, + }; // Act - context.RepositoryClient.GetArchive((stream) => { }, sha: firstCommitId, format: format); + context.RepositoryClient.GetArchive((stream) => { }, fileArchiveQuery); // Assert var requestPathAndQuery = context.Context.LastRequest.RequestUri.PathAndQuery; @@ -434,20 +448,62 @@ public async Task GetArchiveAcceptsShaAndFormatParametersTogether() Assert.Multiple(() => { Assert.That(requestPathAndQuery, Is.Not.Null); - Assert.That(requestPathAndQuery.Contains($"/archive{format}", StringComparison.OrdinalIgnoreCase), Is.True); - Assert.That(requestPathAndQuery.Contains($"?sha={firstCommitId}", StringComparison.OrdinalIgnoreCase), Is.True); + Assert.That(requestPathAndQuery.Contains($"sha={firstCommitId}", StringComparison.OrdinalIgnoreCase), Is.True); }); } [Test] [NGitLabRetry] - public async Task GetArchiveThrowsExceptionWhenFormatDoesNotStartWithDot() + public async Task GetArchivePathValuePassedCorrectly() { // Arrange using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); - var format = "zip"; + var path = RepositoryClientTestsContext.SubfolderName; + var fileArchiveQuery = new FileArchiveQuery + { + Path = path, + }; - // Act and Assert - Assert.Throws(() => context.RepositoryClient.GetArchive((stream) => { }, format: format)); + // Act + context.RepositoryClient.GetArchive((stream) => { }, fileArchiveQuery); + + // Assert + var requestPathAndQuery = context.Context.LastRequest.RequestUri.PathAndQuery; + + Assert.Multiple(() => + { + Assert.That(requestPathAndQuery, Is.Not.Null); + Assert.That(requestPathAndQuery.Contains($"path={path}", StringComparison.OrdinalIgnoreCase), Is.True); + }); + } + + [Test] + [NGitLabRetry] + public async Task GetArchiveCombinationOfValuesPassedCorrectly() + { + // Arrange + using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); + var firstCommitId = context.Commits[0].Id.ToString(); + var path = RepositoryClientTestsContext.SubfolderName; + var fileArchiveQuery = new FileArchiveQuery + { + Format = FileArchiveFormat.Zip, + Path = path, + Ref = firstCommitId, + }; + + // Act + context.RepositoryClient.GetArchive((stream) => { }, fileArchiveQuery); + + // Assert + var requestPathAndQuery = context.Context.LastRequest.RequestUri.PathAndQuery; + + Assert.Multiple(() => + { + Assert.That(requestPathAndQuery, Is.Not.Null); + Assert.That(requestPathAndQuery.Contains($"/archive.zip", StringComparison.OrdinalIgnoreCase), Is.True); + Assert.That(requestPathAndQuery.Contains($"path={path}", StringComparison.OrdinalIgnoreCase), Is.True); + Assert.That(requestPathAndQuery.Contains($"sha={firstCommitId}", StringComparison.OrdinalIgnoreCase), Is.True); + }); } } diff --git a/NGitLab/Extensions/TypeExtensions.cs b/NGitLab/Extensions/TypeExtensions.cs new file mode 100644 index 00000000..54d7d924 --- /dev/null +++ b/NGitLab/Extensions/TypeExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; + +namespace NGitLab.Extensions; + +internal static class TypeExtensions +{ + public static string GetEnumMemberAttributeValue(this TEnum value) + where TEnum : Enum + { + return typeof(TEnum) + .GetTypeInfo() + .DeclaredMembers + .SingleOrDefault(x => string.Equals(x.Name, value.ToString(), StringComparison.Ordinal)) + ?.GetCustomAttribute(inherit: false) + ?.Value; + } +} diff --git a/NGitLab/IRepositoryClient.cs b/NGitLab/IRepositoryClient.cs index d0dec67e..a57a0512 100644 --- a/NGitLab/IRepositoryClient.cs +++ b/NGitLab/IRepositoryClient.cs @@ -21,7 +21,9 @@ public interface IRepositoryClient void GetRawBlob(string sha, Action parser); - void GetArchive(Action parser, string sha = null, string format = null); + void GetArchive(Action parser); + + void GetArchive(Action parser, FileArchiveQuery fileArchiveQuery); IEnumerable Commits { get; } diff --git a/NGitLab/Impl/RepositoryClient.cs b/NGitLab/Impl/RepositoryClient.cs index 1ec785d4..1c228cbb 100644 --- a/NGitLab/Impl/RepositoryClient.cs +++ b/NGitLab/Impl/RepositoryClient.cs @@ -55,17 +55,27 @@ public void GetRawBlob(string sha, Action parser) _api.Get().Stream(_repoPath + "/raw_blobs/" + sha, parser); } - public void GetArchive(Action parser, string sha = null, string format = null) - { - if (!string.IsNullOrEmpty(format) && !format.StartsWith(".", StringComparison.Ordinal)) - throw new ArgumentException($"Format must include the '.' as part of extension", nameof(format)); + public void GetArchive(Action parser) => GetArchive(parser, fileArchiveQuery: null); - var relativePath = $"/archive{format}"; + public void GetArchive(Action parser, FileArchiveQuery fileArchiveQuery) + { + var url = $"{_repoPath}/archive"; - if (!string.IsNullOrEmpty(sha)) - relativePath += $"?sha={sha}"; + if (fileArchiveQuery != null) + { + // If a particular archive file format is requested, it is appended to the path directly as follows: + // /project/123/repository/archive.zip + // /project/123/repository/archive.tar + if (fileArchiveQuery.Format.HasValue) + { + url += fileArchiveQuery.Format.Value.GetEnumMemberAttributeValue(); + } + + url = Utils.AddParameter(url, "path", fileArchiveQuery.Path); + url = Utils.AddParameter(url, "sha", fileArchiveQuery.Ref); + } - _api.Get().Stream(_repoPath + relativePath, parser); + _api.Get().Stream(url, parser); } public IEnumerable Commits => _api.Get().GetAll(_repoPath + $"/commits?per_page={GetCommitsRequest.DefaultPerPage}"); diff --git a/NGitLab/Models/FileArchiveFormat.cs b/NGitLab/Models/FileArchiveFormat.cs new file mode 100644 index 00000000..634ce24a --- /dev/null +++ b/NGitLab/Models/FileArchiveFormat.cs @@ -0,0 +1,33 @@ +using System.Runtime.Serialization; + +namespace NGitLab.Models; + +public enum FileArchiveFormat +{ + [EnumMember(Value = ".bz2")] + Bz2, + + [EnumMember(Value = ".gz")] + Gz, + + [EnumMember(Value = ".tar")] + Tar, + + [EnumMember(Value = ".tar.bz2")] + TarBz2, + + [EnumMember(Value = ".tar.gz")] + TarGz, + + [EnumMember(Value = ".tb2")] + Tb2, + + [EnumMember(Value = ".tbz")] + Tbz, + + [EnumMember(Value = ".tbz2")] + Tbz2, + + [EnumMember(Value = ".zip")] + Zip, +} diff --git a/NGitLab/Models/FileArchiveQuery.cs b/NGitLab/Models/FileArchiveQuery.cs new file mode 100644 index 00000000..c6c87ea9 --- /dev/null +++ b/NGitLab/Models/FileArchiveQuery.cs @@ -0,0 +1,12 @@ +namespace NGitLab.Models; + +public sealed class FileArchiveQuery +{ + public FileArchiveFormat? Format { get; set; } + + // This property is named Ref because even though the query string parameter key is 'sha' it accepts any ref + // i.e. branch name, sha, tag + public string Ref { get; set; } + + public string Path { get; set; } +} diff --git a/NGitLab/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI.Unshipped.txt index 24d96190..1997c980 100644 --- a/NGitLab/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI.Unshipped.txt @@ -873,7 +873,8 @@ NGitLab.Impl.RepositoryClient.Commits.get -> System.Collections.Generic.IEnumera NGitLab.Impl.RepositoryClient.Compare(NGitLab.Models.CompareQuery query) -> NGitLab.Models.CompareResults NGitLab.Impl.RepositoryClient.Contributors.get -> NGitLab.IContributorClient NGitLab.Impl.RepositoryClient.Files.get -> NGitLab.IFilesClient -NGitLab.Impl.RepositoryClient.GetArchive(System.Action parser, string sha = null, string format = null) -> void +NGitLab.Impl.RepositoryClient.GetArchive(System.Action parser) -> void +NGitLab.Impl.RepositoryClient.GetArchive(System.Action parser, NGitLab.Models.FileArchiveQuery fileArchiveQuery) -> void NGitLab.Impl.RepositoryClient.GetCommit(NGitLab.Sha1 sha) -> NGitLab.Models.Commit NGitLab.Impl.RepositoryClient.GetCommitDiff(NGitLab.Sha1 sha) -> System.Collections.Generic.IEnumerable NGitLab.Impl.RepositoryClient.GetCommitRefs(NGitLab.Sha1 sha, NGitLab.Models.CommitRefType type = NGitLab.Models.CommitRefType.All) -> System.Collections.Generic.IEnumerable @@ -1094,7 +1095,8 @@ NGitLab.IRepositoryClient.Commits.get -> System.Collections.Generic.IEnumerable< NGitLab.IRepositoryClient.Compare(NGitLab.Models.CompareQuery query) -> NGitLab.Models.CompareResults NGitLab.IRepositoryClient.Contributors.get -> NGitLab.IContributorClient NGitLab.IRepositoryClient.Files.get -> NGitLab.IFilesClient -NGitLab.IRepositoryClient.GetArchive(System.Action parser, string sha = null, string format = null) -> void +NGitLab.IRepositoryClient.GetArchive(System.Action parser) -> void +NGitLab.IRepositoryClient.GetArchive(System.Action parser, NGitLab.Models.FileArchiveQuery fileArchiveQuery) -> void NGitLab.IRepositoryClient.GetCommit(NGitLab.Sha1 sha) -> NGitLab.Models.Commit NGitLab.IRepositoryClient.GetCommitDiff(NGitLab.Sha1 sha) -> System.Collections.Generic.IEnumerable NGitLab.IRepositoryClient.GetCommitRefs(NGitLab.Sha1 sha, NGitLab.Models.CommitRefType type = NGitLab.Models.CommitRefType.All) -> System.Collections.Generic.IEnumerable @@ -1776,6 +1778,24 @@ NGitLab.Models.EventTargetType.Note = 4 -> NGitLab.Models.EventTargetType NGitLab.Models.EventTargetType.Project = 5 -> NGitLab.Models.EventTargetType NGitLab.Models.EventTargetType.Snippet = 6 -> NGitLab.Models.EventTargetType NGitLab.Models.EventTargetType.User = 7 -> NGitLab.Models.EventTargetType +NGitLab.Models.FileArchiveFormat +NGitLab.Models.FileArchiveFormat.Bz2 = 0 -> NGitLab.Models.FileArchiveFormat +NGitLab.Models.FileArchiveFormat.Gz = 1 -> NGitLab.Models.FileArchiveFormat +NGitLab.Models.FileArchiveFormat.Tar = 2 -> NGitLab.Models.FileArchiveFormat +NGitLab.Models.FileArchiveFormat.TarBz2 = 3 -> NGitLab.Models.FileArchiveFormat +NGitLab.Models.FileArchiveFormat.TarGz = 4 -> NGitLab.Models.FileArchiveFormat +NGitLab.Models.FileArchiveFormat.Tb2 = 5 -> NGitLab.Models.FileArchiveFormat +NGitLab.Models.FileArchiveFormat.Tbz = 6 -> NGitLab.Models.FileArchiveFormat +NGitLab.Models.FileArchiveFormat.Tbz2 = 7 -> NGitLab.Models.FileArchiveFormat +NGitLab.Models.FileArchiveFormat.Zip = 8 -> NGitLab.Models.FileArchiveFormat +NGitLab.Models.FileArchiveQuery +NGitLab.Models.FileArchiveQuery.FileArchiveQuery() -> void +NGitLab.Models.FileArchiveQuery.Format.get -> NGitLab.Models.FileArchiveFormat? +NGitLab.Models.FileArchiveQuery.Format.set -> void +NGitLab.Models.FileArchiveQuery.Path.get -> string +NGitLab.Models.FileArchiveQuery.Path.set -> void +NGitLab.Models.FileArchiveQuery.Ref.get -> string +NGitLab.Models.FileArchiveQuery.Ref.set -> void NGitLab.Models.FileData NGitLab.Models.FileData.BlobId -> string NGitLab.Models.FileData.CommitId -> string From 359668b08776bf05fe79692f21166e4e8e29a603 Mon Sep 17 00:00:00 2001 From: Steve Ryan Date: Mon, 15 Apr 2024 12:14:01 +1000 Subject: [PATCH 3/3] Adds Utils.AppendSegmentToUrl for IRepositoryClient.GetArchive overload --- NGitLab.Tests/Impl/UtilsTests.cs | 114 ++++++++++++++++++ .../RepositoryClient/RepositoryClientTests.cs | 19 +-- NGitLab/Extensions/TypeExtensions.cs | 20 --- NGitLab/Impl/RepositoryClient.cs | 8 +- NGitLab/Impl/Utils.cs | 64 +++++++--- 5 files changed, 177 insertions(+), 48 deletions(-) delete mode 100644 NGitLab/Extensions/TypeExtensions.cs diff --git a/NGitLab.Tests/Impl/UtilsTests.cs b/NGitLab.Tests/Impl/UtilsTests.cs index f4df615f..f7b232fa 100644 --- a/NGitLab.Tests/Impl/UtilsTests.cs +++ b/NGitLab.Tests/Impl/UtilsTests.cs @@ -15,4 +15,118 @@ public void AddParameter_ConsidersEnumMemberAttribute(EventAction value, string Assert.That(url, Is.EqualTo($"{basePath}?event_action={expectedQueryParamValue}")); } + + [TestCase] + public void AppendSegmentToUrl_ValueIsNullIncludeSegmentSeparatorFalse_ReturnsUrlWithoutAnyChange() + { + // Arrange + const string basePath = "https://gitlab.org/api/v4/stuff"; + var url = basePath; + var expected = basePath; + + // Act + var actual = NGitLab.Impl.Utils.AppendSegmentToUrl(url, value: null, includeSegmentSeparator: false); + + // Assert + Assert.That(expected, Is.EqualTo(actual)); + } + + [TestCase] + public void AppendSegmentToUrl_ValueIsNullIncludeSegmentSeparatorTrue_ReturnsUrlWithoutAnyChange() + { + // Arrange + const string basePath = "https://gitlab.org/api/v4/stuff"; + var url = basePath; + var expected = basePath; + + // Act + var actual = NGitLab.Impl.Utils.AppendSegmentToUrl(url, value: null, includeSegmentSeparator: true); + + // Assert + Assert.That(expected, Is.EqualTo(actual)); + } + + [TestCase] + public void AppendSegmentToUrl_UrlAlreadyContainsQueryString_ThrowsInvalidOperationException() + { + // Arrange + const string basePath = "https://gitlab.org/api/v4/stuff"; + var url = basePath; + + url = NGitLab.Impl.Utils.AddParameter(url, "param1", "one"); + + // Act and Assert + Assert.That(() => NGitLab.Impl.Utils.AppendSegmentToUrl(url, "segment"), Throws.InvalidOperationException); + } + + [TestCase("https://gitlab.org/api/v4/stuff/", "/segment")] + [TestCase("https://gitlab.org/api/v4/stuff/", "segment")] + [TestCase("https://gitlab.org/api/v4/stuff", "/segment")] + [TestCase("https://gitlab.org/api/v4/stuff", "segment")] + public void AppendSegmentToUrl_IncludeSegmentSeparatorIsTrue_SegmentAppendedWithSeparator(string basePath, string segment) + { + // Arrange + var url = basePath; + var expected = "https://gitlab.org/api/v4/stuff/segment"; + + // Act + var actual = NGitLab.Impl.Utils.AppendSegmentToUrl(url, value: segment, includeSegmentSeparator: true); + + // Assert + Assert.That(expected, Is.EqualTo(actual)); + } + + [TestCase("https://gitlab.org/api/v4/stuff/", "/segment")] + [TestCase("https://gitlab.org/api/v4/stuff/", "segment")] + [TestCase("https://gitlab.org/api/v4/stuff", "/segment")] + [TestCase("https://gitlab.org/api/v4/stuff", "segment")] + public void AppendSegmentToUrl_IncludeSegmentSeparatorIsFalse_SegmentAppendedWithoutSeparator(string basePath, string segment) + { + // Arrange + var url = basePath; + var expected = "https://gitlab.org/api/v4/stuffsegment"; + + // Act + var actual = NGitLab.Impl.Utils.AppendSegmentToUrl(url, value: segment, includeSegmentSeparator: false); + + // Assert + Assert.That(expected, Is.EqualTo(actual)); + } + + [TestCase("https://gitlab.org/api/v4/stuff.bz2", FileArchiveFormat.Bz2)] + [TestCase("https://gitlab.org/api/v4/stuff.gz", FileArchiveFormat.Gz)] + [TestCase("https://gitlab.org/api/v4/stuff.tar", FileArchiveFormat.Tar)] + [TestCase("https://gitlab.org/api/v4/stuff.tar.bz2", FileArchiveFormat.TarBz2)] + [TestCase("https://gitlab.org/api/v4/stuff.tar.gz", FileArchiveFormat.TarGz)] + [TestCase("https://gitlab.org/api/v4/stuff.tb2", FileArchiveFormat.Tb2)] + [TestCase("https://gitlab.org/api/v4/stuff.tbz", FileArchiveFormat.Tbz)] + [TestCase("https://gitlab.org/api/v4/stuff.tbz2", FileArchiveFormat.Tbz2)] + [TestCase("https://gitlab.org/api/v4/stuff.zip", FileArchiveFormat.Zip)] + public void AppendSegmentToUrl_ValueIsEnumWithEnumMemberAttribute_EnumMemberValueAppended(string expected, FileArchiveFormat fileArchiveFormat) + { + // Arrange + const string basePath = "https://gitlab.org/api/v4/stuff"; + var url = basePath; + + // Act + var actual = NGitLab.Impl.Utils.AppendSegmentToUrl(url, value: fileArchiveFormat, includeSegmentSeparator: false); + + // Assert + Assert.That(expected, Is.EqualTo(actual)); + } + + [TestCase("https://gitlab.org/api/v4/stuff/Group", BadgeKind.Group)] + [TestCase("https://gitlab.org/api/v4/stuff/Project", BadgeKind.Project)] + public void AppendSegmentToUrl_ValueIsEnumWithoutEnumMemberAttribute_EnumToStringValueAppended(string expected, BadgeKind badgeKind) + { + // Arrange + const string basePath = "https://gitlab.org/api/v4/stuff"; + var url = basePath; + + // Act + var actual = NGitLab.Impl.Utils.AppendSegmentToUrl(url, value: badgeKind, includeSegmentSeparator: true); + + // Assert + Assert.That(expected, Is.EqualTo(actual)); + } } diff --git a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs index bfe9403f..bcca5d42 100644 --- a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs +++ b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs @@ -356,7 +356,7 @@ public async Task GetCommitRefs(CommitRefType type) [Test] [NGitLabRetry] - public async Task GetArchive() + public async Task GetArchive_NoQuerySpecified_PathConstructedWithNoParameters() { // Arrange using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2).ConfigureAwait(false); @@ -376,7 +376,7 @@ public async Task GetArchive() [Test] [NGitLabRetry] - public async Task GetArchiveWithNullQueryPassesNoParameters() + public async Task GetArchive_QueryInstanceIsNull_PathConstructedWithNoParameters() { // Arrange using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2).ConfigureAwait(false); @@ -405,7 +405,8 @@ public async Task GetArchiveWithNullQueryPassesNoParameters() [TestCase(FileArchiveFormat.Tbz2, ".tbz2")] [TestCase(FileArchiveFormat.Zip, ".zip")] [NGitLabRetry] - public async Task GetArchiveFormatValuePassedCorrectly(FileArchiveFormat? archiveFormat, string expectedExtension) + public async Task GetArchive_QuerySpecifiesFormatValue_ArchiveExtensionPassedCorrectly( + FileArchiveFormat? archiveFormat, string expectedExtension) { // Arrange using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); @@ -423,13 +424,14 @@ public async Task GetArchiveFormatValuePassedCorrectly(FileArchiveFormat? archiv Assert.Multiple(() => { Assert.That(requestPathAndQuery, Is.Not.Null); - Assert.That(requestPathAndQuery.EndsWith($"/archive{expectedExtension}", StringComparison.OrdinalIgnoreCase), Is.True); + Assert.That(requestPathAndQuery.EndsWith($"/archive{expectedExtension}", + StringComparison.OrdinalIgnoreCase), Is.True); }); } [Test] [NGitLabRetry] - public async Task GetArchiveShaValuePassedCorrectly() + public async Task GetArchive_QuerySpecifiesRevision_ShaValuePassedCorrectly() { // Arrange using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); @@ -448,13 +450,14 @@ public async Task GetArchiveShaValuePassedCorrectly() Assert.Multiple(() => { Assert.That(requestPathAndQuery, Is.Not.Null); - Assert.That(requestPathAndQuery.Contains($"sha={firstCommitId}", StringComparison.OrdinalIgnoreCase), Is.True); + Assert.That(requestPathAndQuery.Contains($"sha={firstCommitId}", + StringComparison.OrdinalIgnoreCase), Is.True); }); } [Test] [NGitLabRetry] - public async Task GetArchivePathValuePassedCorrectly() + public async Task GetArchive_QuerySpecifiesPath_PathValuePassedCorrectly() { // Arrange using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); @@ -479,7 +482,7 @@ public async Task GetArchivePathValuePassedCorrectly() [Test] [NGitLabRetry] - public async Task GetArchiveCombinationOfValuesPassedCorrectly() + public async Task GetArchive_QuerySpecifiesAllParameters_AllParametersPassedCorrectly() { // Arrange using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); diff --git a/NGitLab/Extensions/TypeExtensions.cs b/NGitLab/Extensions/TypeExtensions.cs deleted file mode 100644 index 54d7d924..00000000 --- a/NGitLab/Extensions/TypeExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Runtime.Serialization; - -namespace NGitLab.Extensions; - -internal static class TypeExtensions -{ - public static string GetEnumMemberAttributeValue(this TEnum value) - where TEnum : Enum - { - return typeof(TEnum) - .GetTypeInfo() - .DeclaredMembers - .SingleOrDefault(x => string.Equals(x.Name, value.ToString(), StringComparison.Ordinal)) - ?.GetCustomAttribute(inherit: false) - ?.Value; - } -} diff --git a/NGitLab/Impl/RepositoryClient.cs b/NGitLab/Impl/RepositoryClient.cs index 1c228cbb..3a2854f8 100644 --- a/NGitLab/Impl/RepositoryClient.cs +++ b/NGitLab/Impl/RepositoryClient.cs @@ -59,18 +59,14 @@ public void GetRawBlob(string sha, Action parser) public void GetArchive(Action parser, FileArchiveQuery fileArchiveQuery) { - var url = $"{_repoPath}/archive"; + var url = Utils.AppendSegmentToUrl(_repoPath, "/archive"); if (fileArchiveQuery != null) { // If a particular archive file format is requested, it is appended to the path directly as follows: // /project/123/repository/archive.zip // /project/123/repository/archive.tar - if (fileArchiveQuery.Format.HasValue) - { - url += fileArchiveQuery.Format.Value.GetEnumMemberAttributeValue(); - } - + url = Utils.AppendSegmentToUrl(url, fileArchiveQuery.Format, includeSegmentSeparator: false); url = Utils.AddParameter(url, "path", fileArchiveQuery.Path); url = Utils.AddParameter(url, "sha", fileArchiveQuery.Ref); } diff --git a/NGitLab/Impl/Utils.cs b/NGitLab/Impl/Utils.cs index fd1180ed..7c217cba 100644 --- a/NGitLab/Impl/Utils.cs +++ b/NGitLab/Impl/Utils.cs @@ -8,26 +8,18 @@ namespace NGitLab.Impl; internal static class Utils { + private const char UrlSegmentSeparatorChar = '/'; + public static string AddParameter(string url, string parameterName, T value) { if (value is null) return url; var valueString = value.ToString(); - var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - if (type.IsEnum) - { - var enumField = type.GetFields().FirstOrDefault(f => string.Equals(f.Name, valueString, StringComparison.Ordinal)); - if (enumField is not null) - { - var enumMemberValue = enumField.GetCustomAttributes(typeof(EnumMemberAttribute), inherit: true) - .Cast() - .FirstOrDefault()? - .Value; - if (enumMemberValue is not null) - return AddParameterInternal(url, parameterName, enumMemberValue); - } - } + var enumMemberValue = GetEnumMemberValue(valueString); + + if (enumMemberValue is not null) + return AddParameterInternal(url, parameterName, enumMemberValue); return AddParameterInternal(url, parameterName, valueString); } @@ -96,6 +88,31 @@ public static string AddPageParams(string url, int? page, int? perPage) return url; } + public static string AppendSegmentToUrl(string url, T value, bool includeSegmentSeparator = true) + { + if (value is null) + return url; + + // Don't allow segments to a url which already has parameters present + if (url.Contains('?')) + throw new InvalidOperationException("Cannot append segment to url which already has parameters present"); + + var valueString = value.ToString(); + var enumMemberValue = GetEnumMemberValue(valueString); + + if (enumMemberValue is not null) + valueString = enumMemberValue; + + url = url.TrimEnd(UrlSegmentSeparatorChar); + valueString = valueString.TrimStart(UrlSegmentSeparatorChar); + valueString = WebUtility.UrlEncode(valueString); + + if (includeSegmentSeparator) + return $"{url}{UrlSegmentSeparatorChar}{valueString}"; + + return $"{url}{valueString}"; + } + private static string AddParameterInternal(string url, string parameterName, string stringValue) { var @operator = !url.Contains("?") ? "?" : "&"; @@ -103,4 +120,23 @@ private static string AddParameterInternal(string url, string parameterName, str var parameter = $"{@operator}{parameterName}={formattedValue}"; return url + parameter; } + + private static string GetEnumMemberValue(string valueString) + { + string enumMemberValue = null; + var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + if (type.IsEnum) + { + var enumField = type.GetFields().FirstOrDefault(f => string.Equals(f.Name, valueString, StringComparison.Ordinal)); + if (enumField is not null) + { + enumMemberValue = enumField.GetCustomAttributes(typeof(EnumMemberAttribute), inherit: true) + .Cast() + .FirstOrDefault()? + .Value; + } + } + + return enumMemberValue; + } }