Skip to content

Commit

Permalink
Merge pull request #996 from TattsGroup/protected-branches
Browse files Browse the repository at this point in the history
Add Protected Branches API support
  • Loading branch information
shiftkey committed Dec 20, 2015
2 parents 4c8b7be + a6dd0af commit a3871ef
Show file tree
Hide file tree
Showing 17 changed files with 377 additions and 8 deletions.
10 changes: 10 additions & 0 deletions Octokit.Reactive/Clients/IObservableRepositoriesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,16 @@ public interface IObservableRepositoriesClient
/// <returns>The updated <see cref="T:Octokit.Repository"/></returns>
IObservable<Repository> Edit(string owner, string name, RepositoryUpdate update);

/// <summary>
/// Edit the specified branch with the values given in <paramref name="update"/>
/// </summary>
/// <param name="owner">The owner of the repository</param>
/// <param name="name">The name of the repository</param>
/// <param name="branch">The name of the branch</param>
/// <param name="update">New values to update the branch with</param>
/// <returns>The updated <see cref="T:Octokit.Branch"/></returns>
IObservable<Branch> EditBranch(string owner, string name, string branch, BranchUpdate update);

/// <summary>
/// A client for GitHub's Repo Collaborators.
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions Octokit.Reactive/Clients/ObservableRepositoriesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,19 @@ public IObservable<Repository> Edit(string owner, string name, RepositoryUpdate
return _client.Edit(owner, name, update).ToObservable();
}

/// <summary>
/// Edit the specified branch with the values given in <paramref name="update"/>
/// </summary>
/// <param name="owner">The owner of the repository</param>
/// <param name="name">The name of the repository</param>
/// <param name="branch">The name of the branch</param>
/// <param name="update">New values to update the branch with</param>
/// <returns>The updated <see cref="T:Octokit.Branch"/></returns>
public IObservable<Branch> EditBranch(string owner, string name, string branch, BranchUpdate update)
{
return _client.EditBranch(owner, name, branch, update).ToObservable();
}

/// <summary>
/// Compare two references in a repository
/// </summary>
Expand Down
100 changes: 100 additions & 0 deletions Octokit.Tests.Integration/Clients/BranchesClientTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Octokit;
using Octokit.Tests.Integration;
Expand All @@ -22,7 +23,106 @@ public async Task ReturnsBranches()

Assert.NotEmpty(branches);
Assert.Equal(branches[0].Name, "master");
Assert.NotNull(branches[0].Protection);
}
}
}

