Skip to content

Commit ea707f5

Browse files
davidsvantessonjolheiserlafriks
committed
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 <[email protected]> * Copyright year. Co-authored-by: John Olheiser <[email protected]> Co-authored-by: Lauris BH <[email protected]>
1 parent b39fab4 commit ea707f5

File tree

10 files changed

+65
-2
lines changed

10 files changed

+65
-2
lines changed

models/branches.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type ProtectedBranch struct {
4444
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
4545
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
4646
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
47+
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
4748
CreatedUnix timeutil.TimeStamp `xorm:"created"`
4849
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
4950
}
@@ -166,6 +167,23 @@ func (protectBranch *ProtectedBranch) GetGrantedApprovalsCount(pr *PullRequest)
166167
return approvals
167168
}
168169

170+
// MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews
171+
func (protectBranch *ProtectedBranch) MergeBlockedByRejectedReview(pr *PullRequest) bool {
172+
if !protectBranch.BlockOnRejectedReviews {
173+
return false
174+
}
175+
rejectExist, err := x.Where("issue_id = ?", pr.IssueID).
176+
And("type = ?", ReviewTypeReject).
177+
And("official = ?", true).
178+
Exist(new(Review))
179+
if err != nil {
180+
log.Error("MergeBlockedByRejectedReview: %v", err)
181+
return true
182+
}
183+
184+
return rejectExist
185+
}
186+
169187
// GetProtectedBranchByRepoID getting protected branch by repo ID
170188
func GetProtectedBranchByRepoID(repoID int64) ([]*ProtectedBranch, error) {
171189
protectedBranches := make([]*ProtectedBranch, 0)
@@ -340,7 +358,7 @@ func (repo *Repository) IsProtectedBranchForMerging(pr *PullRequest, branchName
340358
if err != nil {
341359
return true, err
342360
} else if has {
343-
return !protectedBranch.CanUserMerge(doer.ID) || !protectedBranch.HasEnoughApprovals(pr), nil
361+
return !protectedBranch.CanUserMerge(doer.ID) || !protectedBranch.HasEnoughApprovals(pr) || protectedBranch.MergeBlockedByRejectedReview(pr), nil
344362
}
345363

346364
return false, nil

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,8 @@ var migrations = []Migration{
288288
NewMigration("add user_id prefix to existing user avatar name", renameExistingUserAvatarName),
289289
// v116 -> v117
290290
NewMigration("Extend TrackedTimes", extendTrackedTimes),
291+
// v117 -> v118
292+
NewMigration("Add block on rejected reviews branch protection", addBlockOnRejectedReviews),
291293
}
292294

293295
// Migrate database to current version

models/migrations/v117.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"xorm.io/xorm"
9+
)
10+
11+
func addBlockOnRejectedReviews(x *xorm.Engine) error {
12+
type ProtectedBranch struct {
13+
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
14+
}
15+
16+
return x.Sync2(new(ProtectedBranch))
17+
}

modules/auth/repo_form.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ type ProtectBranchForm struct {
171171
EnableApprovalsWhitelist bool
172172
ApprovalsWhitelistUsers string
173173
ApprovalsWhitelistTeams string
174+
BlockOnRejectedReviews bool
174175
}
175176

176177
// Validate validates the fields

options/locale/locale_en-US.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,7 @@ pulls.is_checking = "Merge conflict checking is in progress. Try again in few mo
10541054
pulls.required_status_check_failed = Some required checks were not successful.
10551055
pulls.required_status_check_administrator = As an administrator, you may still merge this pull request.
10561056
pulls.blocked_by_approvals = "This Pull Request doesn't have enough approvals yet. %d of %d approvals granted."
1057+
pulls.blocked_by_rejection = "This Pull Request has changes requested by an official reviewer."
10571058
pulls.can_auto_merge_desc = This pull request can be merged automatically.
10581059
pulls.cannot_auto_merge_desc = This pull request cannot be merged automatically due to conflicts.
10591060
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
14171418
settings.remove_protected_branch_success = Branch protection for branch '%s' has been disabled.
14181419
settings.protected_branch_deletion = Disable Branch Protection
14191420
settings.protected_branch_deletion_desc = Disabling branch protection allows users with write permission to push to the branch. Continue?
1421+
settings.block_rejected_reviews = Block merge on rejected reviews
1422+
settings.block_rejected_reviews_desc = Merging will not be possible when changes are requested by official reviewers, even if there are enough approvals.
14201423
settings.default_branch_desc = Select a default repository branch for pull requests and code commits:
14211424
settings.choose_branch = Choose a branch…
14221425
settings.no_protected_branch = There are no protected branches.

routers/private/hook.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
113113
})
114114
return
115115
}
116+
if protectBranch.MergeBlockedByRejectedReview(pr) {
117+
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)
118+
ctx.JSON(http.StatusForbidden, map[string]interface{}{
119+
"err": fmt.Sprintf("protected branch %s can not be pushed to and pr #%d has requested changes", branchName, opts.ProtectedBranchID),
120+
})
121+
return
122+
}
116123
} else if !canPush {
117124
log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v", opts.UserID, branchName, repo)
118125
ctx.JSON(http.StatusForbidden, map[string]interface{}{

routers/repo/issue.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,8 @@ func ViewIssue(ctx *context.Context) {
962962
}
963963
if pull.ProtectedBranch != nil {
964964
cnt := pull.ProtectedBranch.GetGrantedApprovalsCount(pull)
965-
ctx.Data["IsBlockedByApprovals"] = pull.ProtectedBranch.RequiredApprovals > 0 && cnt < pull.ProtectedBranch.RequiredApprovals
965+
ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull)
966+
ctx.Data["IsBlockedByRejection"] = pull.ProtectedBranch.MergeBlockedByRejectedReview(pull)
966967
ctx.Data["GrantedApprovals"] = cnt
967968
}
968969
ctx.Data["IsPullBranchDeletable"] = canDelete && pull.HeadRepo != nil && git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch)

routers/repo/setting_protected_branch.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
244244
approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
245245
}
246246
}
247+
protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews
247248

