mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-14 03:29:55 +00:00
feat: Add bypass allowlist for branch protection (#36514)
- Introduce a “Bypass Protection Allowlist” on branch rules (users/teams) alongside admins, with BlockAdminMergeOverride still respected. - Surface the allowlist in API (create/edit options, structs) and settings UI; merge box now shows the red button + message for bypass-capable users. - Apply bypass logic to merge checks and pre-receive so allowlisted users can override unmet approvals/status checks/ protected files when force-merging. - Add migration for new columns, locale strings, and unit tests (bypass helper; queue test tweak). <img width="1069" height="218" alt="image" src="https://github.com/user-attachments/assets/0b61bc2a-a27f-47f3-a923-613688008e65" /> Fixes #36476 --------- Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Giteabot <teabot@gitea.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Codex GPT-5.3 <codex@openai.com> Co-authored-by: GPT-5.2 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
This commit is contained in:
@@ -148,6 +148,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
|
||||
forcePushAllowlistUsernames := getWhitelistEntities(readers, bp.ForcePushAllowlistUserIDs)
|
||||
mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs)
|
||||
approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs)
|
||||
bypassAllowlistUsernames := getWhitelistEntities(readers, bp.BypassAllowlistUserIDs)
|
||||
|
||||
teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
@@ -158,6 +159,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
|
||||
forcePushAllowlistTeams := getWhitelistEntities(teamReaders, bp.ForcePushAllowlistTeamIDs)
|
||||
mergeWhitelistTeams := getWhitelistEntities(teamReaders, bp.MergeWhitelistTeamIDs)
|
||||
approvalsWhitelistTeams := getWhitelistEntities(teamReaders, bp.ApprovalsWhitelistTeamIDs)
|
||||
bypassAllowlistTeams := getWhitelistEntities(teamReaders, bp.BypassAllowlistTeamIDs)
|
||||
|
||||
branchName := ""
|
||||
if !git_model.IsRuleNameSpecial(bp.RuleName) {
|
||||
@@ -181,6 +183,9 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
|
||||
EnableMergeWhitelist: bp.EnableMergeWhitelist,
|
||||
MergeWhitelistUsernames: mergeWhitelistUsernames,
|
||||
MergeWhitelistTeams: mergeWhitelistTeams,
|
||||
EnableBypassAllowlist: bp.EnableBypassAllowlist,
|
||||
BypassAllowlistUsernames: bypassAllowlistUsernames,
|
||||
BypassAllowlistTeams: bypassAllowlistTeams,
|
||||
EnableStatusCheck: bp.EnableStatusCheck,
|
||||
StatusCheckContexts: bp.StatusCheckContexts,
|
||||
RequiredApprovals: bp.RequiredApprovals,
|
||||
|
||||
@@ -181,6 +181,9 @@ type ProtectBranchForm struct {
|
||||
EnableMergeWhitelist bool
|
||||
MergeWhitelistUsers string
|
||||
MergeWhitelistTeams string
|
||||
EnableBypassAllowlist bool
|
||||
BypassAllowlistUsers string
|
||||
BypassAllowlistTeams string
|
||||
EnableStatusCheck bool
|
||||
StatusCheckContexts string
|
||||
RequiredApprovals int64
|
||||
|
||||
+16
-14
@@ -139,7 +139,7 @@ const (
|
||||
// - merge: both the head commits must be verified and Gitea must sign the merge commit.
|
||||
// - rebase, rebase-merge, squash: Gitea rewrites the commits and signs each, so only Gitea's
|
||||
// signing ability is checked.
|
||||
func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, mergeStyle repo_model.MergeStyle, adminForceMerge bool) error {
|
||||
func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, mergeStyle repo_model.MergeStyle, forceMerge bool) error {
|
||||
return db.WithTx(stdCtx, func(ctx context.Context) error {
|
||||
if pr.HasMerged {
|
||||
return ErrHasMerged
|
||||
@@ -176,21 +176,21 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
|
||||
return ErrIsChecking
|
||||
}
|
||||
|
||||
if err := CheckPullBranchProtections(ctx, pr, false); err != nil {
|
||||
if !errors.Is(err, ErrNotReadyToMerge) {
|
||||
log.Error("Error whilst checking pull branch protection for %-v: %v", pr, err)
|
||||
return err
|
||||
if errProtection := CheckPullBranchProtections(ctx, pr, false); errProtection != nil {
|
||||
if !errors.Is(errProtection, ErrNotReadyToMerge) {
|
||||
log.Error("Error whilst checking pull branch protection for %-v: %v", pr, errProtection)
|
||||
return errProtection
|
||||
}
|
||||
|
||||
// Now the branch protection check failed, check whether the failure could be skipped (skip by setting err = nil)
|
||||
|
||||
// * when doing Auto Merge (Scheduled Merge After Checks Succeed), skip the branch protection check
|
||||
if mergeCheckType == MergeCheckTypeAuto {
|
||||
err = nil
|
||||
errProtection = nil
|
||||
}
|
||||
|
||||
// * if admin tries to "Force Merge", they could sometimes skip the branch protection check
|
||||
if adminForceMerge {
|
||||
// * if the doer tries to "Force Merge", check whether it is really allowed
|
||||
if forceMerge {
|
||||
isRepoAdmin, errForceMerge := access_model.IsUserRepoAdmin(ctx, pr.BaseRepo, doer)
|
||||
if errForceMerge != nil {
|
||||
return fmt.Errorf("IsUserRepoAdmin failed, repo: %v, doer: %v, err: %w", pr.BaseRepoID, doer.ID, errForceMerge)
|
||||
@@ -201,16 +201,18 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
|
||||
return fmt.Errorf("GetFirstMatchProtectedBranchRule failed, repo: %v, base branch: %v, err: %w", pr.BaseRepoID, pr.BaseBranch, errForceMerge)
|
||||
}
|
||||
|
||||
// if doer is admin and the "Force Merge" is not blocked, then clear the branch protection check error
|
||||
blockAdminForceMerge := protectedBranchRule != nil && protectedBranchRule.BlockAdminMergeOverride
|
||||
if isRepoAdmin && !blockAdminForceMerge {
|
||||
err = nil
|
||||
canForceMerge := isRepoAdmin
|
||||
if protectedBranchRule != nil {
|
||||
canForceMerge = git_model.CanBypassBranchProtection(ctx, protectedBranchRule, doer, isRepoAdmin)
|
||||
}
|
||||
if canForceMerge {
|
||||
errProtection = nil
|
||||
}
|
||||
}
|
||||
|
||||
// If there is still a branch protection check error, return it
|
||||
if err != nil {
|
||||
return err
|
||||
if errProtection != nil {
|
||||
return errProtection
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user