Support for IsTemplate and Create Template from Repository (#2331)

This commit is contained in:
Chris Simpson
2022-06-30 21:20:45 +01:00
committed by GitHub
parent 659ce5f4f7
commit f317f9dadc
11 changed files with 237 additions and 22 deletions

View File

@@ -23,6 +23,15 @@ namespace Octokit.Reactive
/// <returns>An <see cref="IObservable{Repository}"/> instance for the created repository</returns>
IObservable<Repository> Create(string organizationLogin, NewRepository newRepository);
/// <summary>
/// Creates a new repository using a repository template.
/// </summary>
/// <param name="templateOwner">The owner of the template</param>
/// <param name="templateRepo">The name of the template</param>
/// <param name="newRepository">A <see cref="NewRepositoryFromTemplate"/> instance describing the new repository to create from a template</param>
/// <returns>An <see cref="IObservable{Repository}"/> instance for the created repository</returns>
IObservable<Repository> Generate(string templateOwner, string templateRepo, NewRepositoryFromTemplate newRepository);
/// <summary>
/// Deletes a repository for the specified owner and name.
/// </summary>

View File

@@ -72,6 +72,24 @@ namespace Octokit.Reactive
return _client.Create(organizationLogin, newRepository).ToObservable();
}
/// <summary>
/// Creates a new repository from a template
/// </summary>
/// <param name="templateOwner">The organization or person who will owns the template</param>
/// <param name="templateRepo">The name of template repository to work from</param>
/// <param name="newRepository">A <see cref="NewRepositoryFromTemplate"/> instance describing the new repository to create from a template</param>
/// <returns></returns>
public IObservable<Repository> Generate(string templateOwner, string templateRepo, NewRepositoryFromTemplate newRepository)
{
Ensure.ArgumentNotNull(templateOwner, nameof(templateOwner));
Ensure.ArgumentNotNull(templateRepo, nameof(templateRepo));
Ensure.ArgumentNotNull(newRepository, nameof(newRepository));
if (string.IsNullOrEmpty(newRepository.Name))
throw new ArgumentException("The new repository's name must not be null.");
return _client.Generate(templateOwner, templateRepo, newRepository).ToObservable();
}
/// <summary>
/// Deletes a repository for the specified owner and name.
/// </summary>

View File