248249
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
249250
UserIDs: whitelistUsers,

templates/repo/issue/view_content/pull.tmpl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
{{else if .IsFilesConflicted}}grey
4242
{{else if .IsPullRequestBroken}}red
4343
{{else if .IsBlockedByApprovals}}red
44+
{{else if .IsBlockedByRejection}}red
4445
{{else if and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}red
4546
{{else if .Issue.PullRequest.IsChecking}}yellow
4647
{{else if .Issue.PullRequest.CanAutoMerge}}green
@@ -100,6 +101,11 @@
100101
<span class="octicon octicon-x"></span>
101102
{{$.i18n.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .Issue.PullRequest.ProtectedBranch.RequiredApprovals}}
102103
</div>
104+
{{else if .IsBlockedByRejection}}
105+
<div class="item text red">
106+
<span class="octicon octicon-x"></span>
107+
{{$.i18n.Tr "repo.pulls.blocked_by_rejection"}}
108+
</div>
103109
{{else if .Issue.PullRequest.IsChecking}}
104110
<div class="item text yellow">
105111
<span class="octicon octicon-sync"></span>

templates/repo/settings/protected_branch.tmpl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,13 @@
204204
</div>
205205
{{end}}
206206
</div>
207+
<div class="field">
208+
<div class="ui checkbox">
209+
<input name="block_on_rejected_reviews" type="checkbox" {{if .Branch.BlockOnRejectedReviews}}checked{{end}}>
210+
<label for="block_on_rejected_reviews">{{.i18n.Tr "repo.settings.block_rejected_reviews"}}</label>
211+
<p class="help">{{.i18n.Tr "repo.settings.block_rejected_reviews_desc"}}</p>
212+
</div>
213+
</div>
207214
</div>
208215

209216
<div class="ui divider"></div>

0 commit comments

Comments
 (0)