From 270356b5b47c6eeb70ade5d12747974a6e78e5aa Mon Sep 17 00:00:00 2001 From: Jozef Izso Date: Fri, 19 Jan 2018 10:43:46 +0100 Subject: [PATCH] Fixes #1586 - Repository license API (#1630) * Implement GetLicenseContents() method for getting repository's license info * Request License Preview API for calls that return Repository object. * Add missing accept headers to observable methods for ObservableRepositoriesClients * fix impacted unit tests --- .../Clients/IObservableRepositoriesClient.cs | 21 ++++++ .../Clients/ObservableRepositoriesClient.cs | 46 ++++++++++--- .../Clients/RepositoriesClientTests.cs | 69 +++++++++++++++++++ .../Clients/RepositoriesClientTests.cs | 61 +++++++++++++--- Octokit.Tests/Helpers/Arg.cs | 5 ++ Octokit.Tests/Models/LicenseMetadataTests.cs | 32 +++++++++ .../Models/RepositoryContentLicenseTests.cs | 51 ++++++++++++++ .../ObservableRepositoriesClientTests.cs | 40 +++++------ Octokit/Clients/IRepositoriesClient.cs | 21 ++++++ Octokit/Clients/RepositoriesClient.cs | 51 +++++++++++--- Octokit/Helpers/AcceptHeaders.cs | 19 +++++ Octokit/Helpers/ApiUrls.cs | 21 ++++++ Octokit/Models/Response/License.cs | 9 +-- Octokit/Models/Response/LicenseMetadata.cs | 15 +++- Octokit/Models/Response/Repository.cs | 5 +- .../Response/RepositoryContentLicense.cs | 34 +++++++++ 16 files changed, 443 insertions(+), 57 deletions(-) create mode 100644 Octokit.Tests/Models/LicenseMetadataTests.cs create mode 100644 Octokit.Tests/Models/RepositoryContentLicenseTests.cs create mode 100644 Octokit/Models/Response/RepositoryContentLicense.cs diff --git a/Octokit.Reactive/Clients/IObservableRepositoriesClient.cs b/Octokit.Reactive/Clients/IObservableRepositoriesClient.cs index dae1ec2690..bbe9ca16b2 100644 --- a/Octokit.Reactive/Clients/IObservableRepositoriesClient.cs +++ b/Octokit.Reactive/Clients/IObservableRepositoriesClient.cs @@ -422,6 +422,27 @@ public interface IObservableRepositoriesClient /// All of the repositories tags. IObservable GetAllTags(long repositoryId, ApiOptions options); + /// + /// Get the contents of a repository's license + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// Returns the contents of the repository's license file, if one is detected. + IObservable GetLicenseContents(string owner, string name); + + /// + /// Get the contents of a repository's license + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// Returns the contents of the repository's license file, if one is detected. + IObservable GetLicenseContents(long repositoryId); + /// /// Updates the specified repository with the values given in /// diff --git a/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs b/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs index aea66d3b8a..137dd466f4 100644 --- a/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs +++ b/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs @@ -129,7 +129,7 @@ public IObservable Get(long repositoryId) /// A of . public IObservable GetAllPublic() { - return _connection.GetAndFlattenAllPages(ApiUrls.AllPublicRepositories()); + return _connection.GetAndFlattenAllPages(ApiUrls.AllPublicRepositories(), null, AcceptHeaders.LicensesApiPreview); } /// @@ -146,7 +146,7 @@ public IObservable GetAllPublic(PublicRepositoryRequest request) var url = ApiUrls.AllPublicRepositories(request.Since); - return _connection.GetAndFlattenAllPages(url); + return _connection.GetAndFlattenAllPages(url, null, AcceptHeaders.LicensesApiPreview); } /// @@ -172,7 +172,7 @@ public IObservable GetAllForCurrent(ApiOptions options) { Ensure.ArgumentNotNull(options, "options"); - return _connection.GetAndFlattenAllPages(ApiUrls.Repositories(), options); + return _connection.GetAndFlattenAllPages(ApiUrls.Repositories(), null, AcceptHeaders.LicensesApiPreview, options); } /// @@ -203,7 +203,7 @@ public IObservable GetAllForCurrent(RepositoryRequest request, ApiOp Ensure.ArgumentNotNull(request, "request"); Ensure.ArgumentNotNull(options, "options"); - return _connection.GetAndFlattenAllPages(ApiUrls.Repositories(), request.ToParametersDictionary()); + return _connection.GetAndFlattenAllPages(ApiUrls.Repositories(), request.ToParametersDictionary(), AcceptHeaders.LicensesApiPreview); } /// @@ -229,7 +229,7 @@ public IObservable GetAllForUser(string login, ApiOptions options) Ensure.ArgumentNotNullOrEmptyString(login, "login"); Ensure.ArgumentNotNull(options, "options"); - return _connection.GetAndFlattenAllPages(ApiUrls.Repositories(login), options); + return _connection.GetAndFlattenAllPages(ApiUrls.Repositories(login), null, AcceptHeaders.LicensesApiPreview, options); } /// @@ -257,7 +257,7 @@ public IObservable GetAllForOrg(string organization, ApiOptions opti Ensure.ArgumentNotNullOrEmptyString(organization, "organization"); Ensure.ArgumentNotNull(options, "options"); - return _connection.GetAndFlattenAllPages(ApiUrls.OrganizationRepositories(organization), options); + return _connection.GetAndFlattenAllPages(ApiUrls.OrganizationRepositories(organization), null, AcceptHeaders.LicensesApiPreview, options); } /// @@ -265,7 +265,7 @@ public IObservable GetAllForOrg(string organization, ApiOptions opti /// /// /// See the Commit Status API documentation for more - /// details. Also check out the blog post + /// details. Also check out the blog post /// that announced this feature. /// public IObservableCommitStatusClient Status { get; private set; } @@ -303,7 +303,7 @@ public IObservable GetAllForOrg(string organization, ApiOptions opti /// /// A client for GitHub's Repository Forks API. /// - /// See Forks API documentation for more information. + /// See Forks API documentation for more information. public IObservableRepositoryForksClient Forks { get; private set; } /// @@ -649,6 +649,36 @@ public IObservable Edit(string owner, string name, RepositoryUpdate return _client.Edit(owner, name, update).ToObservable(); } + /// + /// Get the contents of a repository's license + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// Returns the contents of the repository's license file, if one is detected. + public IObservable GetLicenseContents(string owner, string name) + { + Ensure.ArgumentNotNullOrEmptyString(owner, nameof(owner)); + Ensure.ArgumentNotNullOrEmptyString(name, nameof(name)); + + return _client.GetLicenseContents(owner, name).ToObservable(); + } + + /// + /// Get the contents of a repository's license + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// Returns the contents of the repository's license file, if one is detected. + public IObservable GetLicenseContents(long repositoryId) + { + return _client.GetLicenseContents(repositoryId).ToObservable(); + } + /// /// Updates the specified repository with the values given in /// diff --git a/Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs b/Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs index 179d702242..baa6c39c4e 100644 --- a/Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs +++ b/Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs @@ -35,6 +35,7 @@ public async Task CreatesANewPublicRepository() Assert.True(repository.HasWiki); Assert.Null(repository.Homepage); Assert.NotNull(repository.DefaultBranch); + Assert.Null(repository.License); } } @@ -189,6 +190,35 @@ public async Task CreatesARepositoryWithAGitignoreTemplate() } } + [IntegrationTest] + public async Task CreatesARepositoryWithALicenseTemplate() + { + var github = Helper.GetAuthenticatedClient(); + var repoName = Helper.MakeNameWithTimestamp("repo-with-license"); + + var newRepository = new NewRepository(repoName) + { + AutoInit = true, + LicenseTemplate = "mit" + }; + + using (var context = await github.CreateRepositoryContext(newRepository)) + { + var createdRepository = context.Repository; + + // NOTE: the License attribute is empty for newly created repositories + Assert.Null(createdRepository.License); + + // license information is not immediatelly available after the repository is created + await Task.Delay(TimeSpan.FromSeconds(1)); + + // check for actual license by reloading repository info + var repository = await github.Repository.Get(Helper.UserName, repoName); + Assert.NotNull(repository.License); + Assert.Equal("mit", repository.License.Key); + } + } + [IntegrationTest] public async Task ThrowsInvalidGitIgnoreExceptionForInvalidTemplateNames() @@ -746,6 +776,18 @@ public async Task ReturnsRepositoryMergeOptionsWithRepositoryId() Assert.NotNull(repository.AllowMergeCommit); } } + + [IntegrationTest] + public async Task ReturnsSpecifiedRepositoryWithLicenseInformation() + { + var github = Helper.GetAuthenticatedClient(); + + var repository = await github.Repository.Get("github", "choosealicense.com"); + + Assert.NotNull(repository.License); + Assert.Equal("mit", repository.License.Key); + Assert.Equal("MIT License", repository.License.Name); + } } public class TheGetAllPublicMethod @@ -1612,4 +1654,31 @@ public async Task GetsPagesOfBranchesWithRepositoryId() Assert.NotEqual(firstPage[4].Name, secondPage[4].Name); } } + + public class TheGetLicenseContentsMethod + { + [IntegrationTest] + public async Task ReturnsLicenseContent() + { + var github = Helper.GetAuthenticatedClient(); + + var license = await github.Repository.GetLicenseContents("octokit", "octokit.net"); + Assert.Equal("LICENSE.txt", license.Name); + Assert.NotNull(license.License); + Assert.Equal("mit", license.License.Key); + Assert.Equal("MIT License", license.License.Name); + } + + [IntegrationTest] + public async Task ReturnsLicenseContentWithRepositoryId() + { + var github = Helper.GetAuthenticatedClient(); + + var license = await github.Repository.GetLicenseContents(7528679); + Assert.Equal("LICENSE.txt", license.Name); + Assert.NotNull(license.License); + Assert.Equal("mit", license.License.Key); + Assert.Equal("MIT License", license.License.Name); + } + } } diff --git a/Octokit.Tests/Clients/RepositoriesClientTests.cs b/Octokit.Tests/Clients/RepositoriesClientTests.cs index 2aea61a8a2..20a1de3254 100644 --- a/Octokit.Tests/Clients/RepositoriesClientTests.cs +++ b/Octokit.Tests/Clients/RepositoriesClientTests.cs @@ -261,7 +261,7 @@ public async Task RequestsCorrectUrl() connection.Received().Get( Arg.Is(u => u.ToString() == "repos/owner/name"), null, - "application/vnd.github.polaris-preview+json"); + "application/vnd.github.polaris-preview+json,application/vnd.github.drax-preview+json"); } [Fact] @@ -275,7 +275,7 @@ public async Task RequestsCorrectUrlWithRepositoryId() connection.Received().Get( Arg.Is(u => u.ToString() == "repositories/1"), null, - "application/vnd.github.polaris-preview+json"); + "application/vnd.github.polaris-preview+json,application/vnd.github.drax-preview+json"); } [Fact] @@ -302,7 +302,7 @@ public async Task RequestsTheCorrectUrlAndReturnsRepositories() await client.GetAllPublic(); connection.Received() - .GetAll(Arg.Is(u => u.ToString() == "repositories")); + .GetAll(Arg.Is(u => u.ToString() == "repositories"), null, "application/vnd.github.drax-preview+json"); } } @@ -317,7 +317,7 @@ public async Task RequestsTheCorrectUrl() await client.GetAllPublic(new PublicRepositoryRequest(364L)); connection.Received() - .GetAll(Arg.Is(u => u.ToString() == "repositories?since=364")); + .GetAll(Arg.Is(u => u.ToString() == "repositories?since=364"), null, "application/vnd.github.drax-preview+json"); } [Fact] @@ -329,7 +329,7 @@ public async Task SendsTheCorrectParameter() await client.GetAllPublic(new PublicRepositoryRequest(364L)); connection.Received() - .GetAll(Arg.Is(u => u.ToString() == "repositories?since=364")); + .GetAll(Arg.Is(u => u.ToString() == "repositories?since=364"), null, "application/vnd.github.drax-preview+json"); } } @@ -344,7 +344,7 @@ public async Task RequestsTheCorrectUrlAndReturnsRepositories() await client.GetAllForCurrent(); connection.Received() - .GetAll(Arg.Is(u => u.ToString() == "user/repos"), Args.ApiOptions); + .GetAll(Arg.Is(u => u.ToString() == "user/repos"), null, "application/vnd.github.drax-preview+json", Args.ApiOptions); } [Fact] @@ -364,6 +364,7 @@ public async Task CanFilterByType() .GetAll( Arg.Is(u => u.ToString() == "user/repos"), Arg.Is>(d => d["type"] == "all"), + "application/vnd.github.drax-preview+json", Args.ApiOptions); } @@ -386,6 +387,7 @@ public async Task CanFilterBySort() Arg.Is(u => u.ToString() == "user/repos"), Arg.Is>(d => d["type"] == "private" && d["sort"] == "full_name"), + "application/vnd.github.drax-preview+json", Args.ApiOptions); } @@ -409,6 +411,7 @@ public async Task CanFilterBySortDirection() Arg.Is(u => u.ToString() == "user/repos"), Arg.Is>(d => d["type"] == "member" && d["sort"] == "updated" && d["direction"] == "asc"), + "application/vnd.github.drax-preview+json", Args.ApiOptions); } @@ -430,6 +433,7 @@ public async Task CanFilterByVisibility() Arg.Is(u => u.ToString() == "user/repos"), Arg.Is>(d => d["visibility"] == "private"), + "application/vnd.github.drax-preview+json", Args.ApiOptions); } @@ -452,6 +456,7 @@ public async Task CanFilterByAffiliation() Arg.Is(u => u.ToString() == "user/repos"), Arg.Is>(d => d["affiliation"] == "owner" && d["sort"] == "full_name"), + "application/vnd.github.drax-preview+json", Args.ApiOptions); } } @@ -467,7 +472,7 @@ public async Task RequestsTheCorrectUrlAndReturnsRepositories() await client.GetAllForUser("username"); connection.Received() - .GetAll(Arg.Is(u => u.ToString() == "users/username/repos"), Args.ApiOptions); + .GetAll(Arg.Is(u => u.ToString() == "users/username/repos"), null, "application/vnd.github.drax-preview+json", Args.ApiOptions); } [Fact] @@ -496,7 +501,7 @@ public async Task RequestsTheCorrectUrl() await client.GetAllForOrg("orgname"); connection.Received() - .GetAll(Arg.Is(u => u.ToString() == "orgs/orgname/repos"), Args.ApiOptions); + .GetAll(Arg.Is(u => u.ToString() == "orgs/orgname/repos"), null, "application/vnd.github.drax-preview+json", Args.ApiOptions); } [Fact] @@ -796,6 +801,42 @@ public async Task EnsuresNonNullArguments() } } + public class TheGetLicenseContentsMethod + { + [Fact] + public async Task RequestsTheCorrectUrl() + { + var connection = Substitute.For(); + var client = new RepositoriesClient(connection); + + await client.GetLicenseContents("owner", "name"); + + connection.Received() + .Get(Arg.Is(u => u.ToString() == "repos/owner/name/license"), null, "application/vnd.github.drax-preview+json"); + } + + [Fact] + public async Task RequestsTheCorrectUrlWithRepositoryId() + { + var connection = Substitute.For(); + var client = new RepositoriesClient(connection); + + await client.GetLicenseContents(1); + + connection.Received() + .Get(Arg.Is(u => u.ToString() == "repositories/1/license"), null, "application/vnd.github.drax-preview+json"); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new RepositoriesClient(Substitute.For()); + + await Assert.ThrowsAsync(() => client.GetLicenseContents(null, "repo")); + await Assert.ThrowsAsync(() => client.GetLicenseContents("owner", null)); + } + } + public class TheGetAllTagsMethod { [Fact] @@ -892,7 +933,7 @@ public void PatchesCorrectUrl() client.Edit("owner", "repo", update); connection.Received() - .Patch(Arg.Is(u => u.ToString() == "repos/owner/repo"), Arg.Any(), "application/vnd.github.polaris-preview+json"); + .Patch(Arg.Is(u => u.ToString() == "repos/owner/repo"), Arg.Any(), "application/vnd.github.polaris-preview+json,application/vnd.github.drax-preview+json"); } [Fact] @@ -905,7 +946,7 @@ public void PatchesCorrectUrlWithRepositoryId() client.Edit(1, update); connection.Received() - .Patch(Arg.Is(u => u.ToString() == "repositories/1"), Arg.Any(), "application/vnd.github.polaris-preview+json"); + .Patch(Arg.Is(u => u.ToString() == "repositories/1"), Arg.Any(), "application/vnd.github.polaris-preview+json,application/vnd.github.drax-preview+json"); } [Fact] diff --git a/Octokit.Tests/Helpers/Arg.cs b/Octokit.Tests/Helpers/Arg.cs index 5408f72ded..aedaa24e87 100644 --- a/Octokit.Tests/Helpers/Arg.cs +++ b/Octokit.Tests/Helpers/Arg.cs @@ -72,5 +72,10 @@ public static ApiOptions ApiOptions { get { return Arg.Any(); } } + + public static string AnyAcceptHeaders + { + get { return Arg.Any(); } + } } } diff --git a/Octokit.Tests/Models/LicenseMetadataTests.cs b/Octokit.Tests/Models/LicenseMetadataTests.cs new file mode 100644 index 0000000000..9cf8d212a7 --- /dev/null +++ b/Octokit.Tests/Models/LicenseMetadataTests.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Octokit.Internal; +using Xunit; + +namespace Octokit.Tests.Models +{ + public class LicenseMetadataTests + { + [Fact] + public void CanBeDeserializedFromLicenseJson() + { + const string json = @"{ + ""key"": ""mit"", + ""name"": ""MIT License"", + ""spdx_id"": ""MIT"", + ""url"": ""https://api.github.com/licenses/mit"", + ""featured"": true +}"; + var serializer = new SimpleJsonSerializer(); + + var license = serializer.Deserialize(json); + + Assert.Equal("mit", license.Key); + Assert.Equal("MIT License", license.Name); + Assert.Equal("MIT", license.SpdxId); + Assert.Equal("https://api.github.com/licenses/mit", license.Url); + Assert.True(license.Featured); + } + } +} + diff --git a/Octokit.Tests/Models/RepositoryContentLicenseTests.cs b/Octokit.Tests/Models/RepositoryContentLicenseTests.cs new file mode 100644 index 0000000000..939279e767 --- /dev/null +++ b/Octokit.Tests/Models/RepositoryContentLicenseTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using Octokit.Internal; +using Xunit; + +namespace Octokit.Tests.Models +{ + public class RepositoryContentLicenseTests + { + [Fact] + public void CanBeDeserializedFromRepositoryContentLicenseJson() + { + const string json = @"{ + ""name"": ""LICENSE"", + ""path"": ""LICENSE"", + ""sha"": ""401c59dcc4570b954dd6d345e76199e1f4e76266"", + ""size"": 1077, + ""url"": ""https://api.github.com/repos/benbalter/gman/contents/LICENSE?ref=master"", + ""html_url"": ""https://github.com/benbalter/gman/blob/master/LICENSE"", + ""git_url"": ""https://api.github.com/repos/benbalter/gman/git/blobs/401c59dcc4570b954dd6d345e76199e1f4e76266"", + ""download_url"": ""https://raw.githubusercontent.com/benbalter/gman/master/LICENSE?lab=true"", + ""type"": ""file"", + ""content"": ""VGhlIE1JVCBMaWNlbnNlIChNSVQpCgpDb3B5cmlnaHQgKGMpIDIwMTMgQmVu\nIEJhbHRlcgoKUGVybWlzc2lvbiBpcyBoZXJlYnkgZ3JhbnRlZCwgZnJlZSBv\nZiBjaGFyZ2UsIHRvIGFueSBwZXJzb24gb2J0YWluaW5nIGEgY29weSBvZgp0\naGlzIHNvZnR3YXJlIGFuZCBhc3NvY2lhdGVkIGRvY3VtZW50YXRpb24gZmls\nZXMgKHRoZSAiU29mdHdhcmUiKSwgdG8gZGVhbCBpbgp0aGUgU29mdHdhcmUg\nd2l0aG91dCByZXN0cmljdGlvbiwgaW5jbHVkaW5nIHdpdGhvdXQgbGltaXRh\ndGlvbiB0aGUgcmlnaHRzIHRvCnVzZSwgY29weSwgbW9kaWZ5LCBtZXJnZSwg\ncHVibGlzaCwgZGlzdHJpYnV0ZSwgc3VibGljZW5zZSwgYW5kL29yIHNlbGwg\nY29waWVzIG9mCnRoZSBTb2Z0d2FyZSwgYW5kIHRvIHBlcm1pdCBwZXJzb25z\nIHRvIHdob20gdGhlIFNvZnR3YXJlIGlzIGZ1cm5pc2hlZCB0byBkbyBzbywK\nc3ViamVjdCB0byB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgpUaGUgYWJv\ndmUgY29weXJpZ2h0IG5vdGljZSBhbmQgdGhpcyBwZXJtaXNzaW9uIG5vdGlj\nZSBzaGFsbCBiZSBpbmNsdWRlZCBpbiBhbGwKY29waWVzIG9yIHN1YnN0YW50\naWFsIHBvcnRpb25zIG9mIHRoZSBTb2Z0d2FyZS4KClRIRSBTT0ZUV0FSRSBJ\nUyBQUk9WSURFRCAiQVMgSVMiLCBXSVRIT1VUIFdBUlJBTlRZIE9GIEFOWSBL\nSU5ELCBFWFBSRVNTIE9SCklNUExJRUQsIElOQ0xVRElORyBCVVQgTk9UIExJ\nTUlURUQgVE8gVEhFIFdBUlJBTlRJRVMgT0YgTUVSQ0hBTlRBQklMSVRZLCBG\nSVRORVNTCkZPUiBBIFBBUlRJQ1VMQVIgUFVSUE9TRSBBTkQgTk9OSU5GUklO\nR0VNRU5ULiBJTiBOTyBFVkVOVCBTSEFMTCBUSEUgQVVUSE9SUyBPUgpDT1BZ\nUklHSFQgSE9MREVSUyBCRSBMSUFCTEUgRk9SIEFOWSBDTEFJTSwgREFNQUdF\nUyBPUiBPVEhFUiBMSUFCSUxJVFksIFdIRVRIRVIKSU4gQU4gQUNUSU9OIE9G\nIENPTlRSQUNULCBUT1JUIE9SIE9USEVSV0lTRSwgQVJJU0lORyBGUk9NLCBP\nVVQgT0YgT1IgSU4KQ09OTkVDVElPTiBXSVRIIFRIRSBTT0ZUV0FSRSBPUiBU\nSEUgVVNFIE9SIE9USEVSIERFQUxJTkdTIElOIFRIRSBTT0ZUV0FSRS4K\n"", + ""encoding"": ""base64"", + ""_links"": { + ""self"": ""https://api.github.com/repos/benbalter/gman/contents/LICENSE?ref=master"", + ""git"": ""https://api.github.com/repos/benbalter/gman/git/blobs/401c59dcc4570b954dd6d345e76199e1f4e76266"", + ""html"": ""https://github.com/benbalter/gman/blob/master/LICENSE"" + }, + ""license"": { + ""key"": ""mit"", + ""name"": ""MIT License"", + ""spdx_id"": ""MIT"", + ""url"": ""https://api.github.com/licenses/mit"", + ""featured"": true + } +}"; + var serializer = new SimpleJsonSerializer(); + + var license = serializer.Deserialize(json); + var licenseMetadata = license.License; + + Assert.Equal("LICENSE", license.Name); + Assert.Equal("LICENSE", license.Path); + Assert.Equal("401c59dcc4570b954dd6d345e76199e1f4e76266", license.Sha); + Assert.NotNull(license.License); + Assert.Equal("mit", licenseMetadata.Key); + } + } +} + diff --git a/Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs b/Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs index b76429fac8..29f6b04d6e 100644 --- a/Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs +++ b/Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs @@ -91,18 +91,18 @@ public async Task IsALukeWarmObservable() var response = Task.Factory.StartNew>(() => new ApiResponse(new Response(), repository)); var connection = Substitute.For(); - connection.Get(Args.Uri, null, "application/vnd.github.polaris-preview+json").Returns(response); + connection.Get(Args.Uri, null, Args.AnyAcceptHeaders).Returns(response); var gitHubClient = new GitHubClient(connection); var client = new ObservableRepositoriesClient(gitHubClient); var observable = client.Get("stark", "ned"); - connection.Received(1).Get(Args.Uri, null, "application/vnd.github.polaris-preview+json"); + connection.Received(1).Get(Args.Uri, null, Args.AnyAcceptHeaders); var result = await observable; - connection.Received(1).Get(Args.Uri, null, "application/vnd.github.polaris-preview+json"); + connection.Received(1).Get(Args.Uri, null, Args.AnyAcceptHeaders); var result2 = await observable; // TODO: If we change this to a warm observable, we'll need to change this to Received(2) - connection.Received(1).Get(Args.Uri, null, "application/vnd.github.polaris-preview+json"); + connection.Received(1).Get(Args.Uri, null, Args.AnyAcceptHeaders); Assert.Same(repository, result); Assert.Same(repository, result2); @@ -117,18 +117,18 @@ public async Task IsALukeWarmObservableWithRepositoryId() var response = Task.Factory.StartNew>(() => new ApiResponse(new Response(), repository)); var connection = Substitute.For(); - connection.Get(Args.Uri, null, "application/vnd.github.polaris-preview+json").Returns(response); + connection.Get(Args.Uri, null, Args.AnyAcceptHeaders).Returns(response); var gitHubClient = new GitHubClient(connection); var client = new ObservableRepositoriesClient(gitHubClient); var observable = client.Get(1); - connection.Received(1).Get(Args.Uri, null, "application/vnd.github.polaris-preview+json"); + connection.Received(1).Get(Args.Uri, null, Args.AnyAcceptHeaders); var result = await observable; - connection.Received(1).Get(Args.Uri, null, "application/vnd.github.polaris-preview+json"); + connection.Received(1).Get(Args.Uri, null, Args.AnyAcceptHeaders); var result2 = await observable; // TODO: If we change this to a warm observable, we'll need to change this to Received(2) - connection.Received(1).Get(Args.Uri, null, "application/vnd.github.polaris-preview+json"); + connection.Received(1).Get(Args.Uri, null, Args.AnyAcceptHeaders); Assert.Same(repository, result); Assert.Same(repository, result2); @@ -182,20 +182,20 @@ public async Task ReturnsEveryPageOfRepositories() }); var gitHubClient = Substitute.For(); - gitHubClient.Connection.Get>(firstPageUrl, Arg.Any>(), null) + gitHubClient.Connection.Get>(firstPageUrl, Arg.Any>(), Args.AnyAcceptHeaders) .Returns(Task.Factory.StartNew>>(() => firstPageResponse)); - gitHubClient.Connection.Get>(secondPageUrl, Arg.Any>(), null) + gitHubClient.Connection.Get>(secondPageUrl, Arg.Any>(), Args.AnyAcceptHeaders) .Returns(Task.Factory.StartNew>>(() => secondPageResponse)); - gitHubClient.Connection.Get>(thirdPageUrl, Arg.Any>(), null) + gitHubClient.Connection.Get>(thirdPageUrl, Arg.Any>(), Args.AnyAcceptHeaders) .Returns(Task.Factory.StartNew>>(() => lastPageResponse)); var repositoriesClient = new ObservableRepositoriesClient(gitHubClient); var results = await repositoriesClient.GetAllForCurrent().ToArray(); Assert.Equal(7, results.Length); - gitHubClient.Connection.Received(1).Get>(firstPageUrl, Arg.Any>(), null); - gitHubClient.Connection.Received(1).Get>(secondPageUrl, Arg.Any>(), null); - gitHubClient.Connection.Received(1).Get>(thirdPageUrl, Arg.Any>(), null); + gitHubClient.Connection.Received(1).Get>(firstPageUrl, Arg.Any>(), "application/vnd.github.drax-preview+json"); + gitHubClient.Connection.Received(1).Get>(secondPageUrl, Arg.Any>(), "application/vnd.github.drax-preview+json"); + gitHubClient.Connection.Received(1).Get>(thirdPageUrl, Arg.Any>(), "application/vnd.github.drax-preview+json"); } [Fact(Skip = "See https://github.com/octokit/octokit.net/issues/1011 for issue to investigate this further")] @@ -302,11 +302,11 @@ public async Task ReturnsEveryPageOfRepositories() }); var gitHubClient = Substitute.For(); - gitHubClient.Connection.Get>(firstPageUrl, null, null) + gitHubClient.Connection.Get>(firstPageUrl, null, Args.AnyAcceptHeaders) .Returns(Task.FromResult(firstPageResponse)); - gitHubClient.Connection.Get>(secondPageUrl, null, null) + gitHubClient.Connection.Get>(secondPageUrl, null, Args.AnyAcceptHeaders) .Returns(Task.FromResult(secondPageResponse)); - gitHubClient.Connection.Get>(thirdPageUrl, null, null) + gitHubClient.Connection.Get>(thirdPageUrl, null, Args.AnyAcceptHeaders) .Returns(Task.FromResult(lastPageResponse)); var repositoriesClient = new ObservableRepositoriesClient(gitHubClient); @@ -314,9 +314,9 @@ public async Task ReturnsEveryPageOfRepositories() var results = await repositoriesClient.GetAllPublic(new PublicRepositoryRequest(364L)).ToArray(); Assert.Equal(7, results.Length); - gitHubClient.Connection.Received(1).Get>(firstPageUrl, null, null); - gitHubClient.Connection.Received(1).Get>(secondPageUrl, null, null); - gitHubClient.Connection.Received(1).Get>(thirdPageUrl, null, null); + gitHubClient.Connection.Received(1).Get>(firstPageUrl, null, "application/vnd.github.drax-preview+json"); + gitHubClient.Connection.Received(1).Get>(secondPageUrl, null, "application/vnd.github.drax-preview+json"); + gitHubClient.Connection.Received(1).Get>(thirdPageUrl, null, "application/vnd.github.drax-preview+json"); } } diff --git a/Octokit/Clients/IRepositoriesClient.cs b/Octokit/Clients/IRepositoriesClient.cs index bab7c37ef9..f77f0bfb19 100644 --- a/Octokit/Clients/IRepositoriesClient.cs +++ b/Octokit/Clients/IRepositoriesClient.cs @@ -527,6 +527,27 @@ public interface IRepositoriesClient /// All of the repositories tags. Task> GetAllTags(long repositoryId, ApiOptions options); + /// + /// Get the contents of a repository's license + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// Returns the contents of the repository's license file, if one is detected. + Task GetLicenseContents(string owner, string name); + + /// + /// Get the contents of a repository's license + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// Returns the contents of the repository's license file, if one is detected. + Task GetLicenseContents(long repositoryId); + /// /// Updates the specified repository with the values given in /// diff --git a/Octokit/Clients/RepositoriesClient.cs b/Octokit/Clients/RepositoriesClient.cs index e7a9b044f4..70070c624a 100644 --- a/Octokit/Clients/RepositoriesClient.cs +++ b/Octokit/Clients/RepositoriesClient.cs @@ -177,7 +177,7 @@ public Task Edit(string owner, string name, RepositoryUpdate update) Ensure.ArgumentNotNull(update, "update"); Ensure.ArgumentNotNull(update.Name, "update.Name"); - return ApiConnection.Patch(ApiUrls.Repository(owner, name), update, AcceptHeaders.SquashCommitPreview); + return ApiConnection.Patch(ApiUrls.Repository(owner, name), update, AcceptHeaders.Concat(AcceptHeaders.SquashCommitPreview, AcceptHeaders.LicensesApiPreview)); } /// @@ -190,7 +190,7 @@ public Task Edit(long repositoryId, RepositoryUpdate update) { Ensure.ArgumentNotNull(update, "update"); - return ApiConnection.Patch(ApiUrls.Repository(repositoryId), update, AcceptHeaders.SquashCommitPreview); + return ApiConnection.Patch(ApiUrls.Repository(repositoryId), update, AcceptHeaders.Concat(AcceptHeaders.SquashCommitPreview, AcceptHeaders.LicensesApiPreview)); } /// @@ -208,7 +208,7 @@ public Task Get(string owner, string name) Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); Ensure.ArgumentNotNullOrEmptyString(name, "name"); - return ApiConnection.Get(ApiUrls.Repository(owner, name), null, AcceptHeaders.SquashCommitPreview); + return ApiConnection.Get(ApiUrls.Repository(owner, name), null, AcceptHeaders.Concat(AcceptHeaders.SquashCommitPreview, AcceptHeaders.LicensesApiPreview)); } /// @@ -222,7 +222,7 @@ public Task Get(string owner, string name) /// A public Task Get(long repositoryId) { - return ApiConnection.Get(ApiUrls.Repository(repositoryId), null, AcceptHeaders.SquashCommitPreview); + return ApiConnection.Get(ApiUrls.Repository(repositoryId), null, AcceptHeaders.Concat(AcceptHeaders.SquashCommitPreview, AcceptHeaders.LicensesApiPreview)); } /// @@ -237,7 +237,7 @@ public Task Get(long repositoryId) /// A of . public Task> GetAllPublic() { - return ApiConnection.GetAll(ApiUrls.AllPublicRepositories()); + return ApiConnection.GetAll(ApiUrls.AllPublicRepositories(), null, AcceptHeaders.LicensesApiPreview); } /// @@ -257,7 +257,7 @@ public Task> GetAllPublic(PublicRepositoryRequest requ var url = ApiUrls.AllPublicRepositories(request.Since); - return ApiConnection.GetAll(url); + return ApiConnection.GetAll(url, null, AcceptHeaders.LicensesApiPreview); } /// @@ -289,7 +289,7 @@ public Task> GetAllForCurrent(ApiOptions options) { Ensure.ArgumentNotNull(options, "options"); - return ApiConnection.GetAll(ApiUrls.Repositories(), options); + return ApiConnection.GetAll(ApiUrls.Repositories(), null, AcceptHeaders.LicensesApiPreview, options); } /// @@ -315,7 +315,7 @@ public Task> GetAllForCurrent(RepositoryRequest reques Ensure.ArgumentNotNull(request, "request"); Ensure.ArgumentNotNull(options, "options"); - return ApiConnection.GetAll(ApiUrls.Repositories(), request.ToParametersDictionary(), options); + return ApiConnection.GetAll(ApiUrls.Repositories(), request.ToParametersDictionary(), AcceptHeaders.LicensesApiPreview, options); } /// @@ -350,7 +350,7 @@ public Task> GetAllForUser(string login, ApiOptions op Ensure.ArgumentNotNullOrEmptyString(login, "login"); Ensure.ArgumentNotNull(options, "options"); - return ApiConnection.GetAll(ApiUrls.Repositories(login), options); + return ApiConnection.GetAll(ApiUrls.Repositories(login), null, AcceptHeaders.LicensesApiPreview, options); } /// @@ -384,7 +384,7 @@ public Task> GetAllForOrg(string organization, ApiOpti Ensure.ArgumentNotNullOrEmptyString(organization, "organization"); Ensure.ArgumentNotNull(options, "options"); - return ApiConnection.GetAll(ApiUrls.OrganizationRepositories(organization), options); + return ApiConnection.GetAll(ApiUrls.OrganizationRepositories(organization), null, AcceptHeaders.LicensesApiPreview, options); } /// @@ -808,6 +808,37 @@ public Task> GetAllTags(long repositoryId, ApiOptio return ApiConnection.GetAll(ApiUrls.RepositoryTags(repositoryId), options); } + /// + /// Get the contents of a repository's license + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// Returns the contents of the repository's license file, if one is detected. + public Task GetLicenseContents(string owner, string name) + { + Ensure.ArgumentNotNullOrEmptyString(owner, nameof(owner)); + Ensure.ArgumentNotNullOrEmptyString(name, nameof(name)); + + return ApiConnection.Get(ApiUrls.RepositoryLicense(owner, name), null, AcceptHeaders.LicensesApiPreview); + + } + + /// + /// Get the contents of a repository's license + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// Returns the contents of the repository's license file, if one is detected. + public Task GetLicenseContents(long repositoryId) + { + return ApiConnection.Get(ApiUrls.RepositoryLicense(repositoryId), null, AcceptHeaders.LicensesApiPreview); + } + /// /// A client for GitHub's Repository Pages API. /// diff --git a/Octokit/Helpers/AcceptHeaders.cs b/Octokit/Helpers/AcceptHeaders.cs index afd058d779..32b6a88389 100644 --- a/Octokit/Helpers/AcceptHeaders.cs +++ b/Octokit/Helpers/AcceptHeaders.cs @@ -14,6 +14,10 @@ public static class AcceptHeaders public const string OrganizationPermissionsPreview = "application/vnd.github.ironman-preview+json"; + /// + /// Support for retrieving information about open source license usage on GitHub.com. + /// Custom media type: drax-preview Announced: 2015-03-09 Update 1: 2015-06-24 Update 2: 2015-08-04 + /// public const string LicensesApiPreview = "application/vnd.github.drax-preview+json"; public const string ProtectedBranchesApiPreview = "application/vnd.github.loki-preview+json"; @@ -50,5 +54,20 @@ public static class AcceptHeaders public const string OrganizationMembershipPreview = "application/vnd.github.korra-preview+json"; public const string NestedTeamsPreview = "application/vnd.github.hellcat-preview+json"; + + /// + /// Combines multiple preview headers. GitHub API supports Accept header with multiple + /// values separated by comma. + /// + /// Accept header values that will be combine to single Accept header. + /// + /// This Accept header application/vnd.github.loki-preview+json,application/vnd.github.drax-preview+json + /// indicated we want both Protected Branches and Licenses preview APIs. + /// + /// Accept header value. + public static string Concat(params string[] headers) + { + return string.Join(",", headers); + } } } diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index a9c18556ce..9e30cb8342 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -3697,5 +3697,26 @@ public static Uri ProjectCardMove(int id) { return "projects/columns/cards/{0}/moves".FormatUri(id); } + + /// + /// Returns the for repository's license requests. + /// + /// The owner of repo + /// The name of repo + /// The for repository's license requests. + public static Uri RepositoryLicense(string owner, string repo) + { + return "repos/{0}/{1}/license".FormatUri(owner, repo); + } + + /// + /// Returns the for repository's license requests. + /// + /// The id of the repository + /// The for repository's license requests. + public static Uri RepositoryLicense(long repositoryId) + { + return "repositories/{0}/license".FormatUri(repositoryId); + } } } diff --git a/Octokit/Models/Response/License.cs b/Octokit/Models/Response/License.cs index 41c2584dc8..61cc60f6d5 100644 --- a/Octokit/Models/Response/License.cs +++ b/Octokit/Models/Response/License.cs @@ -12,6 +12,7 @@ public class License : LicenseMetadata public License( string key, string name, + string spdxId, string url, string htmlUrl, bool featured, @@ -21,7 +22,7 @@ public License( string body, IEnumerable required, IEnumerable permitted, - IEnumerable forbidden) : base(key, name, url) + IEnumerable forbidden) : base(key, name, spdxId, url, featured) { Ensure.ArgumentNotNull(htmlUrl, "htmlUrl"); Ensure.ArgumentNotNull(description, "description"); @@ -33,7 +34,6 @@ public License( Ensure.ArgumentNotNull(forbidden, "forbidden"); HtmlUrl = htmlUrl; - Featured = featured; Description = description; Category = category; Implementation = implementation; @@ -52,11 +52,6 @@ public License() /// public string HtmlUrl { get; protected set; } - /// - /// Whether the license is one of the licenses featured on https://choosealicense.com - /// - public bool Featured { get; protected set; } - /// /// A description of the license. /// diff --git a/Octokit/Models/Response/LicenseMetadata.cs b/Octokit/Models/Response/LicenseMetadata.cs index 5529570bb0..bba39bf281 100644 --- a/Octokit/Models/Response/LicenseMetadata.cs +++ b/Octokit/Models/Response/LicenseMetadata.cs @@ -6,15 +6,18 @@ namespace Octokit [DebuggerDisplay("{DebuggerDisplay,nq}")] public class LicenseMetadata { - public LicenseMetadata(string key, string name, string url) + public LicenseMetadata(string key, string name, string spdxId, string url, bool featured) { Ensure.ArgumentNotNullOrEmptyString(key, "key"); Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNullOrEmptyString(spdxId, "spdxId"); Ensure.ArgumentNotNull(url, "url"); Key = key; Name = name; + SpdxId = spdxId; Url = url; + Featured = featured; } public LicenseMetadata() @@ -31,11 +34,21 @@ public LicenseMetadata() /// public string Name { get; protected set; } + /// + /// SPDX license identifier. + /// + public string SpdxId { get; protected set; } + /// /// URL to retrieve details about a license. /// public string Url { get; protected set; } + /// + /// Whether the license is one of the licenses featured on https://choosealicense.com + /// + public bool Featured { get; protected set; } + internal virtual string DebuggerDisplay { get diff --git a/Octokit/Models/Response/Repository.cs b/Octokit/Models/Response/Repository.cs index 575d8404f7..77d87140c1 100644 --- a/Octokit/Models/Response/Repository.cs +++ b/Octokit/Models/Response/Repository.cs @@ -14,7 +14,7 @@ public Repository(long id) Id = id; } - public Repository(string url, string htmlUrl, string cloneUrl, string gitUrl, string sshUrl, string svnUrl, string mirrorUrl, long id, User owner, string name, string fullName, string description, string homepage, string language, bool @private, bool fork, int forksCount, int stargazersCount, string defaultBranch, int openIssuesCount, DateTimeOffset? pushedAt, DateTimeOffset createdAt, DateTimeOffset updatedAt, RepositoryPermissions permissions, Repository parent, Repository source, bool hasIssues, bool hasWiki, bool hasDownloads, bool hasPages, int subscribersCount, long size, bool? allowRebaseMerge, bool? allowSquashMerge, bool? allowMergeCommit) + public Repository(string url, string htmlUrl, string cloneUrl, string gitUrl, string sshUrl, string svnUrl, string mirrorUrl, long id, User owner, string name, string fullName, string description, string homepage, string language, bool @private, bool fork, int forksCount, int stargazersCount, string defaultBranch, int openIssuesCount, DateTimeOffset? pushedAt, DateTimeOffset createdAt, DateTimeOffset updatedAt, RepositoryPermissions permissions, Repository parent, Repository source, LicenseMetadata license, bool hasIssues, bool hasWiki, bool hasDownloads, bool hasPages, int subscribersCount, long size, bool? allowRebaseMerge, bool? allowSquashMerge, bool? allowMergeCommit) { Url = url; HtmlUrl = htmlUrl; @@ -42,6 +42,7 @@ public Repository(string url, string htmlUrl, string cloneUrl, string gitUrl, st Permissions = permissions; Parent = parent; Source = source; + License = license; HasIssues = hasIssues; HasWiki = hasWiki; HasDownloads = hasDownloads; @@ -105,6 +106,8 @@ public Repository(string url, string htmlUrl, string cloneUrl, string gitUrl, st public Repository Source { get; protected set; } + public LicenseMetadata License { get; protected set; } + public bool HasIssues { get; protected set; } public bool HasWiki { get; protected set; } diff --git a/Octokit/Models/Response/RepositoryContentLicense.cs b/Octokit/Models/Response/RepositoryContentLicense.cs new file mode 100644 index 0000000000..01082a6649 --- /dev/null +++ b/Octokit/Models/Response/RepositoryContentLicense.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class RepositoryContentLicense : RepositoryContentInfo + { + public RepositoryContentLicense(LicenseMetadata license, string name, string path, string sha, int size, ContentType type, string downloadUrl, string url, string gitUrl, string htmlUrl) + : base(name, path, sha, size, type, downloadUrl, url, gitUrl, htmlUrl) + { + Ensure.ArgumentNotNull(license, "license"); + + License = license; + } + + public RepositoryContentLicense() + { + } + + /// + /// License information + /// + public LicenseMetadata License { get; protected set; } + + internal new string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "License: {0} {1}", this.License?.Key, base.DebuggerDisplay); + } + } + } +}