public class TheEditBranchesMethod
{
private readonly IRepositoriesClient _fixture;
private readonly RepositoryContext _context;

public TheEditBranchesMethod()
{
var github = Helper.GetAuthenticatedClient();
_context = github.CreateRepositoryContext("source-repo").Result;
_fixture = github.Repository;
}

public async Task CreateTheWorld()
{
// Set master branch to be protected, with some status checks
var requiredStatusChecks = new RequiredStatusChecks(EnforcementLevel.Everyone, new List<string>() { "check1", "check2" });

var update = new BranchUpdate();
update.Protection = new BranchProtection(true, requiredStatusChecks);

var newBranch = await _fixture.EditBranch(_context.Repository.Owner.Login, _context.Repository.Name, "master", update);
}

[IntegrationTest]
public async Task ProtectsBranch()
{
// Set master branch to be protected, with some status checks
var requiredStatusChecks = new RequiredStatusChecks(EnforcementLevel.Everyone, new List<string>() { "check1", "check2", "check3" });

var update = new BranchUpdate();
update.Protection = new BranchProtection(true, requiredStatusChecks);

var branch = await _fixture.EditBranch(_context.Repository.Owner.Login, _context.Repository.Name, "master", update);

// Ensure a branch object was returned
Assert.NotNull(branch);

// Retrieve master branch
branch = await _fixture.GetBranch(_context.Repository.Owner.Login, _context.Repository.Name, "master");

// Assert the changes were made
Assert.Equal(branch.Protection.Enabled, true);
Assert.Equal(branch.Protection.RequiredStatusChecks.EnforcementLevel, EnforcementLevel.Everyone);
Assert.Equal(branch.Protection.RequiredStatusChecks.Contexts.Count, 3);
}

[IntegrationTest]
public async Task RemoveStatusCheckEnforcement()
{
await CreateTheWorld();

// Remove status check enforcement
var requiredStatusChecks = new RequiredStatusChecks(EnforcementLevel.Off, new List<string>() { "check1" });

var update = new BranchUpdate();
update.Protection = new BranchProtection(true, requiredStatusChecks);

var branch = await _fixture.EditBranch(_context.Repository.Owner.Login, _context.Repository.Name, "master", update);

// Ensure a branch object was returned
Assert.NotNull(branch);

// Retrieve master branch
branch = await _fixture.GetBranch(_context.Repository.Owner.Login, _context.Repository.Name, "master");

// Assert the changes were made
Assert.Equal(branch.Protection.Enabled, true);
Assert.Equal(branch.Protection.RequiredStatusChecks.EnforcementLevel, EnforcementLevel.Off);
Assert.Equal(branch.Protection.RequiredStatusChecks.Contexts.Count, 1);
}

[IntegrationTest]
public async Task UnprotectsBranch()
{
await CreateTheWorld();

// Unprotect branch
// Deliberately set Enforcement and Contexts to some values (these should be ignored)
var requiredStatusChecks = new RequiredStatusChecks(EnforcementLevel.Everyone, new List<string>() { "check1" });

var update = new BranchUpdate();
update.Protection = new BranchProtection(false, requiredStatusChecks);

var branch = await _fixture.EditBranch(_context.Repository.Owner.Login, _context.Repository.Name, "master", update);

// Ensure a branch object was returned
Assert.NotNull(branch);

// Retrieve master branch
branch = await _fixture.GetBranch(_context.Repository.Owner.Login, _context.Repository.Name, "master");

// Assert the branch is unprotected, and enforcement/contexts are cleared
Assert.Equal(branch.Protection.Enabled, false);
Assert.Equal(branch.Protection.RequiredStatusChecks.EnforcementLevel, EnforcementLevel.Off);
Assert.Equal(branch.Protection.RequiredStatusChecks.Contexts.Count, 0);
}
}
}
36 changes: 34 additions & 2 deletions Octokit.Tests/Clients/RepositoriesClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ public void ReturnsBranches()
client.GetAllBranches("owner", "name");

connection.Received()
.GetAll<Branch>(Arg.Is<Uri>(u => u.ToString() == "repos/owner/name/branches"));
.GetAll<Branch>(Arg.Is<Uri>(u => u.ToString() == "repos/owner/name/branches"), null, "application/vnd.github.loki-preview+json");
}

[Fact]
Expand Down Expand Up @@ -565,7 +565,7 @@ public void GetsCorrectUrl()
client.GetBranch("owner", "repo", "branch");

connection.Received()
.Get<Branch>(Arg.Is<Uri>(u => u.ToString() == "repos/owner/repo/branches/branch"), null);
.Get<Branch>(Arg.Is<Uri>(u => u.ToString() == "repos/owner/repo/branches/branch"), null, "application/vnd.github.loki-preview+json");
}

[Fact]
Expand Down Expand Up @@ -717,5 +717,37 @@ public void GetsCorrectUrl()
Arg.Any<Dictionary<string, string>>());
}
}

public class TheEditBranchMethod
{
[Fact]
public void GetsCorrectUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new RepositoriesClient(connection);
var update = new BranchUpdate();
const string previewAcceptsHeader = "application/vnd.github.loki-preview+json";

client.EditBranch("owner", "repo", "branch", update);

connection.Received()
.Patch<Branch>(Arg.Is<Uri>(u => u.ToString() == "repos/owner/repo/branches/branch"), Arg.Any<BranchUpdate>(), previewAcceptsHeader);
}

