feat: Add block on pending codeowner reviews branch protection (#34995)

This commit introduces a new branch protection rule that allows merge
blocking if there are pending reviews from one or more code owners (as
defined in any valid `CODEOWNERS` file). This is determined by
evaluating each rule present in the `CODEOWNERS` file individually. For
every rule, at least one named code owner (or member of a code owner
team) must have given an approving review for merging to be possible.

Closes #32602 

---

This PR does NOT display code owners separately from other reviewers
(#28137)

### Screenshot

<details>
<summary>Pull Request</summary>
<img
src="https://github.com/user-attachments/assets/74560f9c-9f59-477a-b270-63f937b26d6a"
/>
</details>

<details>
<summary>Branch Protection Rule</summary>
<img
src="https://github.com/user-attachments/assets/f0a9ce00-38fe-4eed-b6a3-5290aa404610"
/>
</details>

Doc PR
https://gitea.com/gitea/docs/pulls/245

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Dominique Hummel
2026-07-31 13:00:59 +00:00
committed by GitHub
co-authored by bircni wxiaoguang
parent 805697089e
commit 1f6cfe611a
17 changed files with 408 additions and 41 deletions
+5
View File
@@ -797,6 +797,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
RequiredApprovals: requiredApprovals,
BlockOnRejectedReviews: form.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests,
BlockOnCodeownerReviews: form.BlockOnCodeownerReviews,
DismissStaleApprovals: form.DismissStaleApprovals,
IgnoreStaleApprovals: form.IgnoreStaleApprovals,
RequireSignedCommits: form.RequireSignedCommits,
@@ -965,6 +966,10 @@ func EditBranchProtection(ctx *context.APIContext) {
protectBranch.BlockOnOfficialReviewRequests = *form.BlockOnOfficialReviewRequests
}
if form.BlockOnCodeownerReviews != nil {
protectBranch.BlockOnCodeownerReviews = *form.BlockOnCodeownerReviews
}
if form.DismissStaleApprovals != nil {
protectBranch.DismissStaleApprovals = *form.DismissStaleApprovals
}
+7 -4
View File
@@ -956,11 +956,9 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
// so block on any required status context, not only when enableStatusCheck is on.
data.hasStatusCheckBlocker = (data.enableStatusCheck || data.hasRequiredStatusContexts) && !data.StatusCheckData.RequiredChecksState.IsSuccess()
// this logic is from:
// {{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not $requiredStatusCheckState.IsSuccess))}}
// HINT: if a PR's status is not mergeable, then it is a non-overridable blocker, such logic is handled separately (see IsStatusMergeable)
data.hasOverridableBlockers = data.isBlockedByApprovals || data.isBlockedByRejection ||
data.isBlockedByOfficialReviewRequests || data.isBlockedByOutdatedBranch || data.isBlockedByChangedProtectedFiles ||
data.isBlockedByOfficialReviewRequests || data.isBlockedByCodeowners ||
data.isBlockedByOutdatedBranch || data.isBlockedByChangedProtectedFiles ||
data.hasStatusCheckBlocker
data.canBypassProtection = isRepoAdmin
@@ -1071,6 +1069,11 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxProtectedRules(ctx *context.Co
data.infoProtectionBlockers.AddErrorItem(ctx.Locale.Tr("repo.pulls.blocked_by_official_review_requests"))
}
data.isBlockedByCodeowners = !issue_service.HasAllRequiredCodeownerReviews(ctx, pb, pull)
if data.isBlockedByCodeowners {
data.infoProtectionBlockers.AddErrorItem(ctx.Locale.Tr("repo.pulls.blocked_by_codeowners"))
}
data.isBlockedByOutdatedBranch = issues_model.MergeBlockedByOutdatedBranch(pb, pull)
if data.isBlockedByOutdatedBranch {
data.infoProtectionBlockers.AddErrorItem(ctx.Locale.Tr("repo.pulls.blocked_by_outdated_branch"))
+1
View File
@@ -297,6 +297,7 @@ type pullMergeBoxData struct {
isBlockedByApprovals bool
isBlockedByRejection bool
isBlockedByOfficialReviewRequests bool
isBlockedByCodeowners bool
isBlockedByOutdatedBranch bool
isBlockedByChangedProtectedFiles bool
requireSigned, willSign bool
@@ -266,6 +266,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
}
protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews
protectBranch.BlockOnOfficialReviewRequests = f.BlockOnOfficialReviewRequests
protectBranch.BlockOnCodeownerReviews = f.BlockOnCodeownerReviews
protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
protectBranch.IgnoreStaleApprovals = f.IgnoreStaleApprovals
protectBranch.RequireSignedCommits = f.RequireSignedCommits