From d65ee2fb7b54ef8b8fd6ccfea662952f735e3c97 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Wed, 8 Apr 2020 15:26:26 +0200 Subject: [PATCH 1/4] Block PR on Outdated Branch --- models/branches.go | 12 ++++++++++++ models/migrations/migrations.go | 6 ++++-- models/migrations/v999.go | 16 ++++++++++++++++ modules/auth/repo_form.go | 1 + modules/convert/convert.go | 1 + modules/structs/repo_branch.go | 3 +++ options/locale/locale_en-US.ini | 5 ++++- routers/api/v1/repo/branch.go | 5 +++++ routers/private/hook.go | 1 + routers/repo/issue.go | 1 + routers/repo/setting_protected_branch.go | 1 + services/pull/merge.go | 10 ++++++++-- templates/repo/issue/view_content/pull.tmpl | 13 ++++++++++++- templates/repo/settings/protected_branch.tmpl | 7 +++++++ templates/swagger/v1_json.tmpl | 12 ++++++++++++ 15 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 models/migrations/v999.go diff --git a/models/branches.go b/models/branches.go index 44cfb4140380..5ff4f4e9c741 100644 --- a/models/branches.go +++ b/models/branches.go @@ -47,6 +47,7 @@ type ProtectedBranch struct { ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"` BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"` + BlockOnOutdatedBranch bool `xorm:"NOT NULL DEFAULT false"` DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"` RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"` ProtectedFilePatterns string `xorm:"TEXT"` @@ -194,6 +195,17 @@ func (protectBranch *ProtectedBranch) MergeBlockedByRejectedReview(pr *PullReque return rejectExist } +// MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch +func (protectBranch *ProtectedBranch) MergeBlockedByOutdatedBranch(pr *PullRequest) bool { + if !protectBranch.BlockOnOutdatedBranch { + return false + } + if pr.CommitsBehind == 0 { + return false + } + return true +} + // GetProtectedFilePatterns parses a semicolon separated list of protected file patterns and returns a glob.Glob slice func (protectBranch *ProtectedBranch) GetProtectedFilePatterns() []glob.Glob { extarr := make([]glob.Glob, 0, 10) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index e1d46236a9bd..fe72d0f630d4 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -202,10 +202,12 @@ var migrations = []Migration{ NewMigration("Add EmailHash Table", addEmailHashTable), // v134 -> v135 NewMigration("Refix merge base for merged pull requests", refixMergeBase), - // v135 -> 136 + // v135 -> v136 NewMigration("Add OrgID column to Labels table", addOrgIDLabelColumn), - // v136 -> 137 + // v136 -> v137 NewMigration("Add CommitsAhead and CommitsBehind Column to PullRequest Table", addCommitDivergenceToPulls), + // v137 -> v138 + NewMigration("Add Branch Protection Block Outdated Branch", addBlockOnOutdatedBranch), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v999.go b/models/migrations/v999.go new file mode 100644 index 000000000000..f175cf8a80f1 --- /dev/null +++ b/models/migrations/v999.go @@ -0,0 +1,16 @@ +// 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 addBlockOnOutdatedBranch(x *xorm.Engine) error { + type ProtectedBranch struct { + BlockOnOutdatedBranch bool `xorm:"NOT NULL DEFAULT false"` + } + return x.Sync2(new(ProtectedBranch)) +} diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 80ed31419666..6c3421e4f7d8 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -173,6 +173,7 @@ type ProtectBranchForm struct { ApprovalsWhitelistUsers string ApprovalsWhitelistTeams string BlockOnRejectedReviews bool + BlockOnOutdatedBranch bool DismissStaleApprovals bool RequireSignedCommits bool ProtectedFilePatterns string diff --git a/modules/convert/convert.go b/modules/convert/convert.go index 30d240f809a7..ec18b130567f 100644 --- a/modules/convert/convert.go +++ b/modules/convert/convert.go @@ -118,6 +118,7 @@ func ToBranchProtection(bp *models.ProtectedBranch) *api.BranchProtection { ApprovalsWhitelistUsernames: approvalsWhitelistUsernames, ApprovalsWhitelistTeams: approvalsWhitelistTeams, BlockOnRejectedReviews: bp.BlockOnRejectedReviews, + BlockOnOutdatedBranch: bp.BlockOnOutdatedBranch, DismissStaleApprovals: bp.DismissStaleApprovals, RequireSignedCommits: bp.RequireSignedCommits, ProtectedFilePatterns: bp.ProtectedFilePatterns, diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go index 886018c8587f..490c63070a0c 100644 --- a/modules/structs/repo_branch.go +++ b/modules/structs/repo_branch.go @@ -39,6 +39,7 @@ type BranchProtection struct { ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"` ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"` BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"` + BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"` DismissStaleApprovals bool `json:"dismiss_stale_approvals"` RequireSignedCommits bool `json:"require_signed_commits"` ProtectedFilePatterns string `json:"protected_file_patterns"` @@ -66,6 +67,7 @@ type CreateBranchProtectionOption struct { ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"` ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"` BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"` + BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"` DismissStaleApprovals bool `json:"dismiss_stale_approvals"` RequireSignedCommits bool `json:"require_signed_commits"` ProtectedFilePatterns string `json:"protected_file_patterns"` @@ -88,6 +90,7 @@ type EditBranchProtectionOption struct { ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"` ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"` BlockOnRejectedReviews *bool `json:"block_on_rejected_reviews"` + BlockOnOutdatedBranch *bool `json:"block_on_outdated_branch"` DismissStaleApprovals *bool `json:"dismiss_stale_approvals"` RequireSignedCommits *bool `json:"require_signed_commits"` ProtectedFilePatterns *string `json:"protected_file_patterns"` diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 8b43115c07a3..e8062cf4c50a 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1100,7 +1100,8 @@ 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.blocked_by_rejection = "This Pull Request has an outdated head branch." +pulls.blocked_by_outdated_branch = "This Pull Request is blocked by an outdated head branch." 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. @@ -1525,6 +1526,8 @@ 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.block_outdated_branch = Block merge if branch is outdated +settings.block_outdated_branch_desc = Merging will not be possible when head branch is behind base branch. 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. diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 56a9b5ec2545..07c61595019f 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -340,6 +340,7 @@ func CreateBranchProtection(ctx *context.APIContext, form api.CreateBranchProtec DismissStaleApprovals: form.DismissStaleApprovals, RequireSignedCommits: form.RequireSignedCommits, ProtectedFilePatterns: form.ProtectedFilePatterns, + BlockOnOutdatedBranch: form.BlockOnOutdatedBranch, } err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{ @@ -475,6 +476,10 @@ func EditBranchProtection(ctx *context.APIContext, form api.EditBranchProtection protectBranch.ProtectedFilePatterns = *form.ProtectedFilePatterns } + if form.BlockOnOutdatedBranch != nil { + protectBranch.BlockOnOutdatedBranch = *form.BlockOnOutdatedBranch + } + var whitelistUsers []int64 if form.PushWhitelistUsernames != nil { whitelistUsers, err = models.GetUserIDsByNames(form.PushWhitelistUsernames, false) diff --git a/routers/private/hook.go b/routers/private/hook.go index 846d9b607004..de2b03e0b2e6 100644 --- a/routers/private/hook.go +++ b/routers/private/hook.go @@ -263,6 +263,7 @@ func HookPreReceive(ctx *macaron.Context, opts private.HookOptions) { } } + // Detect Protected file pattern globs := protectBranch.GetProtectedFilePatterns() if len(globs) > 0 { err := checkFileProtection(oldCommitID, newCommitID, globs, gitRepo, env) diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 3655de7ed44f..9ad379684a5c 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -1065,6 +1065,7 @@ func ViewIssue(ctx *context.Context) { cnt := pull.ProtectedBranch.GetGrantedApprovalsCount(pull) ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull) ctx.Data["IsBlockedByRejection"] = pull.ProtectedBranch.MergeBlockedByRejectedReview(pull) + ctx.Data["IsBlockedByOutdatedBranch"] = pull.ProtectedBranch.MergeBlockedByOutdatedBranch(pull) ctx.Data["GrantedApprovals"] = cnt ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits } diff --git a/routers/repo/setting_protected_branch.go b/routers/repo/setting_protected_branch.go index af49aefcec30..ab0fd77eee25 100644 --- a/routers/repo/setting_protected_branch.go +++ b/routers/repo/setting_protected_branch.go @@ -248,6 +248,7 @@ func SettingsProtectedBranchPost(ctx *context.Context, f auth.ProtectBranchForm) protectBranch.DismissStaleApprovals = f.DismissStaleApprovals protectBranch.RequireSignedCommits = f.RequireSignedCommits protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns + protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{ UserIDs: whitelistUsers, diff --git a/services/pull/merge.go b/services/pull/merge.go index 2d4d9722e839..75c42cbc67d7 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -571,16 +571,22 @@ func CheckPRReadyToMerge(pr *models.PullRequest) (err error) { } } - if enoughApprovals := pr.ProtectedBranch.HasEnoughApprovals(pr); !enoughApprovals { + if !pr.ProtectedBranch.HasEnoughApprovals(pr) { return models.ErrNotAllowedToMerge{ Reason: "Does not have enough approvals", } } - if rejected := pr.ProtectedBranch.MergeBlockedByRejectedReview(pr); rejected { + if pr.ProtectedBranch.MergeBlockedByRejectedReview(pr) { return models.ErrNotAllowedToMerge{ Reason: "There are requested changes", } } + if pr.ProtectedBranch.MergeBlockedByOutdatedBranch(pr) { + return models.ErrNotAllowedToMerge{ + Reason: fmt.Sprintf("There head branch %d behind base branch", pr.CommitsBehind), + } + } + return nil } diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index 03ac1b6cd704..9c3cbe7ff064 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -66,6 +66,7 @@ {{- else if .IsPullRequestBroken}}red {{- else if .IsBlockedByApprovals}}red {{- else if .IsBlockedByRejection}}red + {{- else if .IsBlockedByOutdatedBranch}}red {{- else if and .EnableStatusCheck (or .RequiredStatusCheckState.IsFailure .RequiredStatusCheckState.IsError)}}red {{- else if and .EnableStatusCheck (or .RequiredStatusCheckState.IsPending .RequiredStatusCheckState.IsWarning)}}yellow {{- else if and .RequireSigned (not .WillSign)}}}red @@ -138,6 +139,11 @@ {{svg "octicon-x" 16}} {{$.i18n.Tr "repo.pulls.blocked_by_rejection"}} + {{else if .IsBlockedByOutdatedBranch}} +
+ {{svg "octicon-x" 16}} + {{$.i18n.Tr "repo.pulls.blocked_by_outdated_branch"}} +
{{else if and .EnableStatusCheck (or .RequiredStatusCheckState.IsError .RequiredStatusCheckState.IsFailure)}}
{{svg "octicon-x" 16}} @@ -153,7 +159,7 @@ {{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }}
{{end}} - {{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection (and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess))}} + {{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOutdatedBranch (and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess))}} {{if and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .RequireSigned) .WillSign)}} {{if $notAllOverridableChecksOk}}
@@ -337,6 +343,11 @@ {{svg "octicon-x" 16}} {{$.i18n.Tr "repo.pulls.blocked_by_rejection"}}
+ {{else if .IsBlockedByOutdatedBranch}} +
+ {{svg "octicon-x" 16}} + {{$.i18n.Tr "repo.pulls.blocked_by_outdated_branch"}} +
{{else if and .EnableStatusCheck (not .IsRequiredStatusCheckSuccess)}}
{{svg "octicon-x" 16}} diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl index f46e84d3bc26..0496eb189d06 100644 --- a/templates/repo/settings/protected_branch.tmpl +++ b/templates/repo/settings/protected_branch.tmpl @@ -225,6 +225,13 @@

{{.i18n.Tr "repo.settings.require_signed_commits_desc"}}

+
+
+ + +

{{.i18n.Tr "repo.settings.block_outdated_branch_desc"}}

+
+
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 8b10a759901d..21b30e9aa14b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -10054,6 +10054,10 @@ }, "x-go-name": "ApprovalsWhitelistUsernames" }, + "block_on_outdated_branch": { + "type": "boolean", + "x-go-name": "BlockOnOutdatedBranch" + }, "block_on_rejected_reviews": { "type": "boolean", "x-go-name": "BlockOnRejectedReviews" @@ -10374,6 +10378,10 @@ }, "x-go-name": "ApprovalsWhitelistUsernames" }, + "block_on_outdated_branch": { + "type": "boolean", + "x-go-name": "BlockOnOutdatedBranch" + }, "block_on_rejected_reviews": { "type": "boolean", "x-go-name": "BlockOnRejectedReviews" @@ -11186,6 +11194,10 @@ }, "x-go-name": "ApprovalsWhitelistUsernames" }, + "block_on_outdated_branch": { + "type": "boolean", + "x-go-name": "BlockOnOutdatedBranch" + }, "block_on_rejected_reviews": { "type": "boolean", "x-go-name": "BlockOnRejectedReviews" From 767b072bf538301346351fd4d6ed1d65c90edafd Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Tue, 14 Apr 2020 16:11:51 +0200 Subject: [PATCH 2/4] finalize --- models/migrations/{v999.go => v137.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/migrations/{v999.go => v137.go} (100%) diff --git a/models/migrations/v999.go b/models/migrations/v137.go similarity index 100% rename from models/migrations/v999.go rename to models/migrations/v137.go From 005843ed4d8f1d92d0b48bc7ce3bfedb933a6c1f Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Tue, 14 Apr 2020 16:20:40 +0200 Subject: [PATCH 3/4] cleanup --- models/branches.go | 8 +------- options/locale/locale_en-US.ini | 2 +- services/pull/merge.go | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/models/branches.go b/models/branches.go index 5ff4f4e9c741..e6b8d61a7026 100644 --- a/models/branches.go +++ b/models/branches.go @@ -197,13 +197,7 @@ func (protectBranch *ProtectedBranch) MergeBlockedByRejectedReview(pr *PullReque // MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch func (protectBranch *ProtectedBranch) MergeBlockedByOutdatedBranch(pr *PullRequest) bool { - if !protectBranch.BlockOnOutdatedBranch { - return false - } - if pr.CommitsBehind == 0 { - return false - } - return true + return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 } // GetProtectedFilePatterns parses a semicolon separated list of protected file patterns and returns a glob.Glob slice diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e8062cf4c50a..0ff093da8594 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1100,7 +1100,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 an outdated head branch." +pulls.blocked_by_rejection = "This Pull Request has changes requested by an official reviewer." pulls.blocked_by_outdated_branch = "This Pull Request is blocked by an outdated head branch." 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. diff --git a/services/pull/merge.go b/services/pull/merge.go index 75c42cbc67d7..e709116464f3 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -584,7 +584,7 @@ func CheckPRReadyToMerge(pr *models.PullRequest) (err error) { if pr.ProtectedBranch.MergeBlockedByOutdatedBranch(pr) { return models.ErrNotAllowedToMerge{ - Reason: fmt.Sprintf("There head branch %d behind base branch", pr.CommitsBehind), + Reason: fmt.Sprintf("Head branch %d behind base branch", pr.CommitsBehind), } } From 678b230317610383c2cb52a3a1497741a288b8c1 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Wed, 15 Apr 2020 13:36:35 +0200 Subject: [PATCH 4/4] fix typo and sentences thanks @guillep2k Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> --- options/locale/locale_en-US.ini | 4 ++-- services/pull/merge.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 0ff093da8594..5d95d16fd47d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1101,7 +1101,7 @@ 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.blocked_by_outdated_branch = "This Pull Request is blocked by an outdated head branch." +pulls.blocked_by_outdated_branch = "This Pull Request is blocked because it's outdated." 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. @@ -1526,7 +1526,7 @@ 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.block_outdated_branch = Block merge if branch is outdated +settings.block_outdated_branch = Block merge if pull request is outdated settings.block_outdated_branch_desc = Merging will not be possible when head branch is behind base branch. settings.default_branch_desc = Select a default repository branch for pull requests and code commits: settings.choose_branch = Choose a branch… diff --git a/services/pull/merge.go b/services/pull/merge.go index de1312788640..47521ce14770 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -587,7 +587,7 @@ func CheckPRReadyToMerge(pr *models.PullRequest) (err error) { if pr.ProtectedBranch.MergeBlockedByOutdatedBranch(pr) { return models.ErrNotAllowedToMerge{ - Reason: fmt.Sprintf("Head branch %d behind base branch", pr.CommitsBehind), + Reason: "The head branch is behind the base branch", } }