[Fact]
public async Task EnsuresNonNullArguments()
{
var client = new RepositoriesClient(Substitute.For<IApiConnection>());
var update = new BranchUpdate();

await Assert.ThrowsAsync<ArgumentNullException>(() => client.EditBranch(null, "repo", "branch", update));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.EditBranch("owner", null, "branch", update));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.EditBranch("owner", "repo", null, update));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.EditBranch("owner", "repo", "branch", null));
await Assert.ThrowsAsync<ArgumentException>(() => client.EditBranch("", "repo", "branch", update));
await Assert.ThrowsAsync<ArgumentException>(() => client.EditBranch("owner", "", "branch", update));
await Assert.ThrowsAsync<ArgumentException>(() => client.EditBranch("owner", "repo", "", update));
}
}
}
}
33 changes: 33 additions & 0 deletions Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,39 @@ public void CallsIntoClient()
}
}

public class TheEditBranchMethod
{
[Fact]
public async Task EnsuresArguments()
{
var github = Substitute.For<IGitHubClient>();
var nonreactiveClient = new RepositoriesClient(Substitute.For<IApiConnection>());
github.Repository.Returns(nonreactiveClient);
var client = new ObservableRepositoriesClient(github);
var update = new BranchUpdate();

Assert.Throws<ArgumentNullException>(() => client.EditBranch(null, "repo", "branch", update));
Assert.Throws<ArgumentNullException>(() => client.EditBranch("owner", null, "branch", update));
Assert.Throws<ArgumentNullException>(() => client.EditBranch("owner", "repo", null, update));
Assert.Throws<ArgumentNullException>(() => client.EditBranch("owner", "repo", "branch", null));
Assert.Throws<ArgumentException>(() => client.EditBranch("", "repo", "branch", update));
Assert.Throws<ArgumentException>(() => client.EditBranch("owner", "", "branch", update));
Assert.Throws<ArgumentException>(() => client.EditBranch("owner", "repo", "", update));
}

[Fact]
public void CallsIntoClient()
{
var github = Substitute.For<IGitHubClient>();
var client = new ObservableRepositoriesClient(github);
var update = new BranchUpdate();

client.EditBranch("owner", "repo", "branch", update);

github.Repository.Received(1).EditBranch("owner", "repo", "branch", update);
}
}

static IResponse CreateResponseWithApiInfo(IDictionary<string, Uri> links)
{
var response = Substitute.For<IResponse>();
Expand Down
10 changes: 10 additions & 0 deletions Octokit/Clients/IRepositoriesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,5 +327,15 @@ public interface IRepositoriesClient
/// <param name="update">New values to update the repository with</param>
/// <returns>The updated <see cref="T:Octokit.Repository"/></returns>
Task<Repository> Edit(string owner, string name, RepositoryUpdate update);

/// <summary>
/// Edit the specified branch with the values given in <paramref name="update"/>
/// </summary>
/// <param name="owner">The owner of the repository</param>
/// <param name="name">The name of the repository</param>
/// <param name="branch">The name of the branch</param>
/// <param name="update">New values to update the branch with</param>
/// <returns>The updated <see cref="T:Octokit.Branch"/></returns>
Task<Branch> EditBranch(string owner, string name, string branch, BranchUpdate update);
}
}
23 changes: 20 additions & 3 deletions Octokit/Clients/RepositoriesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,24 @@ public Task<Repository> Edit(string owner, string name, RepositoryUpdate update)
return ApiConnection.Patch<Repository>(ApiUrls.Repository(owner, name), update);
}

