From 57fe2ce19374c66e782b302dd98e0b26869fe639 Mon Sep 17 00:00:00 2001 From: Sean Killeen Date: Thu, 25 Feb 2021 09:40:25 -0500 Subject: [PATCH] Add repository topics support (#2246) --- .../Clients/IObservableRepositoriesClient.cs | 74 +++++++++++ .../Clients/ObservableRepositoriesClient.cs | 91 +++++++++++++ .../Clients/RepositoriesClientTests.cs | 98 ++++++++++++++ .../Clients/SearchClientTests.cs | 20 +++ .../Clients/RepositoriesClientTests.cs | 111 ++++++++++++++++ Octokit.Tests/Clients/SearchClientTests.cs | 36 +++++ Octokit.Tests/Models/RepositoryTopicsTests.cs | 46 +++++++ Octokit.sln.DotSettings | 1 + Octokit/Clients/IRepositoriesClient.cs | 72 ++++++++++ Octokit/Clients/RepositoriesClient.cs | 124 ++++++++++++++++++ Octokit/Clients/RepositoryForksClient.cs | 2 + Octokit/Helpers/AcceptHeaders.cs | 2 + Octokit/Helpers/ApiUrls.cs | 21 +++ .../Request/SearchRepositoriesRequest.cs | 19 +++ Octokit/Models/Response/Repository.cs | 8 +- Octokit/Models/Response/RepositoryTopics.cs | 33 +++++ 16 files changed, 757 insertions(+), 1 deletion(-) create mode 100644 Octokit.Tests/Models/RepositoryTopicsTests.cs create mode 100644 Octokit/Models/Response/RepositoryTopics.cs diff --git a/Octokit.Reactive/Clients/IObservableRepositoriesClient.cs b/Octokit.Reactive/Clients/IObservableRepositoriesClient.cs index 7d3218e884..ff3c79ff43 100644 --- a/Octokit.Reactive/Clients/IObservableRepositoriesClient.cs +++ b/Octokit.Reactive/Clients/IObservableRepositoriesClient.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reactive; +using System.Threading.Tasks; namespace Octokit.Reactive { @@ -556,5 +558,77 @@ public interface IObservableRepositoriesClient /// Refer to the API documentation for more information: https://developer.github.com/v3/repos/projects/ /// IObservableProjectsClient Project { get; } + + /// + /// Gets all topics for the specified owner and repository name. + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// Options for changing the API response + /// All topics associated with the repository. + IObservable GetAllTopics(string owner, string name, ApiOptions options); + + /// + /// Gets all topics for the specified owner and repository name. + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// All topics associated with the repository. + IObservable GetAllTopics(string owner, string name); + + /// + /// Gets all topics for the specified repository ID. + /// + /// + /// See the API documentation for more details + /// + /// The ID of the repository + /// Options for changing the API response + /// All topics associated with the repository. + IObservable GetAllTopics(long repositoryId, ApiOptions options); + + /// + /// Gets all topics for the specified repository ID. + /// + /// + /// See the API documentation for more details + /// + /// The ID of the repository + /// All topics associated with the repository. + IObservable GetAllTopics(long repositoryId); + + /// + /// Replaces all topics for the specified repository. + /// + /// + /// See the API documentation for more details + /// + /// This is a replacement operation; it is not additive. To clear repository topics, for example, you could specify an empty list of topics here. + /// + /// The ID of the repository + /// The list of topics to associate with the repository + /// All topics now associated with the repository. + IObservable ReplaceAllTopics(long repositoryId, RepositoryTopics topics); + + /// + /// Replaces all topics for the specified repository. + /// + /// + /// See the API documentation for more details + /// + /// This is a replacement operation; it is not additive. To clear repository topics, for example, you could specify an empty list of topics here. + /// + /// The owner of the repository + /// The name of the repository + /// The list of topics to associate with the repository + /// All topics now associated with the repository. + IObservable ReplaceAllTopics(string owner, string name, RepositoryTopics topics); + } } diff --git a/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs b/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs index ddeb1338bc..6b835c29e1 100644 --- a/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs +++ b/Octokit.Reactive/Clients/ObservableRepositoriesClient.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; +using System.Threading.Tasks; using Octokit.Reactive.Clients; using Octokit.Reactive.Internal; @@ -821,5 +823,94 @@ public IObservable Compare(string owner, string name, string @bas /// Refer to the API documentation for more information: https://developer.github.com/v3/repos/projects/ /// public IObservableProjectsClient Project { get; private set; } + + /// + /// Gets all topics for the specified owner and repository name. + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// Options for changing the API response + /// All topics associated with the repository. + public IObservable GetAllTopics(string owner, string name, ApiOptions options) + { + return _client.GetAllTopics(owner, name, options).ToObservable(); + } + + /// + /// Gets all topics for the specified owner and repository name. + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// All topics associated with the repository. + public IObservable GetAllTopics(string owner, string name) + { + return GetAllTopics(owner, name, ApiOptions.None); + } + + /// + /// Gets all topics for the specified repository ID. + /// + /// + /// See the API documentation for more details + /// + /// The ID of the repository + /// Options for changing the API response + /// All topics associated with the repository. + public IObservable GetAllTopics(long repositoryId, ApiOptions options) + { + return _client.GetAllTopics(repositoryId, options).ToObservable(); + } + + /// + /// Gets all topics for the specified repository ID. + /// + /// + /// See the API documentation for more details + /// + /// The ID of the repository + /// All topics associated with the repository. + public IObservable GetAllTopics(long repositoryId) + { + return GetAllTopics(repositoryId, ApiOptions.None); + } + + /// + /// Replaces all topics for the specified repository. + /// + /// + /// See the API documentation for more details + /// + /// This is a replacement operation; it is not additive. To clear repository topics, for example, you could specify an empty list of topics here. + /// + /// The ID of the repository + /// The list of topics to associate with the repository + /// All topics now associated with the repository. + public IObservable ReplaceAllTopics(long repositoryId, RepositoryTopics topics) + { + return _client.ReplaceAllTopics(repositoryId, topics).ToObservable(); + } + + /// + /// Replaces all topics for the specified repository. + /// + /// + /// See the API documentation for more details + /// + /// This is a replacement operation; it is not additive. To clear repository topics, for example, you could specify an empty list of topics here. + /// + /// The owner of the repository + /// The name of the repository + /// The list of topics to associate with the repository + /// All topics now associated with the repository. + public IObservable ReplaceAllTopics(string owner, string name, RepositoryTopics topics) + { + return _client.ReplaceAllTopics(owner, name, topics).ToObservable(); + } } } diff --git a/Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs b/Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs index 7b048cce60..432deb99c3 100644 --- a/Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs +++ b/Octokit.Tests.Integration/Clients/RepositoriesClientTests.cs @@ -1452,6 +1452,104 @@ public async Task GetsEmptyLanguagesWhenNoneWithRepositoryId() } } + public class TheReplaceAllTopicsMethod : IDisposable + { + readonly IGitHubClient _github = Helper.GetAuthenticatedClient(); + private readonly RepositoryTopics _defaultTopics = new RepositoryTopics(new List { "blog", "ruby", "jekyll" }); + private readonly RepositoryContext _context; + private readonly string _theRepository; + private readonly string _theRepoOwner; + + public TheReplaceAllTopicsMethod() + { + _theRepoOwner = Helper.Organization; + _theRepository = Helper.MakeNameWithTimestamp("topics"); + _context = _github.CreateRepositoryContext(_theRepoOwner, new NewRepository(_theRepository)).Result; + var defaultTopicAssignmentResult = _github.Repository.ReplaceAllTopics(_context.RepositoryId, _defaultTopics).Result; + } + + [IntegrationTest] + public async Task ClearsTopicsWithAnEmptyList() + { + var result = await _github.Repository.ReplaceAllTopics(_theRepoOwner, _theRepository, new RepositoryTopics()); + Assert.Empty(result.Names); + + var doubleCheck = await _github.Repository.GetAllTopics(_theRepoOwner, _theRepository); + Assert.Empty((doubleCheck.Names)); + } + + [IntegrationTest] + public async Task ClearsTopicsWithAnEmptyListWhenUsingRepoId() + { + var repo = await _github.Repository.Get(_theRepoOwner, _theRepository); + var result = await _github.Repository.ReplaceAllTopics(repo.Id, new RepositoryTopics()); + Assert.Empty(result.Names); + + var doubleCheck = await _github.Repository.GetAllTopics(_theRepoOwner, _theRepository); + Assert.Empty((doubleCheck.Names)); + } + + [IntegrationTest] + public async Task ReplacesTopicsWithAList() + { + var defaultTopicsList = new RepositoryTopics(_defaultTopics.Names); + var result = await _github.Repository.ReplaceAllTopics(_theRepoOwner, _theRepository, defaultTopicsList); + + Assert.NotEmpty(result.Names); + Assert.Contains(result.Names, item => _defaultTopics.Names.Contains(item, StringComparer.InvariantCultureIgnoreCase)); + + var doubleCheck = await _github.Repository.GetAllTopics(_theRepoOwner, _theRepository); + Assert.Contains(doubleCheck.Names, item => _defaultTopics.Names.Contains(item, StringComparer.InvariantCultureIgnoreCase)); + } + + [IntegrationTest] + public async Task ReplacesTopicsWithAListWhenUsingRepoId() + { + var defaultTopicsList = new RepositoryTopics(_defaultTopics.Names); + var repo = await _github.Repository.Get(_theRepoOwner, _theRepository); + var result = await _github.Repository.ReplaceAllTopics(repo.Id, defaultTopicsList); + + Assert.NotEmpty(result.Names); + Assert.Contains(result.Names, item => _defaultTopics.Names.Contains(item, StringComparer.InvariantCultureIgnoreCase)); + + var doubleCheck = await _github.Repository.GetAllTopics(_theRepoOwner, _theRepository); + Assert.Contains(doubleCheck.Names, item => _defaultTopics.Names.Contains(item, StringComparer.InvariantCultureIgnoreCase)); + } + + public void Dispose() + { + _context.Dispose(); + } + } + public class TheGetAllTopicsMethod + { + private readonly string _repoOwner = "SeanKilleen"; + private readonly string _repoName = "seankilleen.github.io"; + + [IntegrationTest] + public async Task GetsTopicsByOwnerAndName() + { + var github = Helper.GetAnonymousClient(); + var result = await github.Repository.GetAllTopics(_repoOwner, _repoName); + + Assert.Contains("blog", result.Names); + Assert.Contains("ruby", result.Names); + Assert.Contains("jekyll", result.Names); + } + + [IntegrationTest] + public async Task GetsTopicsByRepoID() + { + var github = Helper.GetAnonymousClient(); + var repo = await github.Repository.Get(_repoOwner, _repoName); + var result = await github.Repository.GetAllTopics(repo.Id); + + Assert.Contains("blog", result.Names); + Assert.Contains("ruby", result.Names); + Assert.Contains("jekyll", result.Names); + } + } + public class TheGetAllTagsMethod { [IntegrationTest] diff --git a/Octokit.Tests.Integration/Clients/SearchClientTests.cs b/Octokit.Tests.Integration/Clients/SearchClientTests.cs index 6b931d1904..506da6f7e6 100644 --- a/Octokit.Tests.Integration/Clients/SearchClientTests.cs +++ b/Octokit.Tests.Integration/Clients/SearchClientTests.cs @@ -24,6 +24,26 @@ public async Task SearchForCSharpRepositories() Assert.NotEmpty(repos.Items); } + [IntegrationTest] + public async Task SearchForSingleTopic() + { + var request = new SearchRepositoriesRequest { Topic = "csharp" }; + + var repos = await _gitHubClient.Search.SearchRepo(request); + + Assert.NotEmpty(repos.Items); + } + + [IntegrationTest] + public async Task SearchForMoreThan2Topics() + { + var request = new SearchRepositoriesRequest { Topics = Octokit.Range.GreaterThanOrEquals(2) }; + + var repos = await _gitHubClient.Search.SearchRepo(request); + + Assert.NotEmpty(repos.Items); + } + [IntegrationTest] public async Task SearchForCSharpRepositoriesUpdatedIn2020() { diff --git a/Octokit.Tests/Clients/RepositoriesClientTests.cs b/Octokit.Tests/Clients/RepositoriesClientTests.cs index 1bc89b7b26..c518b7152c 100644 --- a/Octokit.Tests/Clients/RepositoriesClientTests.cs +++ b/Octokit.Tests/Clients/RepositoriesClientTests.cs @@ -1251,5 +1251,116 @@ public void RequestsTheCorrectUrl() .Get(Arg.Is(u => u.ToString() == "repos/owner/name/commits/reference"), null, "application/vnd.github.v3.sha"); } } + + public class TheGetAllTopicsMethod + { + readonly RepositoriesClient _client = new RepositoriesClient(Substitute.For()); + + [Fact] + public async Task EnsuresNonNullArguments() + { + await Assert.ThrowsAsync(() => _client.GetAllTopics(123, null)); + await Assert.ThrowsAsync(() => _client.GetAllTopics("owner", "repo", null)); + await Assert.ThrowsAsync(() => _client.GetAllTopics(null, "repo", ApiOptions.None)); + await Assert.ThrowsAsync(() => _client.GetAllTopics("owner", null, ApiOptions.None)); + } + + [Fact] + public async Task EnsuresNonEmptyArguments() + { + await Assert.ThrowsAsync(() => _client.GetAllTopics(string.Empty, "repo", ApiOptions.None)); + await Assert.ThrowsAsync(() => _client.GetAllTopics("owner", string.Empty, ApiOptions.None)); + } + + [Fact] + public void RequestsTheCorrectUrlForOwnerAndRepo() + { + var connection = Substitute.For(); + var client = new RepositoriesClient(connection); + + client.GetAllTopics("owner", "name"); + + connection.Received() + .Get(Arg.Is(u => u.ToString() == "repos/owner/name/topics"), null, "application/vnd.github.mercy-preview+json"); + } + + [Fact] + public void RequestsTheCorrectUrlForRepoId() + { + var connection = Substitute.For(); + var client = new RepositoriesClient(connection); + + client.GetAllTopics(1234); + + connection.Received() + .Get(Arg.Is(u => u.ToString() == "repositories/1234/topics"), null, "application/vnd.github.mercy-preview+json"); + } + } + + public class TheReplaceAllTopicsMethod + { + private readonly RepositoryTopics _emptyTopics = new RepositoryTopics(); + private readonly RepositoryTopics _listOfTopics = new RepositoryTopics(new List { "one", "two", "three" }); + private readonly IApiConnection _connection; + private readonly RepositoriesClient _client; + + public TheReplaceAllTopicsMethod() + { + _connection = Substitute.For(); + _client = new RepositoriesClient(_connection); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + await Assert.ThrowsAsync(() => _client.ReplaceAllTopics("owner", "repo", null)); + await Assert.ThrowsAsync(() => _client.ReplaceAllTopics(123, null)); + await Assert.ThrowsAsync(() => _client.ReplaceAllTopics(null, "repo", _emptyTopics)); + await Assert.ThrowsAsync(() => _client.ReplaceAllTopics("owner", null, _emptyTopics)); + } + + [Fact] + public async Task EnsuresNonEmptyArguments() + { + await Assert.ThrowsAsync(() => _client.ReplaceAllTopics(string.Empty, "repo", _emptyTopics)); + await Assert.ThrowsAsync(() => _client.ReplaceAllTopics("owner", string.Empty, _emptyTopics)); + } + + [Fact] + public async Task RequestsTheCorrectUrlForOwnerAndRepoWithEmptyTopics() + { + await _client.ReplaceAllTopics("owner", "name", _emptyTopics); + + _connection.Received() + .Put(Arg.Is(u => u.ToString() == "repos/owner/name/topics"), _emptyTopics, null,"application/vnd.github.mercy-preview+json"); + } + + [Fact] + public async Task RequestsTheCorrectUrlForOwnerAndRepoWithListOfTopics() + { + await _client.ReplaceAllTopics("owner", "name", _listOfTopics); + + _connection.Received() + .Put(Arg.Is(u => u.ToString() == "repos/owner/name/topics"), _listOfTopics,null, "application/vnd.github.mercy-preview+json"); + } + + [Fact] + public async Task RequestsTheCorrectUrlForRepoIdWithEmptyTopics() + { + await _client.ReplaceAllTopics(1234, _emptyTopics); + + _connection.Received() + .Put(Arg.Is(u => u.ToString() == "repositories/1234/topics"), _emptyTopics, null, "application/vnd.github.mercy-preview+json"); + } + + [Fact] + public async Task RequestsTheCorrectUrlForRepoIdWithListOfTopics() + { + await _client.ReplaceAllTopics(1234,_listOfTopics); + + _connection.Received() + .Put(Arg.Is(u => u.ToString() == "repositories/1234/topics"), _listOfTopics,null, "application/vnd.github.mercy-preview+json"); + } + } } } diff --git a/Octokit.Tests/Clients/SearchClientTests.cs b/Octokit.Tests/Clients/SearchClientTests.cs index 1ead16ebf1..d6a481766e 100644 --- a/Octokit.Tests/Clients/SearchClientTests.cs +++ b/Octokit.Tests/Clients/SearchClientTests.cs @@ -702,6 +702,42 @@ public void TestingTheSearchParameter() Arg.Is>(d => string.IsNullOrEmpty(d["q"]))); } + + [Fact] + public async Task TestingTheTopicQualifier() + { + var request = new SearchRepositoriesRequest("github"); + request.Topic = "TheTopic"; + + Assert.Contains("topic:thetopic", request.MergedQualifiers()); + } + + [Fact] + public void TestingTheTopicsQualifierWithGreaterThanOneTopic() + { + var request = new SearchRepositoriesRequest("github"); + request.Topics = Range.GreaterThan(1); + + Assert.Contains("topics:>1", request.MergedQualifiers()); + } + + [Fact] + public void TestingTheTopicsQualifierWithExactlyOneTopic() + { + var request = new SearchRepositoriesRequest("github"); + request.Topics = new Range(1); + + Assert.Contains("topics:1", request.MergedQualifiers()); + } + + [Fact] + public void TestingTheTopicsQualifierWithTwoOrLessTopics() + { + var request = new SearchRepositoriesRequest("github"); + request.Topics = Range.LessThanOrEquals(2); + + Assert.Contains("topics:<=2", request.MergedQualifiers()); + } } public class TheSearchIssuesMethod diff --git a/Octokit.Tests/Models/RepositoryTopicsTests.cs b/Octokit.Tests/Models/RepositoryTopicsTests.cs new file mode 100644 index 0000000000..08c53a27e9 --- /dev/null +++ b/Octokit.Tests/Models/RepositoryTopicsTests.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Xunit; + +namespace Octokit.Tests.Models +{ + public class RepositoryTopicsTests + { + public class Ctor + { + [Fact] + public void EmptyListWhenCtorEmpty() + { + var result = new RepositoryTopics(); + Assert.NotNull(result.Names); + Assert.Empty(result.Names); + } + + [Fact] + public void EmptyListWhenCtorListIsEmpty() + { + var result = new RepositoryTopics(new List()); + Assert.NotNull(result.Names); + Assert.Empty(result.Names); + } + + [Fact] + public void EmptyListWhenCtorIsNull() + { + var result = new RepositoryTopics(null); + Assert.NotNull(result.Names); + Assert.Empty(result.Names); + } + + [Fact] + public void SetsListWhenListProvided() + { + var theItems = new List {"one", "two", "three"}; + var result = new RepositoryTopics(theItems); + + Assert.Contains("one", result.Names); + Assert.Contains("two", result.Names); + Assert.Contains("three", result.Names); + } + } + } +} diff --git a/Octokit.sln.DotSettings b/Octokit.sln.DotSettings index 50852c97ec..7f9262b459 100644 --- a/Octokit.sln.DotSettings +++ b/Octokit.sln.DotSettings @@ -269,6 +269,7 @@ II.2.12 <HandlesEvent /> True True True + True True True True diff --git a/Octokit/Clients/IRepositoriesClient.cs b/Octokit/Clients/IRepositoriesClient.cs index ab9e31fb94..dd291cfd1e 100644 --- a/Octokit/Clients/IRepositoriesClient.cs +++ b/Octokit/Clients/IRepositoriesClient.cs @@ -621,5 +621,77 @@ public interface IRepositoriesClient /// Refer to the API documentation for more information: https://developer.github.com/v3/repos/projects/ /// IProjectsClient Project { get; } + + /// + /// Gets all topics for the specified owner and repository name. + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// Options for changing the API response + /// All topics associated with the repository. + Task GetAllTopics(string owner, string name, ApiOptions options); + + /// + /// Gets all topics for the specified owner and repository name. + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// All topics associated with the repository. + Task GetAllTopics(string owner, string name); + + /// + /// Gets all topics for the specified repository ID. + /// + /// + /// See the API documentation for more details + /// + /// The ID of the repository + /// Options for changing the API response + /// All topics associated with the repository. + Task GetAllTopics(long repositoryId, ApiOptions options); + + /// + /// Gets all topics for the specified repository ID. + /// + /// + /// See the API documentation for more details + /// + /// The ID of the repository + /// All topics associated with the repository. + Task GetAllTopics(long repositoryId); + + /// + /// Replaces all topics for the specified repository. + /// + /// + /// See the API documentation for more details + /// + /// This is a replacement operation; it is not additive. To clear repository topics, for example, you could specify an empty list of topics here. + /// + /// The ID of the repository + /// The list of topics to associate with the repository + /// All topics now associated with the repository. + Task ReplaceAllTopics(long repositoryId, RepositoryTopics topics); + + /// + /// Replaces all topics for the specified repository. + /// + /// + /// See the API documentation for more details + /// + /// This is a replacement operation; it is not additive. To clear repository topics, for example, you could specify an empty list of topics here. + /// + /// The owner of the repository + /// The name of the repository + /// The list of topics to associate with the repository + /// All topics now associated with the repository. + Task ReplaceAllTopics(string owner, string name, RepositoryTopics topics); + } } diff --git a/Octokit/Clients/RepositoriesClient.cs b/Octokit/Clients/RepositoriesClient.cs index 44a7ab700a..256b24fbd9 100644 --- a/Octokit/Clients/RepositoriesClient.cs +++ b/Octokit/Clients/RepositoriesClient.cs @@ -179,6 +179,7 @@ public Task Delete(long repositoryId) /// The name of the repository /// Repository transfer information /// A + [Preview("mercy")] [ManualRoute("POST", "/repos/{owner}/{repo}/transfer")] public Task Transfer(string owner, string name, RepositoryTransfer repositoryTransfer) { @@ -198,6 +199,7 @@ public Task Transfer(string owner, string name, RepositoryTransfer r /// The id of the repository /// Repository transfer information /// A + [Preview("mercy")] [ManualRoute("POST", "/repositories/{id}/transfer")] public Task Transfer(long repositoryId, RepositoryTransfer repositoryTransfer) { @@ -231,6 +233,7 @@ public Task Edit(string owner, string name, RepositoryUpdate update) /// The Id of the repository /// New values to update the repository with /// The updated + [Preview("mercy")] [ManualRoute("PATCH", "/repositories/{id}")] public Task Edit(long repositoryId, RepositoryUpdate update) { @@ -268,6 +271,7 @@ public Task Get(string owner, string name) /// The Id of the repository /// Thrown when a general API error occurs. /// A + [Preview("mercy")] [ManualRoute("GET", "/repositories/{id}")] public Task Get(long repositoryId) { @@ -727,6 +731,126 @@ public Task> GetAllContributors(long reposi return ApiConnection.GetAll(ApiUrls.RepositoryContributors(repositoryId), parameters, options); } + /// + /// Gets all topics for the specified repository ID. + /// + /// + /// See the API documentation for more details + /// + /// The ID of the repository + /// Options for changing the API response + /// All topics associated with the repository. + [ManualRoute("GET", "/repositories/{id}/topics")] + public async Task GetAllTopics(long repositoryId, ApiOptions options) + { + Ensure.ArgumentNotNull(options, nameof(options)); + var endpoint = ApiUrls.RepositoryTopics(repositoryId); + var data = await ApiConnection.Get(endpoint,null,AcceptHeaders.RepositoryTopicsPreview).ConfigureAwait(false); + + return data ?? new RepositoryTopics(); + } + + /// + /// Gets all topics for the specified repository ID. + /// + /// + /// See the API documentation for more details + /// + /// The ID of the repository + /// All topics associated with the repository. + [ManualRoute("GET", "/repositories/{id}/topics")] + + public Task GetAllTopics(long repositoryId) + { + return GetAllTopics(repositoryId, ApiOptions.None); + } + + /// + /// Gets all topics for the specified owner and repository name. + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// Options for changing the API response + /// All topics associated with the repository. + [ManualRoute("GET", "/repos/{owner}/{repo}/topics")] + public async Task GetAllTopics(string owner, string name, ApiOptions options) + { + Ensure.ArgumentNotNullOrEmptyString(owner, nameof(owner)); + Ensure.ArgumentNotNullOrEmptyString(name, nameof(name)); + Ensure.ArgumentNotNull(options, nameof(options)); + + var endpoint = ApiUrls.RepositoryTopics(owner, name); + var data = await ApiConnection.Get(endpoint, null, AcceptHeaders.RepositoryTopicsPreview).ConfigureAwait(false); + + return data ?? new RepositoryTopics(); + } + + /// + /// Gets all topics for the specified owner and repository name. + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// All topics associated with the repository. + [ManualRoute("GET", "/repos/{owner}/{repo}/topics")] + + public Task GetAllTopics(string owner, string name) + { + return GetAllTopics(owner, name, ApiOptions.None); + } + + /// + /// Replaces all topics for the specified repository. + /// + /// + /// See the API documentation for more details + /// + /// This is a replacement operation; it is not additive. To clear repository topics, for example, you could specify an empty list of topics here. + /// + /// The owner of the repository + /// The name of the repository + /// The list of topics to associate with the repository + /// All topics now associated with the repository. + [ManualRoute("PUT", "/repos/{owner}/{repo}/topics")] + public async Task ReplaceAllTopics(string owner, string name, RepositoryTopics topics) + { + Ensure.ArgumentNotNullOrEmptyString(owner, nameof(owner)); + Ensure.ArgumentNotNullOrEmptyString(name, nameof(name)); + Ensure.ArgumentNotNull(topics, nameof(topics)); + + var endpoint = ApiUrls.RepositoryTopics(owner, name); + var data = await ApiConnection.Put(endpoint, topics,null, AcceptHeaders.RepositoryTopicsPreview).ConfigureAwait(false); + + return data ?? new RepositoryTopics(); + } + + /// + /// Replaces all topics for the specified repository. + /// + /// + /// See the API documentation for more details + /// + /// This is a replacement operation; it is not additive. To clear repository topics, for example, you could specify an empty list of topics here. + /// + /// The ID of the repository + /// The list of topics to associate with the repository + /// All topics now associated with the repository. + [ManualRoute("PUT", "/repositories/{id}/topics")] + public async Task ReplaceAllTopics(long repositoryId, RepositoryTopics topics) + { + Ensure.ArgumentNotNull(topics, nameof(topics)); + + var endpoint = ApiUrls.RepositoryTopics(repositoryId); + var data = await ApiConnection.Put(endpoint, topics, null, AcceptHeaders.RepositoryTopicsPreview).ConfigureAwait(false); + + return data ?? new RepositoryTopics(); + } + /// /// Gets all languages for the specified repository. /// diff --git a/Octokit/Clients/RepositoryForksClient.cs b/Octokit/Clients/RepositoryForksClient.cs index 3a32b59b70..89496dddab 100644 --- a/Octokit/Clients/RepositoryForksClient.cs +++ b/Octokit/Clients/RepositoryForksClient.cs @@ -168,6 +168,7 @@ public Task> GetAll(long repositoryId, RepositoryForks /// The owner of the repository /// The name of the repository /// Used to fork a repository + [Preview("mercy")] [ManualRoute("POST", "/repos/{owner}/{repo}/forks")] public Task Create(string owner, string name, NewRepositoryFork fork) { @@ -186,6 +187,7 @@ public Task Create(string owner, string name, NewRepositoryFork fork /// /// The Id of the repository /// Used to fork a repository + [Preview("mercy")] [ManualRoute("POST", "/repositories/{id}/forks")] public Task Create(long repositoryId, NewRepositoryFork fork) { diff --git a/Octokit/Helpers/AcceptHeaders.cs b/Octokit/Helpers/AcceptHeaders.cs index eaca8811f6..09826011e3 100644 --- a/Octokit/Helpers/AcceptHeaders.cs +++ b/Octokit/Helpers/AcceptHeaders.cs @@ -50,6 +50,8 @@ public static class AcceptHeaders public const string OAuthApplicationsPreview = "application/vnd.github.doctor-strange-preview+json"; + public const string RepositoryTopicsPreview = "application/vnd.github.mercy-preview+json"; + public const string VisibilityPreview = "application/vnd.github.nebula-preview+json"; /// diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index 252eedcc61..74f0ff3f9c 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -1902,6 +1902,27 @@ public static Uri RepositoryContributors(string owner, string name) return "repos/{0}/{1}/contributors".FormatUri(owner, name); } + /// + /// Returns the for repository topics. + /// + /// The owner of the repository + /// The name of the repository + /// the for repository topics. + public static Uri RepositoryTopics(string owner, string name) + { + return "repos/{0}/{1}/topics".FormatUri(owner, name); + } + + /// + /// Returns the for repository topics. + /// + /// The ID of the repository + /// the for repository topics. + public static Uri RepositoryTopics(long repositoryId) + { + return "repositories/{0}/topics".FormatUri(repositoryId); + } + /// /// Returns the for repository languages. /// diff --git a/Octokit/Models/Request/SearchRepositoriesRequest.cs b/Octokit/Models/Request/SearchRepositoriesRequest.cs index 21769fdbd8..9e1869713f 100644 --- a/Octokit/Models/Request/SearchRepositoriesRequest.cs +++ b/Octokit/Models/Request/SearchRepositoriesRequest.cs @@ -123,6 +123,16 @@ public IEnumerable In /// public bool? Archived { get; set; } + /// + /// Filters on whether repositories are tagged with the given topic. + /// + public string Topic { get; set; } + + /// + /// Filters on the number of topics that a repository is associated with. + /// + public Range Topics { get; set; } + public override IReadOnlyList MergedQualifiers() { var parameters = new List(); @@ -177,6 +187,15 @@ public override IReadOnlyList MergedQualifiers() parameters.Add(string.Format(CultureInfo.InvariantCulture, "archived:{0}", Archived.ToString().ToLower())); } + if (Topic != null) + { + parameters.Add(string.Format(CultureInfo.InvariantCulture, "topic:{0}", Topic.ToLower())); + } + + if (Topics != null) + { + parameters.Add(string.Format(CultureInfo.InvariantCulture, "topics:{0}", Topics.ToString().ToLower())); + } if (License != null) { parameters.Add(string.Format(CultureInfo.InvariantCulture, "license:{0}", License.ToParameter())); diff --git a/Octokit/Models/Response/Repository.cs b/Octokit/Models/Response/Repository.cs index 23eab039b0..504d98b330 100644 --- a/Octokit/Models/Response/Repository.cs +++ b/Octokit/Models/Response/Repository.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; +using System.Linq; namespace Octokit { @@ -14,7 +17,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, string nodeId, User owner, string name, string fullName, bool isTemplate, 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, bool archived, int watchersCount, bool? deleteBranchOnMerge, RepositoryVisibility visibility) + public Repository(string url, string htmlUrl, string cloneUrl, string gitUrl, string sshUrl, string svnUrl, string mirrorUrl, long id, string nodeId, User owner, string name, string fullName, bool isTemplate, 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, bool archived, int watchersCount, bool? deleteBranchOnMerge, RepositoryVisibility visibility, IEnumerable topics) { Url = url; HtmlUrl = htmlUrl; @@ -56,6 +59,7 @@ public Repository(string url, string htmlUrl, string cloneUrl, string gitUrl, st AllowMergeCommit = allowMergeCommit; Archived = archived; WatchersCount = watchersCount; + Topics = topics.ToList(); DeleteBranchOnMerge = deleteBranchOnMerge; Visibility = visibility; } @@ -144,6 +148,8 @@ public Repository(string url, string htmlUrl, string cloneUrl, string gitUrl, st public bool Archived { get; protected set; } + public IReadOnlyList Topics { get; protected set; } + public bool? DeleteBranchOnMerge { get; protected set; } public RepositoryVisibility? Visibility { get; protected set; } diff --git a/Octokit/Models/Response/RepositoryTopics.cs b/Octokit/Models/Response/RepositoryTopics.cs new file mode 100644 index 0000000000..93af0fd073 --- /dev/null +++ b/Octokit/Models/Response/RepositoryTopics.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class RepositoryTopics + { + public RepositoryTopics() { Names = new List(); } + + public RepositoryTopics(IEnumerable names) + { + var initialItems = names?.ToList() ?? new List(); + Names = new ReadOnlyCollection(initialItems); + } + + public IReadOnlyList Names { get; protected set; } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + "RepositoryTopics: Names: {0}", string.Join(", ", Names)); + } + } + } +}