@@ -219,6 +219,51 @@ public class RepositoriesClientTests
}
}
[IntegrationTest]
public async Task CreatesARepositoryAsTemplate()
{
var github = Helper.GetAuthenticatedClient();
var repoName = Helper.MakeNameWithTimestamp("repo-as-template");
var newRepository = new NewRepository(repoName)
{
IsTemplate = true
};
using (var context = await github.CreateRepositoryContext(newRepository))
{
var createdRepository = context.Repository;
var repository = await github.Repository.Get(Helper.UserName, repoName);
Assert.True(repository.IsTemplate);
}
}
[IntegrationTest]
public async Task CreatesARepositoryFromTemplate()
{
var github = Helper.GetAuthenticatedClient();
var repoTemplateName = Helper.MakeNameWithTimestamp("repo-template");
var repoFromTemplateName = Helper.MakeNameWithTimestamp("repo-from-template");
var owner = github.User.Current().Result.Login;
var newTemplate = new NewRepository(repoTemplateName)
{
IsTemplate = true
};
var newRepo = new NewRepositoryFromTemplate(repoFromTemplateName);
using (var templateContext = await github.CreateRepositoryContext(newTemplate))
using (var context = await github.Generate(owner, repoFromTemplateName, newRepo))
{
var repository = await github.Repository.Get(Helper.UserName, repoFromTemplateName);
Assert.Equal(repoFromTemplateName, repository.Name);
}
}
[IntegrationTest]
public async Task CreatesARepositoryWithDeleteBranchOnMergeEnabled()
{

View File

@@ -26,6 +26,13 @@ namespace Octokit.Tests.Integration.Helpers
return new RepositoryContext(client.Connection, repo);
}
internal static async Task<RepositoryContext> Generate(this IGitHubClient client, string owner, string repoName, NewRepositoryFromTemplate newRepository)
{
var repo = await client.Repository.Generate(owner, repoName, newRepository);
return new RepositoryContext(client.Connection, repo);
}
internal static async Task<TeamContext> CreateTeamContext(this IGitHubClient client, string organization, NewTeam newTeam)
{
newTeam.Privacy = TeamPrivacy.Closed;

View File

@@ -42,7 +42,7 @@ namespace Octokit.Tests.Clients
connection.Received().Post<Repository>(Arg.Is<Uri>(u => u.ToString() == "user/repos"),
Arg.Any<NewRepository>(),
"application/vnd.github.nebula-preview+json");
"application/vnd.github.nebula-preview+json,application/vnd.github.baptiste-preview+json");
}
[Fact]
@@ -54,7 +54,7 @@ namespace Octokit.Tests.Clients
client.Create(newRepository);
connection.Received().Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json");
connection.Received().Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json,application/vnd.github.baptiste-preview+json");
}
[Fact]
@@ -70,7 +70,7 @@ namespace Octokit.Tests.Clients
var connection = Substitute.For<IApiConnection>();
connection.Connection.BaseAddress.Returns(GitHubClient.GitHubApiUrl);
connection.Connection.Credentials.Returns(credentials);
connection.Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json")
connection.Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json,application/vnd.github.baptiste-preview+json")
.Returns<Task<Repository>>(_ => { throw new ApiValidationException(response); });
var client = new RepositoriesClient(connection);
@@ -97,7 +97,7 @@ namespace Octokit.Tests.Clients
var connection = Substitute.For<IApiConnection>();
connection.Connection.BaseAddress.Returns(GitHubClient.GitHubApiUrl);
connection.Connection.Credentials.Returns(credentials);
connection.Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json")
connection.Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json,application/vnd.github.baptiste-preview+json")
.Returns<Task<Repository>>(_ => { throw new ApiValidationException(response); });
var client = new RepositoriesClient(connection);
@@ -130,7 +130,7 @@ namespace Octokit.Tests.Clients
connection.Received().Post<Repository>(
Arg.Is<Uri>(u => u.ToString() == "orgs/theLogin/repos"),
Args.NewRepository,
"application/vnd.github.nebula-preview+json");
"application/vnd.github.nebula-preview+json,application/vnd.github.baptiste-preview+json");
}
[Fact]
@@ -142,7 +142,7 @@ namespace Octokit.Tests.Clients
await client.Create("aLogin", newRepository);
connection.Received().Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json");
connection.Received().Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json,application/vnd.github.baptiste-preview+json");
}
[Fact]
@@ -156,7 +156,7 @@ namespace Octokit.Tests.Clients
+ @"""code"":""custom"",""field"":""name"",""message"":""name already exists on this account""}]}");
var connection = Substitute.For<IApiConnection>();
connection.Connection.BaseAddress.Returns(GitHubClient.GitHubApiUrl);
connection.Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json")
connection.Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json,application/vnd.github.baptiste-preview+json")
.Returns<Task<Repository>>(_ => { throw new ApiValidationException(response); });
var client = new RepositoriesClient(connection);
@@ -181,7 +181,7 @@ namespace Octokit.Tests.Clients
+ @"""http://developer.github.com/v3/repos/#create"",""errors"":[]}");
var connection = Substitute.For<IApiConnection>();
connection.Connection.BaseAddress.Returns(GitHubClient.GitHubApiUrl);
connection.Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json")
connection.Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json,application/vnd.github.baptiste-preview+json")
.Returns<Task<Repository>>(_ => { throw new ApiValidationException(response); });
var client = new RepositoriesClient(connection);
@@ -202,7 +202,7 @@ namespace Octokit.Tests.Clients
+ @"""code"":""custom"",""field"":""name"",""message"":""name already exists on this account""}]}");
var connection = Substitute.For<IApiConnection>();
connection.Connection.BaseAddress.Returns(new Uri("https://example.com"));
connection.Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json")
connection.Post<Repository>(Args.Uri, newRepository, "application/vnd.github.nebula-preview+json,application/vnd.github.baptiste-preview+json")
.Returns<Task<Repository>>(_ => { throw new ApiValidationException(response); });
var client = new RepositoriesClient(connection);
@@ -214,6 +214,44 @@ namespace Octokit.Tests.Clients
}
}
public class TheGenerateMethod
{
[Fact]
public async Task EnsuresNonNullArguments()
{
var client = new RepositoriesClient(Substitute.For<IApiConnection>());
await Assert.ThrowsAsync<ArgumentNullException>(() => client.Generate(null, null, null));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.Generate("asd", null, null));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.Generate("asd", "asd", null));
}
[Fact]
public void UsesTheUserReposUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new RepositoriesClient(connection);
client.Generate("asd", "asd", new NewRepositoryFromTemplate("aName"));
connection.Received().Post<Repository>(Arg.Is<Uri>(u => u.ToString() == "repos/asd/asd/generate"),
Arg.Any<NewRepositoryFromTemplate>(),
"application/vnd.github.baptiste-preview+json");
}
[Fact]
public void TheNewRepositoryDescription()
{
var connection = Substitute.For<IApiConnection>();
var client = new RepositoriesClient(connection);
var newRepository = new NewRepositoryFromTemplate("aName");
client.Generate("anOwner", "aRepo", newRepository);
connection.Received().Post<Repository>(Args.Uri, newRepository, "application/vnd.github.baptiste-preview+json");
}
}
public class TheTransferMethod
{
[Fact]
@@ -490,7 +528,7 @@ namespace Octokit.Tests.Clients
connection.Received()
.GetAll<Repository>(Arg.Is<Uri>(u => u.ToString() == "user/repos"),
null,
"application/vnd.github.nebula-preview+json",
"application/vnd.github.nebula-preview+json,application/vnd.github.baptiste-preview+json",
Args.ApiOptions);
}
@@ -644,7 +682,7 @@ namespace Octokit.Tests.Clients
await client.GetAllForOrg("orgname");
connection.Received()
.GetAll<Repository>(Arg.Is<Uri>(u => u.ToString() == "orgs/orgname/repos"), null, "application/vnd.github.nebula-preview+json", Args.ApiOptions);
.GetAll<Repository>(Arg.Is<Uri>(u => u.ToString() == "orgs/orgname/repos"), null, "application/vnd.github.nebula-preview+json,application/vnd.github.baptiste-preview+json", Args.ApiOptions);
}
[Fact]
@@ -1078,7 +1116,7 @@ namespace Octokit.Tests.Clients
connection.Received()
.Patch<Repository>(Arg.Is<Uri>(u => u.ToString() == "repos/owner/repo"),
Arg.Any<RepositoryUpdate>(),
"application/vnd.github.nebula-preview+json");
"application/vnd.github.nebula-preview+json,application/vnd.github.baptiste-preview+json");
}
[Fact]
@@ -1332,7 +1370,7 @@ namespace Octokit.Tests.Clients
await _client.ReplaceAllTopics("owner", "name", _emptyTopics);
_connection.Received()
.Put<RepositoryTopics>(Arg.Is<Uri>(u => u.ToString() == "repos/owner/name/topics"), _emptyTopics, null,"application/vnd.github.mercy-preview+json");
.Put<RepositoryTopics>(Arg.Is<Uri>(u => u.ToString() == "repos/owner/name/topics"), _emptyTopics, null, "application/vnd.github.mercy-preview+json");
}
[Fact]
@@ -1341,7 +1379,7 @@ namespace Octokit.Tests.Clients
await _client.ReplaceAllTopics("owner", "name", _listOfTopics);
_connection.Received()
.Put<RepositoryTopics>(Arg.Is<Uri>(u => u.ToString() == "repos/owner/name/topics"), _listOfTopics,null, "application/vnd.github.mercy-preview+json");
.Put<RepositoryTopics>(Arg.Is<Uri>(u => u.ToString() == "repos/owner/name/topics"), _listOfTopics, null, "application/vnd.github.mercy-preview+json");
}
[Fact]
@@ -1356,10 +1394,10 @@ namespace Octokit.Tests.Clients
[Fact]
public async Task RequestsTheCorrectUrlForRepoIdWithListOfTopics()
{
await _client.ReplaceAllTopics(1234,_listOfTopics);
await _client.ReplaceAllTopics(1234, _listOfTopics);
_connection.Received()
.Put<RepositoryTopics>(Arg.Is<Uri>(u => u.ToString() == "repositories/1234/topics"), _listOfTopics,null, "application/vnd.github.mercy-preview+json");
.Put<RepositoryTopics>(Arg.Is<Uri>(u => u.ToString() == "repositories/1234/topics"), _listOfTopics, null, "application/vnd.github.mercy-preview+json");
}
}
}

View File

@@ -77,6 +77,16 @@ namespace Octokit
/// <returns>A <see cref="Repository"/> instance for the created repository</returns>
Task<Repository> Create(string organizationLogin, NewRepository newRepository);
/// <summary>
/// Creates a new repository from a template
/// </summary>
/// <param name="templateOwner">The organization or person who will owns the template</param>
/// <param name="templateRepo">The name of template repository to work from</param>
/// <param name="newRepository"></param>
/// <returns></returns>
Task<Repository> Generate(string templateOwner, string templateRepo, NewRepositoryFromTemplate newRepository);
/// <summary>
/// Deletes the specified repository.
/// </summary>

View File

@@ -70,6 +70,7 @@ namespace Octokit
/// <exception cref="ApiException">Thrown when a general API error occurs.</exception>
/// <returns>A <see cref="Repository"/> instance for the created repository</returns>
[Preview("nebula")]
[Preview("baptiste")]
[ManualRoute("POST", "/orgs/{org}/repos")]
public Task<Repository> Create(string organizationLogin, NewRepository newRepository)
{
@@ -81,11 +82,31 @@ namespace Octokit
return Create(ApiUrls.OrganizationRepositories(organizationLogin), organizationLogin, newRepository);
}
/// <summary>
/// Creates a new repository from a template
/// </summary>
/// <param name="templateOwner">The organization or person who will owns the template</param>
/// <param name="templateRepo">The name of template repository to work from</param>
/// <param name="newRepository"></param>
/// <returns></returns>
[Preview("baptiste")]
[ManualRoute("POST", "/repos/{owner}/{repo}/generate")]
public Task<Repository> Generate(string templateOwner, string templateRepo, NewRepositoryFromTemplate newRepository)
{
Ensure.ArgumentNotNull(templateOwner, nameof(templateOwner));
Ensure.ArgumentNotNull(templateRepo, nameof(templateRepo));
Ensure.ArgumentNotNull(newRepository, nameof(newRepository));
if (string.IsNullOrEmpty(newRepository.Name))
throw new ArgumentException("The new repository's name must not be null.");
return ApiConnection.Post<Repository>(ApiUrls.Repositories(templateOwner, templateRepo), newRepository, AcceptHeaders.TemplatePreview);
}
async Task<Repository> Create(Uri url, string organizationLogin, NewRepository newRepository)
{
try
{
return await ApiConnection.Post<Repository>(url, newRepository, AcceptHeaders.VisibilityPreview).ConfigureAwait(false);
return await ApiConnection.Post<Repository>(url, newRepository, AcceptHeaders.Concat(AcceptHeaders.VisibilityPreview, AcceptHeaders.TemplatePreview)).ConfigureAwait(false);
}
catch (ApiValidationException e)
{
@@ -216,6 +237,7 @@ namespace Octokit
/// <param name="update">New values to update the repository with</param>
/// <returns>The updated <see cref="T:Octokit.Repository"/></returns>
[Preview("nebula")]
[Preview("baptiste")]
[ManualRoute("PATCH", "/repos/{owner}/{repo}")]
public Task<Repository> Edit(string owner, string name, RepositoryUpdate update)
{
@@ -224,7 +246,7 @@ namespace Octokit
Ensure.ArgumentNotNull(update, nameof(update));
Ensure.ArgumentNotNull(update.Name, nameof(update.Name));
return ApiConnection.Patch<Repository>(ApiUrls.Repository(owner, name), update, AcceptHeaders.VisibilityPreview);
return ApiConnection.Patch<Repository>(ApiUrls.Repository(owner, name), update, AcceptHeaders.Concat(AcceptHeaders.VisibilityPreview, AcceptHeaders.TemplatePreview));
}
/// <summary>
@@ -326,6 +348,7 @@ namespace Octokit
/// <exception cref="ApiException">Thrown when a general API error occurs.</exception>
/// <returns>A <see cref="IReadOnlyList{Repository}"/> of <see cref="Repository"/>.</returns>
[Preview("nebula")]
[Preview("baptiste")]
[ManualRoute("GET", "/user/repos")]
public Task<IReadOnlyList<Repository>> GetAllForCurrent()
{
@@ -343,12 +366,13 @@ namespace Octokit
/// <exception cref="ApiException">Thrown when a general API error occurs.</exception>
/// <returns>A <see cref="IReadOnlyList{Repository}"/> of <see cref="Repository"/>.</returns>
[Preview("nebula")]
[Preview("baptiste")]
[ManualRoute("GET", "/user/repos")]
public Task<IReadOnlyList<Repository>> GetAllForCurrent(ApiOptions options)
{
Ensure.ArgumentNotNull(options, nameof(options));
return ApiConnection.GetAll<Repository>(ApiUrls.Repositories(), null, AcceptHeaders.VisibilityPreview, options);
return ApiConnection.GetAll<Repository>(ApiUrls.Repositories(), null, AcceptHeaders.Concat(AcceptHeaders.VisibilityPreview, AcceptHeaders.TemplatePreview), options);
}
/// <summary>
@@ -440,6 +464,7 @@ namespace Octokit
/// <exception cref="ApiException">Thrown when a general API error occurs.</exception>
/// <returns>A <see cref="IReadOnlyList{Repository}"/> of <see cref="Repository"/>.</returns>
[Preview("nebula")]
[Preview("baptiste")]
[ManualRoute("GET", "/orgs/{org}/repos")]
public Task<IReadOnlyList<Repository>> GetAllForOrg(string organization)
{
@@ -459,13 +484,14 @@ namespace Octokit
/// <exception cref="ApiException">Thrown when a general API error occurs.</exception>
/// <returns>A <see cref="IReadOnlyList{Repository}"/> of <see cref="Repository"/>.</returns>
[Preview("nebula")]
[Preview("baptiste")]
[ManualRoute("GET", "/orgs/{org}/repos")]
public Task<IReadOnlyList<Repository>> GetAllForOrg(string organization, ApiOptions options)
{
Ensure.ArgumentNotNullOrEmptyString(organization, nameof(organization));
Ensure.ArgumentNotNull(options, nameof(options));
return ApiConnection.GetAll<Repository>(ApiUrls.OrganizationRepositories(organization), null, AcceptHeaders.VisibilityPreview, options);
return ApiConnection.GetAll<Repository>(ApiUrls.OrganizationRepositories(organization), null, AcceptHeaders.Concat(AcceptHeaders.VisibilityPreview, AcceptHeaders.TemplatePreview), options);
}
/// <summary>
@@ -745,7 +771,7 @@ namespace Octokit
{
Ensure.ArgumentNotNull(options, nameof(options));
var endpoint = ApiUrls.RepositoryTopics(repositoryId);
var data = await ApiConnection.Get<RepositoryTopics>(endpoint,null,AcceptHeaders.RepositoryTopicsPreview).ConfigureAwait(false);
var data = await ApiConnection.Get<RepositoryTopics>(endpoint, null, AcceptHeaders.RepositoryTopicsPreview).ConfigureAwait(false);
return data ?? new RepositoryTopics();
}
@@ -824,7 +850,7 @@ namespace Octokit
Ensure.ArgumentNotNull(topics, nameof(topics));
var endpoint = ApiUrls.RepositoryTopics(owner, name);
var data = await ApiConnection.Put<RepositoryTopics>(endpoint, topics,null, AcceptHeaders.RepositoryTopicsPreview).ConfigureAwait(false);
var data = await ApiConnection.Put<RepositoryTopics>(endpoint, topics, null, AcceptHeaders.RepositoryTopicsPreview).ConfigureAwait(false);
return data ?? new RepositoryTopics();
}

View File

@@ -54,6 +54,8 @@ namespace Octokit
public const string VisibilityPreview = "application/vnd.github.nebula-preview+json";
public const string TemplatePreview = "application/vnd.github.baptiste-preview+json";
/// <summary>
/// Combines multiple preview headers. GitHub API supports Accept header with multiple
/// values separated by comma.

View File

@@ -58,6 +58,15 @@ namespace Octokit
return "users/{0}/repos".FormatUri(login);
}
/// <summary>
/// Returns the <see cref="Uri"/> that create a repository using a template.
/// </summary>
/// <returns></returns>
public static Uri Repositories(string owner, string repo)
{
return "repos/{0}/{1}/generate".FormatUri(owner, repo);
}
/// <summary>
/// Returns the <see cref="Uri"/> that returns all of the repositories for the specified organization in
/// response to a GET request. A POST to this URL creates a new repository for the organization.

View File

@@ -47,6 +47,11 @@ namespace Octokit
/// </summary>
public bool? HasWiki { get; set; }
/// <summary>
/// Either true to make this repo available as a template repository or false to prevent it. Default: false.
/// </summary>
public bool? IsTemplate { get; set; }
/// <summary>
/// Optional. Gets or sets the new repository's optional website.
/// </summary>

View File

@@ -0,0 +1,46 @@
using System.Diagnostics;
using System.Globalization;
namespace Octokit
{
/// <summary>
/// Describes a new repository to create via the <see cref="IRepositoriesClient.Generate"/> method.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class NewRepositoryFromTemplate
{
/// <summary>
/// Creates an object that describes the repository to create on GitHub.
/// </summary>
/// <param name="name">The name of the repository. This is the only required parameter.</param>
public NewRepositoryFromTemplate(string name)
{
Ensure.ArgumentNotNullOrEmptyString(name, nameof(name));
Name = name;
}
/// <summary>
/// Optional. The organization or person who will own the new repository.
/// To create a new repository in an organization, the authenticated user must be a member of the specified organization.
/// </summary>
public string Owner { get; set; }
/// <summary>
/// Required. The name of the new repository.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Optional. A short description of the new repository.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Optional. Either true to create a new private repository or false to create a new public one. Default: false
/// </summary>
public bool Private { get; set; }
internal string DebuggerDisplay => string.Format(CultureInfo.InvariantCulture, "Name: {0} Description: {1}", Name, Description);
}
}