Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add branch protection option to block merge on requested changes. #9592

Merged
merged 6 commits into from
Jan 3, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion models/branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
davidsvantesson marked this conversation as resolved.
Show resolved Hide resolved
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions modules/auth/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ type ProtectBranchForm struct {
EnableApprovalsWhitelist bool
ApprovalsWhitelistUsers string
ApprovalsWhitelistTeams string
BlockOnRejectedReviews bool
}

// Validate validates the fields
Expand Down
3 changes: 3 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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 enough approvals but is blocked by changes requested by an official reviewer."
davidsvantesson marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down Expand Up @@ -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 rejcted reviews
davidsvantesson marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down
7 changes: 7 additions & 0 deletions routers/private/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
})
return
}
if !protectBranch.MergeBlockedByRejectedReview(pr) {
davidsvantesson marked this conversation as resolved.
Show resolved Hide resolved
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{}{
Expand Down
3 changes: 2 additions & 1 deletion routers/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions routers/repo/setting_protected_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions templates/repo/issue/view_content/pull.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand Down
7 changes: 7 additions & 0 deletions templates/repo/settings/protected_branch.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down