From 93889b46345b39fe9c8a3aff27b5ba2e1a8d46f4 Mon Sep 17 00:00:00 2001 From: Steve Ryan Date: Fri, 12 Apr 2024 12:44:46 +1000 Subject: [PATCH] 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 fca73d2ea..66faae3b5 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 b9ec4f3e3..bfe9403ff 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 000000000..54d7d9249 --- /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 d0dec67ea..a57a05128 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 1ec785d44..1c228cbba 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 000000000..634ce24ae --- /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 000000000..c6c87ea9f --- /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 4e464fcb0..49d6d1a1a 100644 --- a/NGitLab/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI.Unshipped.txt @@ -869,7 +869,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 @@ -1086,7 +1087,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 @@ -1768,6 +1770,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