Skip to content

Commit c52d48a

Browse files
6543guillep2klafriks
authored
Prevent merge of outdated PRs on protected branches (#11012)
* Block PR on Outdated Branch * finalize * cleanup * fix typo and sentences thanks @guillep2k Co-Authored-By: guillep2k <[email protected]> Co-authored-by: guillep2k <[email protected]> Co-authored-by: Lauris BH <[email protected]>
1 parent 2cb5878 commit c52d48a

File tree

15 files changed

+81
-5
lines changed

15 files changed

+81
-5
lines changed

models/branches.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type ProtectedBranch struct {
4747
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
4848
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
4949
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
50+
BlockOnOutdatedBranch bool `xorm:"NOT NULL DEFAULT false"`
5051
DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
5152
RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"`
5253
ProtectedFilePatterns string `xorm:"TEXT"`
@@ -194,6 +195,11 @@ func (protectBranch *ProtectedBranch) MergeBlockedByRejectedReview(pr *PullReque
194195
return rejectExist
195196
}
196197

198+
// MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch
199+
func (protectBranch *ProtectedBranch) MergeBlockedByOutdatedBranch(pr *PullRequest) bool {
200+
return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
201+
}
202+
197203
// GetProtectedFilePatterns parses a semicolon separated list of protected file patterns and returns a glob.Glob slice
198204
func (protectBranch *ProtectedBranch) GetProtectedFilePatterns() []glob.Glob {
199205
extarr := make([]glob.Glob, 0, 10)

models/migrations/migrations.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,12 @@ var migrations = []Migration{
202202
NewMigration("Add EmailHash Table", addEmailHashTable),
203203
// v134 -> v135
204204
NewMigration("Refix merge base for merged pull requests", refixMergeBase),
205-
// v135 -> 136
205+
// v135 -> v136
206206
NewMigration("Add OrgID column to Labels table", addOrgIDLabelColumn),
207-
// v136 -> 137
207+
// v136 -> v137
208208
NewMigration("Add CommitsAhead and CommitsBehind Column to PullRequest Table", addCommitDivergenceToPulls),
209+
// v137 -> v138
210+
NewMigration("Add Branch Protection Block Outdated Branch", addBlockOnOutdatedBranch),
209211
}
210212

211213
// GetCurrentDBVersion returns the current db version

models/migrations/v137.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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 addBlockOnOutdatedBranch(x *xorm.Engine) error {
12+
type ProtectedBranch struct {
13+
BlockOnOutdatedBranch bool `xorm:"NOT NULL DEFAULT false"`
14+
}
15+
return x.Sync2(new(ProtectedBranch))
16+
}

modules/auth/repo_form.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ type ProtectBranchForm struct {
173173
ApprovalsWhitelistUsers string
174174
ApprovalsWhitelistTeams string
175175
BlockOnRejectedReviews bool
176+
BlockOnOutdatedBranch bool
176177
DismissStaleApprovals bool
177178
RequireSignedCommits bool
178179
ProtectedFilePatterns string

modules/convert/convert.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ func ToBranchProtection(bp *models.ProtectedBranch) *api.BranchProtection {
118118
ApprovalsWhitelistUsernames: approvalsWhitelistUsernames,
119119
ApprovalsWhitelistTeams: approvalsWhitelistTeams,
120120
BlockOnRejectedReviews: bp.BlockOnRejectedReviews,
121+
BlockOnOutdatedBranch: bp.BlockOnOutdatedBranch,
121122
DismissStaleApprovals: bp.DismissStaleApprovals,
122123
RequireSignedCommits: bp.RequireSignedCommits,
123124
ProtectedFilePatterns: bp.ProtectedFilePatterns,

modules/structs/repo_branch.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type BranchProtection struct {
3939
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
4040
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
4141
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
42+
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
4243
DismissStaleApprovals bool `json:"dismiss_stale_approvals"`
4344
RequireSignedCommits bool `json:"require_signed_commits"`
4445
ProtectedFilePatterns string `json:"protected_file_patterns"`
@@ -66,6 +67,7 @@ type CreateBranchProtectionOption struct {
6667
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
6768
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
6869
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
70+
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
6971
DismissStaleApprovals bool `json:"dismiss_stale_approvals"`
7072
RequireSignedCommits bool `json:"require_signed_commits"`
7173
ProtectedFilePatterns string `json:"protected_file_patterns"`
@@ -88,6 +90,7 @@ type EditBranchProtectionOption struct {
8890
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
8991
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
9092
BlockOnRejectedReviews *bool `json:"block_on_rejected_reviews"`
93+
BlockOnOutdatedBranch *bool `json:"block_on_outdated_branch"`
9194
DismissStaleApprovals *bool `json:"dismiss_stale_approvals"`
9295
RequireSignedCommits *bool `json:"require_signed_commits"`
9396
ProtectedFilePatterns *string `json:"protected_file_patterns"`

options/locale/locale_en-US.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,7 @@ pulls.required_status_check_missing = Some required checks are missing.
11021102
pulls.required_status_check_administrator = As an administrator, you may still merge this pull request.
11031103
pulls.blocked_by_approvals = "This Pull Request doesn't have enough approvals yet. %d of %d approvals granted."
11041104
pulls.blocked_by_rejection = "This Pull Request has changes requested by an official reviewer."
1105+
pulls.blocked_by_outdated_branch = "This Pull Request is blocked because it's outdated."
11051106
pulls.can_auto_merge_desc = This pull request can be merged automatically.
11061107
pulls.cannot_auto_merge_desc = This pull request cannot be merged automatically due to conflicts.
11071108
pulls.cannot_auto_merge_helper = Merge manually to resolve the conflicts.
@@ -1528,6 +1529,8 @@ settings.protected_branch_deletion = Disable Branch Protection
15281529
settings.protected_branch_deletion_desc = Disabling branch protection allows users with write permission to push to the branch. Continue?
15291530
settings.block_rejected_reviews = Block merge on rejected reviews
15301531
settings.block_rejected_reviews_desc = Merging will not be possible when changes are requested by official reviewers, even if there are enough approvals.
1532+
settings.block_outdated_branch = Block merge if pull request is outdated
1533+
settings.block_outdated_branch_desc = Merging will not be possible when head branch is behind base branch.
15311534
settings.default_branch_desc = Select a default repository branch for pull requests and code commits:
15321535
settings.choose_branch = Choose a branch…
15331536
settings.no_protected_branch = There are no protected branches.

routers/api/v1/repo/branch.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ func CreateBranchProtection(ctx *context.APIContext, form api.CreateBranchProtec
340340
DismissStaleApprovals: form.DismissStaleApprovals,
341341
RequireSignedCommits: form.RequireSignedCommits,
342342
ProtectedFilePatterns: form.ProtectedFilePatterns,
343+
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
343344
}
344345

345346
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
@@ -475,6 +476,10 @@ func EditBranchProtection(ctx *context.APIContext, form api.EditBranchProtection
475476
protectBranch.ProtectedFilePatterns = *form.ProtectedFilePatterns
476477
}
477478

479+
if form.BlockOnOutdatedBranch != nil {
480+
protectBranch.BlockOnOutdatedBranch = *form.BlockOnOutdatedBranch
481+
}
482+
478483
var whitelistUsers []int64
479484
if form.PushWhitelistUsernames != nil {
480485
whitelistUsers, err = models.GetUserIDsByNames(form.PushWhitelistUsernames, false)

routers/private/hook.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) {
263263
}
264264
}
265265

266+
// Detect Protected file pattern
266267
globs := protectBranch.GetProtectedFilePatterns()
267268
if len(globs) > 0 {
268269
err := checkFileProtection(oldCommitID, newCommitID, globs, gitRepo, env)

routers/repo/issue.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,7 @@ func ViewIssue(ctx *context.Context) {
10651065
cnt := pull.ProtectedBranch.GetGrantedApprovalsCount(pull)
10661066
ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull)
10671067
ctx.Data["IsBlockedByRejection"] = pull.ProtectedBranch.MergeBlockedByRejectedReview(pull)
1068+
ctx.Data["IsBlockedByOutdatedBranch"] = pull.ProtectedBranch.MergeBlockedByOutdatedBranch(pull)
10681069
ctx.Data["GrantedApprovals"] = cnt
10691070
ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits
10701071
}

routers/repo/setting_protected_branch.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm)
248248
protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
249249
protectBranch.RequireSignedCommits = f.RequireSignedCommits
250250
protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
251+
protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
251252

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

services/pull/merge.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -574,16 +574,22 @@ func CheckPRReadyToMerge(pr *models.PullRequest) (err error) {
574574
}
575575
}
576576

577-
if enoughApprovals := pr.ProtectedBranch.HasEnoughApprovals(pr); !enoughApprovals {
577+
if !pr.ProtectedBranch.HasEnoughApprovals(pr) {
578578
return models.ErrNotAllowedToMerge{
579579
Reason: "Does not have enough approvals",
580580
}
581581
}
582-
if rejected := pr.ProtectedBranch.MergeBlockedByRejectedReview(pr); rejected {
582+
if pr.ProtectedBranch.MergeBlockedByRejectedReview(pr) {
583583
return models.ErrNotAllowedToMerge{
584584
Reason: "There are requested changes",
585585
}
586586
}
587587

588+
if pr.ProtectedBranch.MergeBlockedByOutdatedBranch(pr) {
589+
return models.ErrNotAllowedToMerge{
590+
Reason: "The head branch is behind the base branch",
591+
}
592+
}
593+
588594
return nil
589595
}

templates/repo/issue/view_content/pull.tmpl

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
{{- else if .IsPullRequestBroken}}red
6767
{{- else if .IsBlockedByApprovals}}red
6868
{{- else if .IsBlockedByRejection}}red
69+
{{- else if .IsBlockedByOutdatedBranch}}red
6970
{{- else if and .EnableStatusCheck (or .RequiredStatusCheckState.IsFailure .RequiredStatusCheckState.IsError)}}red
7071
{{- else if and .EnableStatusCheck (or (not $.LatestCommitStatus) .RequiredStatusCheckState.IsPending .RequiredStatusCheckState.IsWarning)}}yellow
7172
{{- else if and .RequireSigned (not .WillSign)}}}red
@@ -138,6 +139,11 @@
138139
<i class="icon icon-octicon">{{svg "octicon-x" 16}}</i>
139140
{{$.i18n.Tr "repo.pulls.blocked_by_rejection"}}
140141
</div>
142+
{{else if .IsBlockedByOutdatedBranch}}
143+
<div class="item text red">
144+
<i class="icon icon-octicon">{{svg "octicon-x" 16}}</i>
145+
{{$.i18n.Tr "repo.pulls.blocked_by_outdated_branch"}}
146+
</div>
141147
{{else if and .EnableStatusCheck (or .RequiredStatusCheckState.IsError .RequiredStatusCheckState.IsFailure)}}
142148
<div class="item text red">
143149
<i class="icon icon-octicon">{{svg "octicon-x" 16}}</i>
@@ -158,7 +164,7 @@
158164
{{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }}
159165
</div>
160166
{{end}}
161-
{{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection (and .EnableStatusCheck (not .RequiredStatusCheckState.IsSuccess))}}
167+
{{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOutdatedBranch (and .EnableStatusCheck (not .RequiredStatusCheckState.IsSuccess))}}
162168
{{if and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .RequireSigned) .WillSign)}}
163169
{{if $notAllOverridableChecksOk}}
164170
<div class="item text yellow">
@@ -342,6 +348,11 @@
342348
{{svg "octicon-x" 16}}
343349
{{$.i18n.Tr "repo.pulls.blocked_by_rejection"}}
344350
</div>
351+
{{else if .IsBlockedByOutdatedBranch}}
352+
<div class="item text red">
353+
<i class="icon icon-octicon">{{svg "octicon-x" 16}}</i>
354+
{{$.i18n.Tr "repo.pulls.blocked_by_outdated_branch"}}
355+
</div>
345356
{{else if and .EnableStatusCheck (not .RequiredStatusCheckState.IsSuccess)}}
346357
<div class="item text red">
347358
{{svg "octicon-x" 16}}

templates/repo/settings/protected_branch.tmpl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,13 @@
225225
<p class="help">{{.i18n.Tr "repo.settings.require_signed_commits_desc"}}</p>
226226
</div>
227227
</div>
228+
<div class="field">
229+
<div class="ui checkbox">
230+
<input name="block_on_outdated_branch" type="checkbox" {{if .Branch.BlockOnOutdatedBranch}}checked{{end}}>
231+
<label for="block_on_outdated_branch">{{.i18n.Tr "repo.settings.block_outdated_branch"}}</label>
232+
<p class="help">{{.i18n.Tr "repo.settings.block_outdated_branch_desc"}}</p>
233+
</div>
234+
</div>
228235
<div class="field">
229236
<label for="protected_file_patterns">{{.i18n.Tr "repo.settings.protect_protected_file_patterns"}}</label>
230237
<input name="protected_file_patterns" id="protected_file_patterns" type="text" value="{{.Branch.ProtectedFilePatterns}}">

templates/swagger/v1_json.tmpl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10072,6 +10072,10 @@
1007210072
},
1007310073
"x-go-name": "ApprovalsWhitelistUsernames"
1007410074
},
10075+
"block_on_outdated_branch": {
10076+
"type": "boolean",
10077+
"x-go-name": "BlockOnOutdatedBranch"
10078+
},
1007510079
"block_on_rejected_reviews": {
1007610080
"type": "boolean",
1007710081
"x-go-name": "BlockOnRejectedReviews"
@@ -10392,6 +10396,10 @@
1039210396
},
1039310397
"x-go-name": "ApprovalsWhitelistUsernames"
1039410398
},
10399+
"block_on_outdated_branch": {
10400+
"type": "boolean",
10401+
"x-go-name": "BlockOnOutdatedBranch"
10402+
},
1039510403
"block_on_rejected_reviews": {
1039610404
"type": "boolean",
1039710405
"x-go-name": "BlockOnRejectedReviews"
@@ -11204,6 +11212,10 @@
1120411212
},
1120511213
"x-go-name": "ApprovalsWhitelistUsernames"
1120611214
},
11215+
"block_on_outdated_branch": {
11216+
"type": "boolean",
11217+
"x-go-name": "BlockOnOutdatedBranch"
11218+
},
1120711219
"block_on_rejected_reviews": {
1120811220
"type": "boolean",
1120911221
"x-go-name": "BlockOnRejectedReviews"

0 commit comments

Comments
 (0)