Add branch protection option to block merge on requested changes. (#9592)
* Add branch protection option to block merge on requested changes. * Add migration step * Fix check to correct negation * Apply suggestions from code review Language improvement. Co-Authored-By: John Olheiser <42128690+jolheiser@users.noreply.github.com> * Copyright year. Co-authored-by: John Olheiser <42128690+jolheiser@users.noreply.github.com> Co-authored-by: Lauris BH <lauris@nix.lv>
This commit is contained in:
		
							parent
							
								
									b39fab41c8
								
							
						
					
					
						commit
						ea707f5a77
					
				
					 10 changed files with 65 additions and 2 deletions
				
			
		| 
						 | 
				
			
			@ -44,6 +44,7 @@ type ProtectedBranch struct {
 | 
			
		|||
	ApprovalsWhitelistUserIDs []int64            `xorm:"JSON TEXT"`
 | 
			
		||||
	ApprovalsWhitelistTeamIDs []int64            `xorm:"JSON TEXT"`
 | 
			
		||||
	RequiredApprovals         int64              `xorm:"NOT NULL DEFAULT 0"`
 | 
			
		||||
	BlockOnRejectedReviews    bool               `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
	CreatedUnix               timeutil.TimeStamp `xorm:"created"`
 | 
			
		||||
	UpdatedUnix               timeutil.TimeStamp `xorm:"updated"`
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -166,6 +167,23 @@ func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest)
 | 
			
		|||
	return approvals
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews
 | 
			
		||||
func (protectBranch *ProtectedBranch) MergeBlockedByRejectedReview(pr *PullRequest) bool {
 | 
			
		||||
	if !protectBranch.BlockOnRejectedReviews {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	rejectExist, err := x.Where("issue_id = ?", pr.IssueID).
 | 
			
		||||
		And("type = ?", ReviewTypeReject).
 | 
			
		||||
		And("official = ?", true).
 | 
			
		||||
		Exist(new(Review))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("MergeBlockedByRejectedReview: %v", err)
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return rejectExist
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetProtectedBranchByRepoID getting protected branch by repo ID
 | 
			
		||||
func GetProtectedBranchByRepoID(repoID int64) ([]*ProtectedBranch, error) {
 | 
			
		||||
	protectedBranches := make([]*ProtectedBranch, 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -340,7 +358,7 @@ func (repo *Repository) IsProtectedBranchForMerging(pr *PullRequest, branchName
 | 
			
		|||
	if err != nil {
 | 
			
		||||
		return true, err
 | 
			
		||||
	} else if has {
 | 
			
		||||
		return !protectedBranch.CanUserMerge(doer.ID) || !protectedBranch.HasEnoughApprovals(pr), nil
 | 
			
		||||
		return !protectedBranch.CanUserMerge(doer.ID) || !protectedBranch.HasEnoughApprovals(pr) || protectedBranch.MergeBlockedByRejectedReview(pr), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false, nil
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -288,6 +288,8 @@ var migrations = []Migration{
 | 
			
		|||
	NewMigration("add user_id prefix to existing user avatar name", renameExistingUserAvatarName),
 | 
			
		||||
	// v116 -> v117
 | 
			
		||||
	NewMigration("Extend TrackedTimes", extendTrackedTimes),
 | 
			
		||||
	// v117 -> v118
 | 
			
		||||
	NewMigration("Add block on rejected reviews branch protection", addBlockOnRejectedReviews),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Migrate database to current version
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										17
									
								
								models/migrations/v117.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								models/migrations/v117.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
// Copyright 2020 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package migrations
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"xorm.io/xorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func addBlockOnRejectedReviews(x *xorm.Engine) error {
 | 
			
		||||
	type ProtectedBranch struct {
 | 
			
		||||
		BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return x.Sync2(new(ProtectedBranch))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -171,6 +171,7 @@ type ProtectBranchForm struct {
 | 
			
		|||
	EnableApprovalsWhitelist bool
 | 
			
		||||
	ApprovalsWhitelistUsers  string
 | 
			
		||||
	ApprovalsWhitelistTeams  string
 | 
			
		||||
	BlockOnRejectedReviews   bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Validate validates the fields
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1054,6 +1054,7 @@ pulls.is_checking = "Merge conflict checking is in progress. Try again in few mo
 | 
			
		|||
pulls.required_status_check_failed = Some required checks were not successful.
 | 
			
		||||
pulls.required_status_check_administrator = As an administrator, you may still merge this pull request.
 | 
			
		||||
pulls.blocked_by_approvals = "This Pull Request doesn't have enough approvals yet. %d of %d approvals granted."
 | 
			
		||||
pulls.blocked_by_rejection = "This Pull Request has changes requested by an official reviewer."
 | 
			
		||||
pulls.can_auto_merge_desc = This pull request can be merged automatically.
 | 
			
		||||
pulls.cannot_auto_merge_desc = This pull request cannot be merged automatically due to conflicts.
 | 
			
		||||
pulls.cannot_auto_merge_helper = Merge manually to resolve the conflicts.
 | 
			
		||||
| 
						 | 
				
			
			@ -1417,6 +1418,8 @@ settings.update_protect_branch_success = Branch protection for branch '%s' has b
 | 
			
		|||
settings.remove_protected_branch_success = Branch protection for branch '%s' has been disabled.
 | 
			
		||||
settings.protected_branch_deletion = Disable Branch Protection
 | 
			
		||||
settings.protected_branch_deletion_desc = Disabling branch protection allows users with write permission to push to the branch. Continue?
 | 
			
		||||
settings.block_rejected_reviews = Block merge on rejected reviews
 | 
			
		||||
settings.block_rejected_reviews_desc = Merging will not be possible when changes are requested by official reviewers, even if there are enough approvals.
 | 
			
		||||
settings.default_branch_desc = Select a default repository branch for pull requests and code commits:
 | 
			
		||||
settings.choose_branch = Choose a branch…
 | 
			
		||||
settings.no_protected_branch = There are no protected branches.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -113,6 +113,13 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
 | 
			
		|||
					})
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				if protectBranch.MergeBlockedByRejectedReview(pr) {
 | 
			
		||||
					log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v and pr #%d has requested changes", opts.UserID, branchName, repo, pr.Index)
 | 
			
		||||
					ctx.JSON(http.StatusForbidden, map[string]interface{}{
 | 
			
		||||
						"err": fmt.Sprintf("protected branch %s can not be pushed to and pr #%d has requested changes", branchName, opts.ProtectedBranchID),
 | 
			
		||||
					})
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
			} else if !canPush {
 | 
			
		||||
				log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v", opts.UserID, branchName, repo)
 | 
			
		||||
				ctx.JSON(http.StatusForbidden, map[string]interface{}{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -962,7 +962,8 @@ func ViewIssue(ctx *context.Context) {
 | 
			
		|||
		}
 | 
			
		||||
		if pull.ProtectedBranch != nil {
 | 
			
		||||
			cnt := pull.ProtectedBranch.GetGrantedApprovalsCount(pull)
 | 
			
		||||
			ctx.Data["IsBlockedByApprovals"] = pull.ProtectedBranch.RequiredApprovals > 0 && cnt < pull.ProtectedBranch.RequiredApprovals
 | 
			
		||||
			ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull)
 | 
			
		||||
			ctx.Data["IsBlockedByRejection"] = pull.ProtectedBranch.MergeBlockedByRejectedReview(pull)
 | 
			
		||||
			ctx.Data["GrantedApprovals"] = cnt
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Data["IsPullBranchDeletable"] = canDelete && pull.HeadRepo != nil && git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -244,6 +244,7 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
 | 
			
		|||
				approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews
 | 
			
		||||
 | 
			
		||||
		err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
 | 
			
		||||
			UserIDs:          whitelistUsers,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,6 +41,7 @@
 | 
			
		|||
	{{else if .IsFilesConflicted}}grey
 | 
			
		||||
	{{else if .IsPullRequestBroken}}red
 | 
			
		||||
	{{else if .IsBlockedByApprovals}}red
 | 
			
		||||
	{{else if .IsBlockedByRejection}}red
 | 
			
		||||
	{{else if and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}red
 | 
			
		||||
	{{else if .Issue.PullRequest.IsChecking}}yellow
 | 
			
		||||
	{{else if .Issue.PullRequest.CanAutoMerge}}green
 | 
			
		||||
| 
						 | 
				
			
			@ -100,6 +101,11 @@
 | 
			
		|||
					<span class="octicon octicon-x"></span>
 | 
			
		||||
				{{$.i18n.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .Issue.PullRequest.ProtectedBranch.RequiredApprovals}}
 | 
			
		||||
				</div>
 | 
			
		||||
			{{else if .IsBlockedByRejection}}
 | 
			
		||||
				<div class="item text red">
 | 
			
		||||
					<span class="octicon octicon-x"></span>
 | 
			
		||||
				{{$.i18n.Tr "repo.pulls.blocked_by_rejection"}}
 | 
			
		||||
				</div>			
 | 
			
		||||
			{{else if .Issue.PullRequest.IsChecking}}
 | 
			
		||||
				<div class="item text yellow">
 | 
			
		||||
					<span class="octicon octicon-sync"></span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -204,6 +204,13 @@
 | 
			
		|||
							</div>
 | 
			
		||||
						{{end}}
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="field">
 | 
			
		||||
						<div class="ui checkbox">
 | 
			
		||||
							<input name="block_on_rejected_reviews" type="checkbox" {{if .Branch.BlockOnRejectedReviews}}checked{{end}}>
 | 
			
		||||
							<label for="block_on_rejected_reviews">{{.i18n.Tr "repo.settings.block_rejected_reviews"}}</label>
 | 
			
		||||
							<p class="help">{{.i18n.Tr "repo.settings.block_rejected_reviews_desc"}}</p>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>					
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div class="ui divider"></div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue