diff --git a/Octokit.Reactive/Clients/IObservableRepositoryBranchesClient.cs b/Octokit.Reactive/Clients/IObservableRepositoryBranchesClient.cs index 49e0a7c82d..dbf2452d0f 100644 --- a/Octokit.Reactive/Clients/IObservableRepositoryBranchesClient.cs +++ b/Octokit.Reactive/Clients/IObservableRepositoryBranchesClient.cs @@ -92,5 +92,70 @@ public interface IObservableRepositoryBranchesClient /// New values to update the branch with [Obsolete("BranchProtection preview functionality in the GitHub API has had breaking changes. This existing implementation will cease to work when the preview period ends.")] IObservable Edit(int repositoryId, string branch, BranchUpdate update); + + /// + /// Get the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + IObservable GetBranchProtection(string owner, string name, string branch); + + /// + /// Get the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// The name of the branch + IObservable GetBranchProtection(int repositoryId, string branch); + + /// + /// Update the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + /// Branch protection settings + IObservable UpdateBranchProtection(string owner, string name, string branch, BranchProtectionSettingsUpdate update); + + /// + /// Update the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// The name of the branch + /// Branch protection settings + IObservable UpdateBranchProtection(int repositoryId, string branch, BranchProtectionSettingsUpdate update); + + /// + /// Remove the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + IObservable DeleteBranchProtection(string owner, string name, string branch); + + /// + /// Remove the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// The name of the branch + IObservable DeleteBranchProtection(int repositoryId, string branch); } } diff --git a/Octokit.Reactive/Clients/ObservableRepositoryBranchesClient.cs b/Octokit.Reactive/Clients/ObservableRepositoryBranchesClient.cs index 269da55fca..eb200bdb45 100644 --- a/Octokit.Reactive/Clients/ObservableRepositoryBranchesClient.cs +++ b/Octokit.Reactive/Clients/ObservableRepositoryBranchesClient.cs @@ -147,5 +147,108 @@ public IObservable Edit(int repositoryId, string branch, BranchUpdate up return _client.Edit(repositoryId, branch, update).ToObservable(); } + + /// + /// Get the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + public IObservable GetBranchProtection(string owner, string name, string branch) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNullOrEmptyString(branch, "branch"); + + return _client.GetBranchProtection(owner, name, branch).ToObservable(); + } + + /// + /// Get the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// The name of the branch + public IObservable GetBranchProtection(int repositoryId, string branch) + { + Ensure.ArgumentNotNullOrEmptyString(branch, "branch"); + + return _client.GetBranchProtection(repositoryId, branch).ToObservable(); + } + + /// + /// Update the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + /// Branch protection settings + public IObservable UpdateBranchProtection(string owner, string name, string branch, BranchProtectionSettingsUpdate update) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNullOrEmptyString(branch, "branch"); + Ensure.ArgumentNotNull(update, "update"); + + return _client.UpdateBranchProtection(owner, name, branch, update).ToObservable(); + } + + /// + /// Update the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// The name of the branch + /// Branch protection settings + public IObservable UpdateBranchProtection(int repositoryId, string branch, BranchProtectionSettingsUpdate update) + { + Ensure.ArgumentNotNullOrEmptyString(branch, "branch"); + Ensure.ArgumentNotNull(update, "update"); + + return _client.UpdateBranchProtection(repositoryId, branch, update).ToObservable(); + } + + /// + /// Remove the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + public IObservable DeleteBranchProtection(string owner, string name, string branch) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNullOrEmptyString(branch, "branch"); + + return _client.DeleteBranchProtection(owner, name, branch).ToObservable(); + } + + /// + /// Remove the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// The name of the branch + public IObservable DeleteBranchProtection(int repositoryId, string branch) + { + Ensure.ArgumentNotNullOrEmptyString(branch, "branch"); + + return _client.DeleteBranchProtection(repositoryId, branch).ToObservable(); + } } } diff --git a/Octokit.Tests.Integration/Clients/RepositoryBranchesClientTests.cs b/Octokit.Tests.Integration/Clients/RepositoryBranchesClientTests.cs index 5202fe0090..39542e6181 100644 --- a/Octokit.Tests.Integration/Clients/RepositoryBranchesClientTests.cs +++ b/Octokit.Tests.Integration/Clients/RepositoryBranchesClientTests.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Octokit; using Octokit.Tests.Integration; @@ -18,11 +20,12 @@ public async Task GetsAllBranches() Assert.NotEmpty(branches); - // Ensure Protection attribute is deserialized foreach (var branch in branches) { Assert.NotNull(branch.Protection); } + + Assert.True(branches.First(x => x.Name == "master").Protected); } [IntegrationTest] @@ -33,6 +36,13 @@ public async Task GetsAllBranchesWithRepositoryId() var branches = await github.Repository.Branch.GetAll(7528679); Assert.NotEmpty(branches); + + foreach (var branch in branches) + { + Assert.NotNull(branch.Protection); + } + + Assert.True(branches.First(x => x.Name == "master").Protected); } [IntegrationTest] @@ -179,6 +189,8 @@ public async Task GetsABranch() Assert.NotNull(branch); Assert.Equal("master", branch.Name); + Assert.NotNull(branch.Protection); + Assert.True(branch.Protected); } [IntegrationTest] @@ -190,6 +202,9 @@ public async Task GetsABranchWithRepositoryId() Assert.NotNull(branch); Assert.Equal("master", branch.Name); + + Assert.NotNull(branch.Protection); + Assert.True(branch.Protected); } } @@ -290,4 +305,242 @@ public async Task UnprotectsBranch() Assert.Equal(branch.Protection.RequiredStatusChecks.Contexts.Count, 0); } } + + public class TheGetBranchProtectionMethod : IDisposable + { + IRepositoryBranchesClient _client; + RepositoryContext _userRepoContext; + OrganizationRepositoryWithTeamContext _orgRepoContext; + + public TheGetBranchProtectionMethod() + { + var github = Helper.GetAuthenticatedClient(); + _client = github.Repository.Branch; + + _userRepoContext = github.CreateRepositoryWithProtectedBranch().Result; + _orgRepoContext = github.CreateOrganizationRepositoryWithProtectedBranch().Result; + } + + [IntegrationTest] + public async Task GetsBranchProtection() + { + var repoOwner = _userRepoContext.RepositoryOwner; + var repoName = _userRepoContext.RepositoryName; + var protection = await _client.GetBranchProtection(repoOwner, repoName, "master"); + + Assert.True(protection.RequiredStatusChecks.IncludeAdmins); + Assert.True(protection.RequiredStatusChecks.Strict); + Assert.Equal(2, protection.RequiredStatusChecks.Contexts.Count); + + Assert.Null(protection.Restrictions); + } + + [IntegrationTest] + public async Task GetsBranchProtectionWithRepositoryId() + { + var repoId = _userRepoContext.RepositoryId; + var protection = await _client.GetBranchProtection(repoId, "master"); + + Assert.True(protection.RequiredStatusChecks.IncludeAdmins); + Assert.True(protection.RequiredStatusChecks.Strict); + Assert.Equal(2, protection.RequiredStatusChecks.Contexts.Count); + + Assert.Null(protection.Restrictions); + } + + [IntegrationTest] + public async Task GetsBranchProtectionForOrgRepo() + { + var repoOwner = _orgRepoContext.RepositoryContext.RepositoryOwner; + var repoName = _orgRepoContext.RepositoryContext.RepositoryName; + var protection = await _client.GetBranchProtection(repoOwner, repoName, "master"); + + Assert.True(protection.RequiredStatusChecks.IncludeAdmins); + Assert.True(protection.RequiredStatusChecks.Strict); + Assert.Equal(2, protection.RequiredStatusChecks.Contexts.Count); + + Assert.Equal(1, protection.Restrictions.Teams.Count); + Assert.Equal(0, protection.Restrictions.Users.Count); + } + + [IntegrationTest] + public async Task GetsBranchProtectionForOrgRepoWithRepositoryId() + { + var repoId = _orgRepoContext.RepositoryContext.RepositoryId; + var protection = await _client.GetBranchProtection(repoId, "master"); + + Assert.True(protection.RequiredStatusChecks.IncludeAdmins); + Assert.True(protection.RequiredStatusChecks.Strict); + Assert.Equal(2, protection.RequiredStatusChecks.Contexts.Count); + + Assert.Equal(1, protection.Restrictions.Teams.Count); + Assert.Equal(0, protection.Restrictions.Users.Count); + } + + public void Dispose() + { + if (_userRepoContext != null) + _userRepoContext.Dispose(); + + if (_orgRepoContext != null) + _orgRepoContext.Dispose(); + } + } + + public class TheUpdateBranchProtectionMethod : IDisposable + { + IRepositoryBranchesClient _client; + RepositoryContext _userRepoContext; + OrganizationRepositoryWithTeamContext _orgRepoContext; + + public TheUpdateBranchProtectionMethod() + { + var github = Helper.GetAuthenticatedClient(); + _client = github.Repository.Branch; + + _userRepoContext = github.CreateRepositoryWithProtectedBranch().Result; + _orgRepoContext = github.CreateOrganizationRepositoryWithProtectedBranch().Result; + } + + [IntegrationTest] + public async Task UpdatesBranchProtection() + { + var repoOwner = _userRepoContext.RepositoryOwner; + var repoName = _userRepoContext.RepositoryName; + var update = new BranchProtectionSettingsUpdate( + new BranchProtectionRequiredStatusChecksUpdate(false, false, new[] { "new" })); + + var protection = await _client.UpdateBranchProtection(repoOwner, repoName, "master", update); + + Assert.False(protection.RequiredStatusChecks.IncludeAdmins); + Assert.False(protection.RequiredStatusChecks.Strict); + Assert.Equal(1, protection.RequiredStatusChecks.Contexts.Count); + + Assert.Null(protection.Restrictions); + } + + [IntegrationTest] + public async Task UpdatesBranchProtectionWithRepositoryId() + { + var repoId = _userRepoContext.RepositoryId; + var update = new BranchProtectionSettingsUpdate( + new BranchProtectionRequiredStatusChecksUpdate(false, false, new[] { "new" })); + + var protection = await _client.UpdateBranchProtection(repoId, "master", update); + + Assert.False(protection.RequiredStatusChecks.IncludeAdmins); + Assert.False(protection.RequiredStatusChecks.Strict); + Assert.Equal(1, protection.RequiredStatusChecks.Contexts.Count); + + Assert.Null(protection.Restrictions); + } + + [IntegrationTest] + public async Task UpdatesBranchProtectionForOrgRepo() + { + var repoOwner = _orgRepoContext.RepositoryContext.RepositoryOwner; + var repoName = _orgRepoContext.RepositoryContext.RepositoryName; + var update = new BranchProtectionSettingsUpdate( + new BranchProtectionRequiredStatusChecksUpdate(false, false, new[] { "new" }), + new BranchProtectionPushRestrictionsUpdate()); + + var protection = await _client.UpdateBranchProtection(repoOwner, repoName, "master", update); + + Assert.False(protection.RequiredStatusChecks.IncludeAdmins); + Assert.False(protection.RequiredStatusChecks.Strict); + Assert.Equal(1, protection.RequiredStatusChecks.Contexts.Count); + + Assert.Empty(protection.Restrictions.Teams); + Assert.Empty(protection.Restrictions.Users); + } + + [IntegrationTest] + public async Task UpdatesBranchProtectionForOrgRepoWithRepositoryId() + { + var repoId = _orgRepoContext.RepositoryContext.RepositoryId; + var update = new BranchProtectionSettingsUpdate( + new BranchProtectionRequiredStatusChecksUpdate(false, false, new[] { "new" }), + new BranchProtectionPushRestrictionsUpdate()); + + var protection = await _client.UpdateBranchProtection(repoId, "master", update); + + Assert.False(protection.RequiredStatusChecks.IncludeAdmins); + Assert.False(protection.RequiredStatusChecks.Strict); + Assert.Equal(1, protection.RequiredStatusChecks.Contexts.Count); + + Assert.Empty(protection.Restrictions.Teams); + Assert.Empty(protection.Restrictions.Users); + } + + public void Dispose() + { + if (_userRepoContext != null) + _userRepoContext.Dispose(); + + if (_orgRepoContext != null) + _orgRepoContext.Dispose(); + } + } + + public class TheDeleteBranchProtectionMethod + { + IGitHubClient _github; + IRepositoryBranchesClient _client; + + public TheDeleteBranchProtectionMethod() + { + _github = Helper.GetAuthenticatedClient(); + _client = _github.Repository.Branch; + } + + [IntegrationTest] + public async Task DeletesBranchProtection() + { + using (var context = await _github.CreateRepositoryWithProtectedBranch()) + { + var repoOwner = context.RepositoryOwner; + var repoName = context.RepositoryName; + var deleted = await _client.DeleteBranchProtection(repoOwner, repoName, "master"); + + Assert.True(deleted); + } + } + + [IntegrationTest] + public async Task DeletesBranchProtectionWithRepositoryId() + { + using (var context = await _github.CreateRepositoryWithProtectedBranch()) + { + var repoId = context.RepositoryId; + var deleted = await _client.DeleteBranchProtection(repoId, "master"); + + Assert.True(deleted); + } + } + + [IntegrationTest] + public async Task DeletesBranchProtectionForOrgRepo() + { + using (var context = await _github.CreateOrganizationRepositoryWithProtectedBranch()) + { + var repoOwner = context.RepositoryContext.RepositoryOwner; + var repoName = context.RepositoryContext.RepositoryName; + var deleted = await _client.DeleteBranchProtection(repoOwner, repoName, "master"); + + Assert.True(deleted); + } + } + + [IntegrationTest] + public async Task DeletesBranchProtectionForOrgRepoWithRepositoryId() + { + using (var context = await _github.CreateOrganizationRepositoryWithProtectedBranch()) + { + var repoId = context.RepositoryContext.RepositoryId; + var deleted = await _client.DeleteBranchProtection(repoId, "master"); + + Assert.True(deleted); + } + } + } } diff --git a/Octokit.Tests.Integration/Helpers/OrganizationRepositoryWithTeamContext.cs b/Octokit.Tests.Integration/Helpers/OrganizationRepositoryWithTeamContext.cs new file mode 100644 index 0000000000..6760060679 --- /dev/null +++ b/Octokit.Tests.Integration/Helpers/OrganizationRepositoryWithTeamContext.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Octokit.Tests.Integration.Helpers +{ + internal class OrganizationRepositoryWithTeamContext : IDisposable + { + internal RepositoryContext RepositoryContext { get; set; } + internal TeamContext TeamContext { get; set; } + + public void Dispose() + { + if (RepositoryContext != null) + RepositoryContext.Dispose(); + + if (TeamContext != null) + TeamContext.Dispose(); + } + } + + internal static class RepositoryProtectedBranchHelperExtensions + { + internal async static Task CreateRepositoryWithProtectedBranch(this IGitHubClient client) + { + // Create user owned repo + var userRepo = new NewRepository(Helper.MakeNameWithTimestamp("protected-repo")) { AutoInit = true }; + var contextUserRepo = await client.CreateRepositoryContext(userRepo); + + // Protect master branch + var update = new BranchProtectionSettingsUpdate( + new BranchProtectionRequiredStatusChecksUpdate(true, true, new[] { "build", "test" })); + + await client.Repository.Branch.UpdateBranchProtection(contextUserRepo.RepositoryOwner, contextUserRepo.RepositoryName, "master", update); + + return contextUserRepo; + } + + internal async static Task CreateOrganizationRepositoryWithProtectedBranch(this IGitHubClient client) + { + // Create organization owned repo + var orgRepo = new NewRepository(Helper.MakeNameWithTimestamp("protected-org-repo")) { AutoInit = true }; + var contextOrgRepo = await client.CreateRepositoryContext(Helper.Organization, orgRepo); + + // Create team in org + var contextOrgTeam = await client.CreateTeamContext(Helper.Organization, new NewTeam(Helper.MakeNameWithTimestamp("team"))); + + // Grant team push access to repo + await client.Organization.Team.AddRepository( + contextOrgTeam.TeamId, + contextOrgRepo.RepositoryOwner, + contextOrgRepo.RepositoryName, + new RepositoryPermissionRequest(Permission.Push)); + + // Protect master branch + var protection = new BranchProtectionSettingsUpdate( + new BranchProtectionRequiredStatusChecksUpdate(true, true, new[] { "build", "test" }), + new BranchProtectionPushRestrictionsUpdate(new BranchProtectionTeamCollection { contextOrgTeam.TeamName })); + await client.Repository.Branch.UpdateBranchProtection(contextOrgRepo.RepositoryOwner, contextOrgRepo.RepositoryName, "master", protection); + + return new OrganizationRepositoryWithTeamContext + { + RepositoryContext = contextOrgRepo, + TeamContext = contextOrgTeam + }; + } + } +} diff --git a/Octokit.Tests.Integration/Helpers/RepositoryContext.cs b/Octokit.Tests.Integration/Helpers/RepositoryContext.cs index 509cba7a3a..ca0b9f37ad 100644 --- a/Octokit.Tests.Integration/Helpers/RepositoryContext.cs +++ b/Octokit.Tests.Integration/Helpers/RepositoryContext.cs @@ -13,11 +13,13 @@ internal RepositoryContext(IConnection connection, Repository repo) { _connection = connection; Repository = repo; + RepositoryId = repo.Id; RepositoryOwner = repo.Owner.Login; RepositoryName = repo.Name; } private IConnection _connection; + internal int RepositoryId { get; private set; } internal string RepositoryOwner { get; private set; } internal string RepositoryName { get; private set; } diff --git a/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj b/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj index 4c8ae8b5f5..f0176627ab 100644 --- a/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj +++ b/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj @@ -137,6 +137,7 @@ + diff --git a/Octokit.Tests/Clients/RepositoryBranchesClientTests.cs b/Octokit.Tests/Clients/RepositoryBranchesClientTests.cs index f9164c4b34..6a1fea9652 100644 --- a/Octokit.Tests/Clients/RepositoryBranchesClientTests.cs +++ b/Octokit.Tests/Clients/RepositoryBranchesClientTests.cs @@ -201,5 +201,154 @@ public async Task EnsuresNonNullArguments() await Assert.ThrowsAsync(() => client.Edit(1, "", update)); } } + + public class TheGetBranchProtectectionMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var connection = Substitute.For(); + var client = new RepositoryBranchesClient(connection); + const string previewAcceptsHeader = "application/vnd.github.loki-preview+json"; + + client.GetBranchProtection("owner", "repo", "branch"); + + connection.Received() + .Get(Arg.Is(u => u.ToString() == "repos/owner/repo/branches/branch/protection"), null, previewAcceptsHeader); + } + + [Fact] + public void RequestsTheCorrectUrlWithRepositoryId() + { + var connection = Substitute.For(); + var client = new RepositoryBranchesClient(connection); + const string previewAcceptsHeader = "application/vnd.github.loki-preview+json"; + + client.GetBranchProtection(1, "branch"); + + connection.Received() + .Get(Arg.Is(u => u.ToString() == "repositories/1/branches/branch/protection"), null, previewAcceptsHeader); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new RepositoryBranchesClient(Substitute.For()); + + await Assert.ThrowsAsync(() => client.GetBranchProtection(null, "repo", "branch")); + await Assert.ThrowsAsync(() => client.GetBranchProtection("owner", null, "branch")); + await Assert.ThrowsAsync(() => client.GetBranchProtection("owner", "repo", null)); + + await Assert.ThrowsAsync(() => client.GetBranchProtection(1, null)); + + await Assert.ThrowsAsync(() => client.GetBranchProtection("", "repo", "branch")); + await Assert.ThrowsAsync(() => client.GetBranchProtection("owner", "", "branch")); + await Assert.ThrowsAsync(() => client.GetBranchProtection("owner", "repo", "")); + + await Assert.ThrowsAsync(() => client.GetBranchProtection(1, "")); + } + } + + public class TheUpdateBranchProtectionMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var connection = Substitute.For(); + var client = new RepositoryBranchesClient(connection); + var update = new BranchProtectionSettingsUpdate( + new BranchProtectionRequiredStatusChecksUpdate(true, true, new[] { "test" })); + const string previewAcceptsHeader = "application/vnd.github.loki-preview+json"; + + client.UpdateBranchProtection("owner", "repo", "branch", update); + + connection.Received() + .Put(Arg.Is(u => u.ToString() == "repos/owner/repo/branches/branch/protection"), Arg.Any(), null, previewAcceptsHeader); + } + + [Fact] + public void RequestsTheCorrectUrlWithRepositoryId() + { + var connection = Substitute.For(); + var client = new RepositoryBranchesClient(connection); + var update = new BranchProtectionSettingsUpdate( + new BranchProtectionRequiredStatusChecksUpdate(true, true, new[] { "test" })); + const string previewAcceptsHeader = "application/vnd.github.loki-preview+json"; + + client.UpdateBranchProtection(1, "branch", update); + + connection.Received() + .Put(Arg.Is(u => u.ToString() == "repositories/1/branches/branch/protection"), Arg.Any(), null, previewAcceptsHeader); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new RepositoryBranchesClient(Substitute.For()); + var update = new BranchProtectionSettingsUpdate( + new BranchProtectionRequiredStatusChecksUpdate(true, true, new[] { "test" })); + + await Assert.ThrowsAsync(() => client.UpdateBranchProtection(null, "repo", "branch", update)); + await Assert.ThrowsAsync(() => client.UpdateBranchProtection("owner", null, "branch", update)); + await Assert.ThrowsAsync(() => client.UpdateBranchProtection("owner", "repo", null, update)); + await Assert.ThrowsAsync(() => client.UpdateBranchProtection("owner", "repo", "branch", null)); + + await Assert.ThrowsAsync(() => client.UpdateBranchProtection(1, null, update)); + await Assert.ThrowsAsync(() => client.UpdateBranchProtection(1, "branch", null)); + + await Assert.ThrowsAsync(() => client.UpdateBranchProtection("", "repo", "branch", update)); + await Assert.ThrowsAsync(() => client.UpdateBranchProtection("owner", "", "branch", update)); + await Assert.ThrowsAsync(() => client.UpdateBranchProtection("owner", "repo", "", update)); + + await Assert.ThrowsAsync(() => client.UpdateBranchProtection(1, "", update)); + } + } + + public class TheDeleteBranchProtectectionMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var connection = Substitute.For(); + var client = new RepositoryBranchesClient(connection); + const string previewAcceptsHeader = "application/vnd.github.loki-preview+json"; + + client.DeleteBranchProtection("owner", "repo", "branch"); + + connection.Connection.Received() + .Delete(Arg.Is(u => u.ToString() == "repos/owner/repo/branches/branch/protection"), Arg.Any(), previewAcceptsHeader); + } + + [Fact] + public void RequestsTheCorrectUrlWithRepositoryId() + { + var connection = Substitute.For(); + var client = new RepositoryBranchesClient(connection); + const string previewAcceptsHeader = "application/vnd.github.loki-preview+json"; + + client.DeleteBranchProtection(1, "branch"); + + connection.Connection.Received() + .Delete(Arg.Is(u => u.ToString() == "repositories/1/branches/branch/protection"), Arg.Any(), previewAcceptsHeader); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new RepositoryBranchesClient(Substitute.For()); + + await Assert.ThrowsAsync(() => client.DeleteBranchProtection(null, "repo", "branch")); + await Assert.ThrowsAsync(() => client.DeleteBranchProtection("owner", null, "branch")); + await Assert.ThrowsAsync(() => client.DeleteBranchProtection("owner", "repo", null)); + + await Assert.ThrowsAsync(() => client.DeleteBranchProtection(1, null)); + + await Assert.ThrowsAsync(() => client.DeleteBranchProtection("", "repo", "branch")); + await Assert.ThrowsAsync(() => client.DeleteBranchProtection("owner", "", "branch")); + await Assert.ThrowsAsync(() => client.DeleteBranchProtection("owner", "repo", "")); + + await Assert.ThrowsAsync(() => client.DeleteBranchProtection(1, "")); + } + } } } diff --git a/Octokit.Tests/Reactive/ObservableRepositoryBranchesClientTests.cs b/Octokit.Tests/Reactive/ObservableRepositoryBranchesClientTests.cs index 91811fd1b8..5a8e633810 100644 --- a/Octokit.Tests/Reactive/ObservableRepositoryBranchesClientTests.cs +++ b/Octokit.Tests/Reactive/ObservableRepositoryBranchesClientTests.cs @@ -152,7 +152,7 @@ public async Task EnsuresNonNullArguments() public class TheEditMethod { [Fact] - public void PatchsTheCorrectUrl() + public void RequestsTheCorrectUrl() { var github = Substitute.For(); var client = new ObservableRepositoryBranchesClient(github); @@ -164,7 +164,7 @@ public void PatchsTheCorrectUrl() } [Fact] - public void PatchsTheCorrectUrlWithRepositoryId() + public void RequestsTheCorrectUrlWithRepositoryId() { var github = Substitute.For(); var client = new ObservableRepositoryBranchesClient(github); @@ -196,5 +196,148 @@ public async Task EnsuresNonNullArguments() Assert.Throws(() => client.Edit(1, "", update)); } } + + public class TheGetBranchProtectectionMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var gitHubClient = Substitute.For(); + var client = new ObservableRepositoryBranchesClient(gitHubClient); + + client.GetBranchProtection("owner", "repo", "branch"); + + gitHubClient.Repository.Branch.Received() + .GetBranchProtection("owner", "repo", "branch"); + } + + [Fact] + public void RequestsTheCorrectUrlWithRepositoryId() + { + var gitHubClient = Substitute.For(); + var client = new ObservableRepositoryBranchesClient(gitHubClient); + + client.GetBranchProtection(1, "branch"); + + gitHubClient.Repository.Branch.Received() + .GetBranchProtection(1, "branch"); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new ObservableRepositoryBranchesClient(Substitute.For()); + + Assert.Throws(() => client.GetBranchProtection(null, "repo", "branch")); + Assert.Throws(() => client.GetBranchProtection("owner", null, "branch")); + Assert.Throws(() => client.GetBranchProtection("owner", "repo", null)); + + Assert.Throws(() => client.GetBranchProtection(1, null)); + + Assert.Throws(() => client.GetBranchProtection("", "repo", "branch")); + Assert.Throws(() => client.GetBranchProtection("owner", "", "branch")); + Assert.Throws(() => client.GetBranchProtection("owner", "repo", "")); + + Assert.Throws(() => client.GetBranchProtection(1, "")); + } + } + + public class TheUpdateBranchProtectionMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var gitHubClient = Substitute.For(); + var client = new ObservableRepositoryBranchesClient(gitHubClient); + var update = new BranchProtectionSettingsUpdate( + new BranchProtectionRequiredStatusChecksUpdate(true, true, new[] { "test" })); + + client.UpdateBranchProtection("owner", "repo", "branch", update); + + gitHubClient.Repository.Branch.Received() + .UpdateBranchProtection("owner", "repo", "branch", update); + } + + [Fact] + public void RequestsTheCorrectUrlWithRepositoryId() + { + var gitHubClient = Substitute.For(); + var client = new ObservableRepositoryBranchesClient(gitHubClient); + var update = new BranchProtectionSettingsUpdate( + new BranchProtectionRequiredStatusChecksUpdate(true, true, new[] { "test" })); + + client.UpdateBranchProtection(1, "branch", update); + + gitHubClient.Repository.Branch.Received() + .UpdateBranchProtection(1, "branch", update); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new ObservableRepositoryBranchesClient(Substitute.For()); + var update = new BranchProtectionSettingsUpdate( + new BranchProtectionRequiredStatusChecksUpdate(true, true, new[] { "test" })); + + Assert.Throws(() => client.UpdateBranchProtection(null, "repo", "branch", update)); + Assert.Throws(() => client.UpdateBranchProtection("owner", null, "branch", update)); + Assert.Throws(() => client.UpdateBranchProtection("owner", "repo", null, update)); + Assert.Throws(() => client.UpdateBranchProtection("owner", "repo", "branch", null)); + + Assert.Throws(() => client.UpdateBranchProtection(1, null, update)); + Assert.Throws(() => client.UpdateBranchProtection(1, "branch", null)); + + Assert.Throws(() => client.UpdateBranchProtection("", "repo", "branch", update)); + Assert.Throws(() => client.UpdateBranchProtection("owner", "", "branch", update)); + Assert.Throws(() => client.UpdateBranchProtection("owner", "repo", "", update)); + + Assert.Throws(() => client.UpdateBranchProtection(1, "", update)); + } + } + + public class TheDeleteBranchProtectectionMethod + { + [Fact] + public void RequestsTheCorrectUrl() + { + var gitHubClient = Substitute.For(); + var client = new ObservableRepositoryBranchesClient(gitHubClient); + + client.DeleteBranchProtection("owner", "repo", "branch"); + + gitHubClient.Repository.Branch.Received() + .DeleteBranchProtection("owner", "repo", "branch"); + } + + [Fact] + public void RequestsTheCorrectUrlWithRepositoryId() + { + var gitHubClient = Substitute.For(); + var client = new ObservableRepositoryBranchesClient(gitHubClient); + + client.DeleteBranchProtection(1, "branch"); + + gitHubClient.Repository.Branch.Received() + .DeleteBranchProtection(1, "branch"); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var client = new ObservableRepositoryBranchesClient(Substitute.For()); + + Assert.Throws(() => client.DeleteBranchProtection(null, "repo", "branch")); + Assert.Throws(() => client.DeleteBranchProtection("owner", null, "branch")); + Assert.Throws(() => client.DeleteBranchProtection("owner", "repo", null)); + + Assert.Throws(() => client.DeleteBranchProtection(1, null)); + + Assert.Throws(() => client.DeleteBranchProtection("", "repo", "branch")); + Assert.Throws(() => client.DeleteBranchProtection("owner", "", "branch")); + Assert.Throws(() => client.DeleteBranchProtection("owner", "repo", "")); + + Assert.Throws(() => client.DeleteBranchProtection(1, "")); + } + } } } diff --git a/Octokit/Clients/IRepositoryBranchesClient.cs b/Octokit/Clients/IRepositoryBranchesClient.cs index fdccd556e1..1e4fff9e9b 100644 --- a/Octokit/Clients/IRepositoryBranchesClient.cs +++ b/Octokit/Clients/IRepositoryBranchesClient.cs @@ -97,5 +97,70 @@ public interface IRepositoryBranchesClient /// New values to update the branch with [Obsolete("BranchProtection preview functionality in the GitHub API has had breaking changes. This existing implementation will cease to work when the preview period ends.")] Task Edit(int repositoryId, string branch, BranchUpdate update); + + /// + /// Get the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + Task GetBranchProtection(string owner, string name, string branch); + + /// + /// Get the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// The name of the branch + Task GetBranchProtection(int repositoryId, string branch); + + /// + /// Update the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + /// Branch protection settings + Task UpdateBranchProtection(string owner, string name, string branch, BranchProtectionSettingsUpdate update); + + /// + /// Update the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// The name of the branch + /// Branch protection settings + Task UpdateBranchProtection(int repositoryId, string branch, BranchProtectionSettingsUpdate update); + + /// + /// Remove the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + Task DeleteBranchProtection(string owner, string name, string branch); + + /// + /// Remove the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// The name of the branch + Task DeleteBranchProtection(int repositoryId, string branch); } } diff --git a/Octokit/Clients/RepositoryBranchesClient.cs b/Octokit/Clients/RepositoryBranchesClient.cs index acce61afc2..799273b818 100644 --- a/Octokit/Clients/RepositoryBranchesClient.cs +++ b/Octokit/Clients/RepositoryBranchesClient.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Text; +using System.Net; using System.Threading.Tasks; namespace Octokit @@ -151,5 +149,126 @@ public Task Edit(int repositoryId, string branch, BranchUpdate update) return ApiConnection.Patch(ApiUrls.RepoBranch(repositoryId, branch), update, AcceptHeaders.ProtectedBranchesApiPreview); } + + /// + /// Get the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + public Task GetBranchProtection(string owner, string name, string branch) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNullOrEmptyString(branch, "branch"); + + return ApiConnection.Get(ApiUrls.RepoBranchProtection(owner, name, branch), null, AcceptHeaders.ProtectedBranchesApiPreview); + } + + /// + /// Get the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// The name of the branch + public Task GetBranchProtection(int repositoryId, string branch) + { + Ensure.ArgumentNotNullOrEmptyString(branch, "branch"); + + return ApiConnection.Get(ApiUrls.RepoBranchProtection(repositoryId, branch), null, AcceptHeaders.ProtectedBranchesApiPreview); + } + + /// + /// Update the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + /// Branch protection settings + public Task UpdateBranchProtection(string owner, string name, string branch, BranchProtectionSettingsUpdate update) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNullOrEmptyString(branch, "branch"); + Ensure.ArgumentNotNull(update, "update"); + + return ApiConnection.Put(ApiUrls.RepoBranchProtection(owner, name, branch), update, null, AcceptHeaders.ProtectedBranchesApiPreview); + } + + /// + /// Update the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// The name of the branch + /// Branch protection settings + public Task UpdateBranchProtection(int repositoryId, string branch, BranchProtectionSettingsUpdate update) + { + Ensure.ArgumentNotNullOrEmptyString(branch, "branch"); + Ensure.ArgumentNotNull(update, "update"); + + return ApiConnection.Put(ApiUrls.RepoBranchProtection(repositoryId, branch), update, null, AcceptHeaders.ProtectedBranchesApiPreview); + } + + /// + /// Remove the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + public async Task DeleteBranchProtection(string owner, string name, string branch) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNullOrEmptyString(branch, "branch"); + + var endpoint = ApiUrls.RepoBranchProtection(owner, name, branch); + try + { + var httpStatusCode = await Connection.Delete(endpoint, null, AcceptHeaders.ProtectedBranchesApiPreview).ConfigureAwait(false); + return httpStatusCode == HttpStatusCode.NoContent; + } + catch (NotFoundException) + { + return false; + } + } + + /// + /// Remove the branch protection settings for the specified branch /> + /// + /// + /// See the API documentation for more details + /// + /// The Id of the repository + /// The name of the branch + public async Task DeleteBranchProtection(int repositoryId, string branch) + { + Ensure.ArgumentNotNullOrEmptyString(branch, "branch"); + + var endpoint = ApiUrls.RepoBranchProtection(repositoryId, branch); + try + { + var httpStatusCode = await Connection.Delete(endpoint, null, AcceptHeaders.ProtectedBranchesApiPreview).ConfigureAwait(false); + return httpStatusCode == HttpStatusCode.NoContent; + } + catch (NotFoundException) + { + return false; + } + } } } diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index c4d964e849..aa8a98fa11 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -1624,6 +1624,29 @@ public static Uri RepoBranch(string owner, string name, string branchName) return "repos/{0}/{1}/branches/{2}".FormatUri(owner, name, branchName); } + /// + /// Returns the for a repository branches protection. + /// + /// The owner of the repository + /// The name of the repository + /// The name of the branch + /// + public static Uri RepoBranchProtection(string owner, string name, string branchName) + { + return "repos/{0}/{1}/branches/{2}/protection".FormatUri(owner, name, branchName); + } + + /// + /// Returns the for a repository branches protection. + /// + /// The Id of the repository + /// The name of the branch + /// + public static Uri RepoBranchProtection(int repositoryId, string branchName) + { + return "repositories/{0}/branches/{1}/protection".FormatUri(repositoryId, branchName); + } + /// /// Returns the for a repository. /// diff --git a/Octokit/Models/Request/BranchProtectionUpdate.cs b/Octokit/Models/Request/BranchProtectionUpdate.cs new file mode 100644 index 0000000000..d7c6ed6b7c --- /dev/null +++ b/Octokit/Models/Request/BranchProtectionUpdate.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; +using Octokit.Internal; + +namespace Octokit +{ + /// + /// Specifies the requested settings for branch protection + /// + /// + /// Note: this is a PREVIEW api: https://developer.github.com/changes/2016-06-27-protected-branches-api-update/ + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class BranchProtectionSettingsUpdate + { + /// + /// Create a BranchProtection update request + /// + /// Specifies the requested status check settings. Pass null to disable status checks + public BranchProtectionSettingsUpdate(BranchProtectionRequiredStatusChecksUpdate requiredStatusChecks) + { + RequiredStatusChecks = requiredStatusChecks; + Restrictions = null; + } + + /// + /// Create a BranchProtection update request + /// + /// Specifies the requested status check settings. Pass null to disable status checks + /// Specifies the requested push access restrictions (applies only to Organization owned repositories). Pass null to disable push access restrictions + public BranchProtectionSettingsUpdate(BranchProtectionRequiredStatusChecksUpdate requiredStatusChecks, BranchProtectionPushRestrictionsUpdate restrictions) + { + RequiredStatusChecks = requiredStatusChecks; + Restrictions = restrictions; + } + + /// + /// Status check settings for the protected branch + /// + [SerializeNull] + public BranchProtectionRequiredStatusChecksUpdate RequiredStatusChecks { get; protected set; } + + /// + /// Push access restrictions for the protected branch + /// + [SerializeNull] + public BranchProtectionPushRestrictionsUpdate Restrictions { get; protected set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + "StatusChecks: {0} Restrictions: {1}", + RequiredStatusChecks == null ? "disabled" : RequiredStatusChecks.DebuggerDisplay, + Restrictions == null ? "disabled" : Restrictions.DebuggerDisplay); + } + } + } + + /// + /// Specifies settings for status checks which must pass before branches can be merged into the protected branch + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class BranchProtectionRequiredStatusChecksUpdate + { + /// + /// Status check settings for branch protection + /// + /// Enforce required status checks for repository administrators + /// Require branches to be up to date before merging + /// Require status checks to pass before merging + public BranchProtectionRequiredStatusChecksUpdate(bool includeAdmins, bool strict, IReadOnlyList contexts) + { + IncludeAdmins = includeAdmins; + Strict = strict; + Contexts = contexts; + } + + /// + /// Enforce required status checks for repository administrators + /// + public bool IncludeAdmins { get; protected set; } + + /// + /// Require branches to be up to date before merging + /// + public bool Strict { get; protected set; } + + /// + /// Require status checks to pass before merging + /// + public IReadOnlyList Contexts { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, "IncludeAdmins: {0} Strict: {1} Contexts: {2}", IncludeAdmins, Strict, Contexts == null ? "" : String.Join(",", Contexts)); + } + } + } + + /// + /// Specifies teams and/or people allowed to push to the protected branch. Required status checks will still prevent these people from merging if the checks fail + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class BranchProtectionPushRestrictionsUpdate + { + /// + /// Specify only administrators are allowed to push to this branch. Required status checks will still prevent these people from merging if the checks fail + /// + public BranchProtectionPushRestrictionsUpdate() + { + Teams = new BranchProtectionTeamCollection(); + Users = new BranchProtectionUserCollection(); + } + + /// + /// Specify teams (in addition to Administrators) allowed to push to this branch. Required status checks will still prevent these people from merging if the checks fail + /// + /// Teams allowed to push to this branch + public BranchProtectionPushRestrictionsUpdate(BranchProtectionTeamCollection teams) + { + Ensure.ArgumentNotNull(teams, "teams"); + + Teams = teams; + Users = new BranchProtectionUserCollection(); + } + + /// + /// Specify people (in addition to Administrators) allowed to push to this branch. Required status checks will still prevent these people from merging if the checks fail + /// + /// Users allowed to push to this branch + public BranchProtectionPushRestrictionsUpdate(BranchProtectionUserCollection users) + { + Ensure.ArgumentNotNull(users, "users"); + + Teams = new BranchProtectionTeamCollection(); + Users = users; + } + + /// + /// Specify teams and/or people (in addition to Administrators) allowed to push to this branch. Required status checks will still prevent these people from merging if the checks fail + /// + /// Teams allowed to push to this branch + /// Users allowed to push to this branch + public BranchProtectionPushRestrictionsUpdate(BranchProtectionTeamCollection teams, BranchProtectionUserCollection users) + { + Ensure.ArgumentNotNull(teams, "teams"); + Ensure.ArgumentNotNull(users, "users"); + + Teams = teams; + Users = users; + } + + /// + /// Teams allowed to push to this branch + /// + public BranchProtectionTeamCollection Teams { get; private set; } + + /// + /// Users allowed to push to this branch + /// + public BranchProtectionUserCollection Users { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + "Teams: {0} Users: {1}", + Teams == null ? "" : Teams.DebuggerDisplay, + Users == null ? "" : Users.DebuggerDisplay); + } + } + } + + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class BranchProtectionTeamCollection : Collection + { + public BranchProtectionTeamCollection() + { } + + public BranchProtectionTeamCollection(IList list) : base(list) + { } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, String.Join(", ", this)); + } + } + } + + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class BranchProtectionUserCollection : Collection + { + public BranchProtectionUserCollection() + { } + + public BranchProtectionUserCollection(IList list) : base(list) + { } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, String.Join(", ", this)); + } + } + } +} diff --git a/Octokit/Models/Request/BranchUpdate.cs b/Octokit/Models/Request/BranchUpdate.cs index 7afca66af1..776e594721 100644 --- a/Octokit/Models/Request/BranchUpdate.cs +++ b/Octokit/Models/Request/BranchUpdate.cs @@ -6,8 +6,10 @@ namespace Octokit { /// /// Specifies the values used to update a . - /// Note: this is a PREVIEW api: https://developer.github.com/changes/2015-11-11-protected-branches-api/ /// + /// + /// Note: this is a PREVIEW api: https://developer.github.com/changes/2015-11-11-protected-branches-api/ + /// [DebuggerDisplay("{DebuggerDisplay,nq}")] [Obsolete("BranchProtection preview functionality in the GitHub API has had breaking changes. This existing implementation will cease to work when the preview period ends.")] public class BranchUpdate @@ -21,7 +23,7 @@ internal string DebuggerDisplay { get { - return String.Format(CultureInfo.InvariantCulture, "Protection: {0}", Protection.DebuggerDisplay); + return string.Format(CultureInfo.InvariantCulture, "Protection: {0}", Protection.DebuggerDisplay); } } } diff --git a/Octokit/Models/Response/Branch.cs b/Octokit/Models/Response/Branch.cs index 6317fd56f6..432068224b 100644 --- a/Octokit/Models/Response/Branch.cs +++ b/Octokit/Models/Response/Branch.cs @@ -9,11 +9,24 @@ public class Branch { public Branch() { } -#pragma warning disable CS0618 // Type or member is obsolete - public Branch(string name, GitReference commit, BranchProtection protection) + public Branch(string name, GitReference commit, bool @protected) { Name = name; Commit = commit; + Protected = @protected; + } + +#pragma warning disable CS0618 // Type or member is obsolete + public Branch(string name, GitReference commit, BranchProtection protection) + : this(name, commit, protection, false) + { + } +#pragma warning restore CS0618 // Type or member is obsolete + +#pragma warning disable CS0618 // Type or member is obsolete + public Branch(string name, GitReference commit, BranchProtection protection, bool @protected) + : this(name, commit, @protected) + { Protection = protection; } #pragma warning restore CS0618 // Type or member is obsolete @@ -25,11 +38,15 @@ public Branch(string name, GitReference commit, BranchProtection protection) /// /// The details for this . - /// Note: this is a PREVIEW api: https://developer.github.com/changes/2015-11-11-protected-branches-api/ /// [Obsolete("BranchProtection preview functionality in the GitHub API has had breaking changes. This existing implementation will cease to work when the preview period ends.", false)] public BranchProtection Protection { get; protected set; } + /// + /// Whether this is protected. + /// + public bool Protected { get; protected set; } + /// /// The history for this . /// diff --git a/Octokit/Models/Response/BranchProtection.cs b/Octokit/Models/Response/BranchProtection.cs index 454f0bfb6e..06b25991ec 100644 --- a/Octokit/Models/Response/BranchProtection.cs +++ b/Octokit/Models/Response/BranchProtection.cs @@ -9,8 +9,10 @@ namespace Octokit { /// /// Protection details for a . - /// Note: this is a PREVIEW api: https://developer.github.com/changes/2015-11-11-protected-branches-api/ /// + /// + /// Note: this is a PREVIEW api: https://developer.github.com/changes/2015-11-11-protected-branches-api/ + /// [DebuggerDisplay("{DebuggerDisplay,nq}")] [Obsolete("BranchProtection preview functionality in the GitHub API has had breaking changes. This existing implementation will cease to work when the preview period ends.")] public class BranchProtection @@ -37,7 +39,7 @@ internal string DebuggerDisplay { get { - return String.Format(CultureInfo.InvariantCulture, "Enabled: {0}", Enabled); + return string.Format(CultureInfo.InvariantCulture, "Enabled: {0}", Enabled); } } } @@ -68,7 +70,7 @@ internal string DebuggerDisplay { get { - return String.Format(CultureInfo.InvariantCulture, "EnforcementLevel: {0} Contexts: {1}", EnforcementLevel.ToString(), Contexts.Count); + return string.Format(CultureInfo.InvariantCulture, "EnforcementLevel: {0} Contexts: {1}", EnforcementLevel.ToString(), Contexts.Count); } } } @@ -94,4 +96,122 @@ public enum EnforcementLevel /// Everyone } + + /// + /// Protection details for a . + /// + /// + /// Note: this is a PREVIEW api: https://developer.github.com/changes/2016-06-27-protected-branches-api-update/ + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class BranchProtectionSettings + { + public BranchProtectionSettings() { } + + public BranchProtectionSettings(BranchProtectionRequiredStatusChecks requiredStatusChecks, BranchProtectionPushRestrictions restrictions) + { + RequiredStatusChecks = requiredStatusChecks; + Restrictions = restrictions; + } + + /// + /// Status check settings for the protected branch + /// + public BranchProtectionRequiredStatusChecks RequiredStatusChecks { get; protected set; } + + /// + /// Push access restrictions for the protected branch + /// + public BranchProtectionPushRestrictions Restrictions { get; protected set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + "StatusChecks: {0} Restrictions: {1}", + RequiredStatusChecks == null ? "disabled" : RequiredStatusChecks.DebuggerDisplay, + Restrictions == null ? "disabled" : Restrictions.DebuggerDisplay); + } + } + } + + /// + /// Specifies settings for status checks which must pass before branches can be merged into the protected branch + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class BranchProtectionRequiredStatusChecks + { + public BranchProtectionRequiredStatusChecks() { } + + public BranchProtectionRequiredStatusChecks(bool includeAdmins, bool strict, IReadOnlyList contexts) + { + IncludeAdmins = includeAdmins; + Strict = strict; + Contexts = contexts; + } + + /// + /// Enforce required status checks for repository administrators + /// + public bool IncludeAdmins { get; protected set; } + + /// + /// Require branches to be up to date before merging + /// + public bool Strict { get; protected set; } + + /// + /// Require status checks to pass before merging + /// + public IReadOnlyList Contexts { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + "IncludeAdmins: {0} Strict: {1} Contexts: {2}", + IncludeAdmins, + Strict, + Contexts == null ? "" : String.Join(",", Contexts)); + } + } + } + + /// + /// Specifies people or teams allowed to push to the protected branch. Required status checks will still prevent these people from merging if the checks fail + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class BranchProtectionPushRestrictions + { + public BranchProtectionPushRestrictions() { } + + public BranchProtectionPushRestrictions(IReadOnlyList teams, IReadOnlyList users) + { + Teams = teams; + Users = users; + } + + /// + /// Push access is restricted to the specified Teams + /// + public IReadOnlyList Teams { get; private set; } + + /// + /// Push access is restricted to the specified Users + /// + public IReadOnlyList Users { get; private set; } + + internal string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + "Teams: {0} Users: {1}", + Teams == null ? "" : String.Join(",", Teams), + Users == null ? "" : String.Join(",", Users)); + } + } + } } diff --git a/Octokit/Octokit-Mono.csproj b/Octokit/Octokit-Mono.csproj index 06fe2de9e0..10f624f666 100644 --- a/Octokit/Octokit-Mono.csproj +++ b/Octokit/Octokit-Mono.csproj @@ -491,6 +491,7 @@ + \ No newline at end of file diff --git a/Octokit/Octokit-MonoAndroid.csproj b/Octokit/Octokit-MonoAndroid.csproj index 2f1544ef68..7dc3e9b2d8 100644 --- a/Octokit/Octokit-MonoAndroid.csproj +++ b/Octokit/Octokit-MonoAndroid.csproj @@ -502,6 +502,7 @@ + \ No newline at end of file diff --git a/Octokit/Octokit-Monotouch.csproj b/Octokit/Octokit-Monotouch.csproj index d78c107573..cfc5f43da8 100644 --- a/Octokit/Octokit-Monotouch.csproj +++ b/Octokit/Octokit-Monotouch.csproj @@ -498,6 +498,7 @@ + diff --git a/Octokit/Octokit-Portable.csproj b/Octokit/Octokit-Portable.csproj index 17048e23bc..04a7fbc1eb 100644 --- a/Octokit/Octokit-Portable.csproj +++ b/Octokit/Octokit-Portable.csproj @@ -488,6 +488,7 @@ + diff --git a/Octokit/Octokit-netcore45.csproj b/Octokit/Octokit-netcore45.csproj index 6529d0aced..1d515df112 100644 --- a/Octokit/Octokit-netcore45.csproj +++ b/Octokit/Octokit-netcore45.csproj @@ -185,6 +185,7 @@ + diff --git a/Octokit/Octokit.csproj b/Octokit/Octokit.csproj index fd8d6e446f..9f5f976109 100644 --- a/Octokit/Octokit.csproj +++ b/Octokit/Octokit.csproj @@ -136,6 +136,7 @@ +