/// <summary>
/// Edit the specified branch with the values given in <paramref name="update"/>
/// </summary>
/// <param name="owner">The owner of the repository</param>
/// <param name="name">The name of the repository</param>
/// <param name="branch">The name of the branch</param>
/// <param name="update">New values to update the branch with</param>
/// <returns>The updated <see cref="T:Octokit.Branch"/></returns>
public Task<Branch> EditBranch(string owner, string name, string branch, BranchUpdate update)
{
Ensure.ArgumentNotNullOrEmptyString(owner, "owner");
Ensure.ArgumentNotNullOrEmptyString(name, "repositoryName");
Ensure.ArgumentNotNullOrEmptyString(branch, "branchName");
Ensure.ArgumentNotNull(update, "update");

return ApiConnection.Patch<Branch>(ApiUrls.RepoBranch(owner, name, branch), update, AcceptHeaders.ProtectedBranchesApiPreview);
}

/// <summary>
/// Gets the specified repository.
/// </summary>
Expand Down Expand Up @@ -389,8 +407,7 @@ public Task<IReadOnlyList<Branch>> GetAllBranches(string owner, string name)
Ensure.ArgumentNotNullOrEmptyString(owner, "owner");
Ensure.ArgumentNotNullOrEmptyString(name, "name");

var endpoint = ApiUrls.RepoBranches(owner, name);
return ApiConnection.GetAll<Branch>(endpoint);
return ApiConnection.GetAll<Branch>(ApiUrls.RepoBranches(owner, name), null, AcceptHeaders.ProtectedBranchesApiPreview);
}


Expand Down Expand Up @@ -502,7 +519,7 @@ public Task<Branch> GetBranch(string owner, string repositoryName, string branch
Ensure.ArgumentNotNullOrEmptyString(repositoryName, "repositoryName");
Ensure.ArgumentNotNullOrEmptyString(branchName, "branchName");

return ApiConnection.Get<Branch>(ApiUrls.RepoBranch(owner, repositoryName, branchName));
return ApiConnection.Get<Branch>(ApiUrls.RepoBranch(owner, repositoryName, branchName), null, AcceptHeaders.ProtectedBranchesApiPreview);
}
}
}
8 changes: 8 additions & 0 deletions Octokit/Helpers/AcceptHeaders.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Octokit
{
public static class AcceptHeaders
{
public const string ProtectedBranchesApiPreview = "application/vnd.github.loki-preview+json";
}
}

27 changes: 27 additions & 0 deletions Octokit/Models/Request/BranchUpdate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Diagnostics;
using System.Globalization;

namespace Octokit
{
/// <summary>
/// Specifies the values used to update a <see cref="Branch"/>.
/// Note: this is a PREVIEW api: https://developer.github.com/changes/2015-11-11-protected-branches-api/
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class BranchUpdate
{
/// <summary>
/// The <see cref="BranchProtection"/> details
/// </summary>
public BranchProtection Protection { get; set; }

internal string DebuggerDisplay
{
get
{
return String.Format(CultureInfo.InvariantCulture, "Protection: {0}", Protection.DebuggerDisplay);
}
}
}
}
9 changes: 8 additions & 1 deletion Octokit/Models/Response/Branch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,24 @@ public class Branch
{
public Branch() { }

public Branch(string name, GitReference commit)
public Branch(string name, GitReference commit, BranchProtection protection)
{
Name = name;
Commit = commit;
Protection = protection;
}

/// <summary>
/// Name of this <see cref="Branch"/>.
/// </summary>
public string Name { get; protected set; }

/// <summary>
/// The <see cref="BranchProtection"/> details for this <see cref="Branch"/>.
/// Note: this is a PREVIEW api: https://developer.github.com/changes/2015-11-11-protected-branches-api/
/// </summary>
public BranchProtection Protection { get; protected set; }

/// <summary>
/// The <see cref="GitReference"/> history for this <see cref="Branch"/>.
/// </summary>
Expand Down
Loading

0 comments on commit a3871ef

Please sign in to comment.