[FEAT] Configure if protected branch rule should apply to admins
- Currently protected branch rules do not apply to admins, however in some cases (like in the case of Forgejo project) you might also want to apply these rules to admins to avoid accidental merges. - Add new option to configure this on a per-rule basis. - Adds integration tests. - Resolves #65
This commit is contained in:
parent
a11116602e
commit
79b7089360
16 changed files with 167 additions and 26 deletions
|
@ -54,6 +54,8 @@ var migrations = []*Migration{
|
||||||
NewMigration("Add the `enable_repo_unit_hints` column to the `user` table", forgejo_v1_22.AddUserRepoUnitHintsSetting),
|
NewMigration("Add the `enable_repo_unit_hints` column to the `user` table", forgejo_v1_22.AddUserRepoUnitHintsSetting),
|
||||||
// v7 -> v8
|
// v7 -> v8
|
||||||
NewMigration("Modify the `release`.`note` content to remove SSH signatures", forgejo_v1_22.RemoveSSHSignaturesFromReleaseNotes),
|
NewMigration("Modify the `release`.`note` content to remove SSH signatures", forgejo_v1_22.RemoveSSHSignaturesFromReleaseNotes),
|
||||||
|
// v8 -> v9
|
||||||
|
NewMigration("Add the `apply_to_admins` column to the `protected_branch` table", forgejo_v1_22.AddApplyToAdminsSetting),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||||
|
|
15
models/forgejo_migrations/v1_22/v9.go
Normal file
15
models/forgejo_migrations/v1_22/v9.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_22 //nolint
|
||||||
|
|
||||||
|
import "xorm.io/xorm"
|
||||||
|
|
||||||
|
func AddApplyToAdminsSetting(x *xorm.Engine) error {
|
||||||
|
type ProtectedBranch struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
ApplyToAdmins bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync(&ProtectedBranch{})
|
||||||
|
}
|
|
@ -58,6 +58,7 @@ type ProtectedBranch struct {
|
||||||
RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"`
|
RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
ProtectedFilePatterns string `xorm:"TEXT"`
|
ProtectedFilePatterns string `xorm:"TEXT"`
|
||||||
UnprotectedFilePatterns string `xorm:"TEXT"`
|
UnprotectedFilePatterns string `xorm:"TEXT"`
|
||||||
|
ApplyToAdmins bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
|
|
@ -47,6 +47,7 @@ type BranchProtection struct {
|
||||||
RequireSignedCommits bool `json:"require_signed_commits"`
|
RequireSignedCommits bool `json:"require_signed_commits"`
|
||||||
ProtectedFilePatterns string `json:"protected_file_patterns"`
|
ProtectedFilePatterns string `json:"protected_file_patterns"`
|
||||||
UnprotectedFilePatterns string `json:"unprotected_file_patterns"`
|
UnprotectedFilePatterns string `json:"unprotected_file_patterns"`
|
||||||
|
ApplyToAdmins bool `json:"apply_to_admins"`
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
Created time.Time `json:"created_at"`
|
Created time.Time `json:"created_at"`
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
|
@ -80,6 +81,7 @@ type CreateBranchProtectionOption struct {
|
||||||
RequireSignedCommits bool `json:"require_signed_commits"`
|
RequireSignedCommits bool `json:"require_signed_commits"`
|
||||||
ProtectedFilePatterns string `json:"protected_file_patterns"`
|
ProtectedFilePatterns string `json:"protected_file_patterns"`
|
||||||
UnprotectedFilePatterns string `json:"unprotected_file_patterns"`
|
UnprotectedFilePatterns string `json:"unprotected_file_patterns"`
|
||||||
|
ApplyToAdmins bool `json:"apply_to_admins"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditBranchProtectionOption options for editing a branch protection
|
// EditBranchProtectionOption options for editing a branch protection
|
||||||
|
@ -106,4 +108,5 @@ type EditBranchProtectionOption struct {
|
||||||
RequireSignedCommits *bool `json:"require_signed_commits"`
|
RequireSignedCommits *bool `json:"require_signed_commits"`
|
||||||
ProtectedFilePatterns *string `json:"protected_file_patterns"`
|
ProtectedFilePatterns *string `json:"protected_file_patterns"`
|
||||||
UnprotectedFilePatterns *string `json:"unprotected_file_patterns"`
|
UnprotectedFilePatterns *string `json:"unprotected_file_patterns"`
|
||||||
|
ApplyToAdmins *bool `json:"apply_to_admins"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -2348,6 +2348,7 @@ settings.event_pull_request_review_request = Pull request review requested
|
||||||
settings.event_pull_request_review_request_desc = Pull request review requested or review request removed.
|
settings.event_pull_request_review_request_desc = Pull request review requested or review request removed.
|
||||||
settings.event_pull_request_approvals = Pull request approvals
|
settings.event_pull_request_approvals = Pull request approvals
|
||||||
settings.event_pull_request_merge = Pull request merge
|
settings.event_pull_request_merge = Pull request merge
|
||||||
|
settings.event_pull_request_enforcement = Enforcement
|
||||||
settings.event_package = Package
|
settings.event_package = Package
|
||||||
settings.event_package_desc = Package created or deleted in a repository.
|
settings.event_package_desc = Package created or deleted in a repository.
|
||||||
settings.branch_filter = Branch filter
|
settings.branch_filter = Branch filter
|
||||||
|
@ -2462,6 +2463,8 @@ settings.block_on_official_review_requests = Block merge on official review requ
|
||||||
settings.block_on_official_review_requests_desc = Merging will not be possible when it has official review requests, even if there are enough approvals.
|
settings.block_on_official_review_requests_desc = Merging will not be possible when it has official review requests, even if there are enough approvals.
|
||||||
settings.block_outdated_branch = Block merge if pull request 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.block_outdated_branch_desc = Merging will not be possible when head branch is behind base branch.
|
||||||
|
settings.enforce_on_admins = Enforce this rule for repository admins
|
||||||
|
settings.enforce_on_admins_desc = Repository admins cannot bypass this rule.
|
||||||
settings.default_branch_desc = Select a default repository branch for pull requests and code commits:
|
settings.default_branch_desc = Select a default repository branch for pull requests and code commits:
|
||||||
settings.merge_style_desc = Merge styles
|
settings.merge_style_desc = Merge styles
|
||||||
settings.default_merge_style_desc = Default merge style
|
settings.default_merge_style_desc = Default merge style
|
||||||
|
|
|
@ -621,6 +621,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||||
ProtectedFilePatterns: form.ProtectedFilePatterns,
|
ProtectedFilePatterns: form.ProtectedFilePatterns,
|
||||||
UnprotectedFilePatterns: form.UnprotectedFilePatterns,
|
UnprotectedFilePatterns: form.UnprotectedFilePatterns,
|
||||||
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
|
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
|
||||||
|
ApplyToAdmins: form.ApplyToAdmins,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
|
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
|
||||||
|
@ -808,6 +809,10 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||||
protectBranch.BlockOnOutdatedBranch = *form.BlockOnOutdatedBranch
|
protectBranch.BlockOnOutdatedBranch = *form.BlockOnOutdatedBranch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if form.ApplyToAdmins != nil {
|
||||||
|
protectBranch.ApplyToAdmins = *form.ApplyToAdmins
|
||||||
|
}
|
||||||
|
|
||||||
var whitelistUsers []int64
|
var whitelistUsers []int64
|
||||||
if form.PushWhitelistUsernames != nil {
|
if form.PushWhitelistUsernames != nil {
|
||||||
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
|
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
|
||||||
|
|
|
@ -337,13 +337,9 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're an admin for the repository we can ignore status checks, reviews and override protected files
|
// It's not allowed t overwrite protected files. Unless if the user is an
|
||||||
if ctx.userPerm.IsAdmin() {
|
// admin and the protected branch rule doesn't apply to admins.
|
||||||
return
|
if changedProtectedfiles && (!ctx.user.IsAdmin || protectBranch.ApplyToAdmins) {
|
||||||
}
|
|
||||||
|
|
||||||
// Now if we're not an admin - we can't overwrite protected files so fail now
|
|
||||||
if changedProtectedfiles {
|
|
||||||
log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
|
log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
|
||||||
ctx.JSON(http.StatusForbidden, private.Response{
|
ctx.JSON(http.StatusForbidden, private.Response{
|
||||||
UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
|
UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
|
||||||
|
@ -352,8 +348,12 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check all status checks and reviews are ok
|
// Check all status checks and reviews are ok
|
||||||
if err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil {
|
if pb, err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil {
|
||||||
if models.IsErrDisallowedToMerge(err) {
|
if models.IsErrDisallowedToMerge(err) {
|
||||||
|
// Allow this if the rule doesn't apply to admins and the user is an admin.
|
||||||
|
if ctx.user.IsAdmin && !pb.ApplyToAdmins {
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error())
|
log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error())
|
||||||
ctx.JSON(http.StatusForbidden, private.Response{
|
ctx.JSON(http.StatusForbidden, private.Response{
|
||||||
UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()),
|
UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()),
|
||||||
|
|
|
@ -237,6 +237,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
|
||||||
protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
|
protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
|
||||||
protectBranch.UnprotectedFilePatterns = f.UnprotectedFilePatterns
|
protectBranch.UnprotectedFilePatterns = f.UnprotectedFilePatterns
|
||||||
protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
|
protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
|
||||||
|
protectBranch.ApplyToAdmins = f.ApplyToAdmins
|
||||||
|
|
||||||
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
|
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
|
||||||
UserIDs: whitelistUsers,
|
UserIDs: whitelistUsers,
|
||||||
|
|
|
@ -162,6 +162,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch) *api
|
||||||
RequireSignedCommits: bp.RequireSignedCommits,
|
RequireSignedCommits: bp.RequireSignedCommits,
|
||||||
ProtectedFilePatterns: bp.ProtectedFilePatterns,
|
ProtectedFilePatterns: bp.ProtectedFilePatterns,
|
||||||
UnprotectedFilePatterns: bp.UnprotectedFilePatterns,
|
UnprotectedFilePatterns: bp.UnprotectedFilePatterns,
|
||||||
|
ApplyToAdmins: bp.ApplyToAdmins,
|
||||||
Created: bp.CreatedUnix.AsTime(),
|
Created: bp.CreatedUnix.AsTime(),
|
||||||
Updated: bp.UpdatedUnix.AsTime(),
|
Updated: bp.UpdatedUnix.AsTime(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -219,6 +219,7 @@ type ProtectBranchForm struct {
|
||||||
RequireSignedCommits bool
|
RequireSignedCommits bool
|
||||||
ProtectedFilePatterns string
|
ProtectedFilePatterns string
|
||||||
UnprotectedFilePatterns string
|
UnprotectedFilePatterns string
|
||||||
|
ApplyToAdmins bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the fields
|
// Validate validates the fields
|
||||||
|
|
|
@ -104,7 +104,7 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce
|
||||||
return ErrIsChecking
|
return ErrIsChecking
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := CheckPullBranchProtections(ctx, pr, false); err != nil {
|
if pb, err := CheckPullBranchProtections(ctx, pr, false); err != nil {
|
||||||
if !models.IsErrDisallowedToMerge(err) {
|
if !models.IsErrDisallowedToMerge(err) {
|
||||||
log.Error("Error whilst checking pull branch protection for %-v: %v", pr, err)
|
log.Error("Error whilst checking pull branch protection for %-v: %v", pr, err)
|
||||||
return err
|
return err
|
||||||
|
@ -117,8 +117,9 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// * if the doer is admin, they could skip the branch protection check
|
// * if the doer is admin, they could skip the branch protection check,
|
||||||
if adminSkipProtectionCheck {
|
// if that's allowed by the protected branch rule.
|
||||||
|
if adminSkipProtectionCheck && !pb.ApplyToAdmins {
|
||||||
if isRepoAdmin, errCheckAdmin := access_model.IsUserRepoAdmin(ctx, pr.BaseRepo, doer); errCheckAdmin != nil {
|
if isRepoAdmin, errCheckAdmin := access_model.IsUserRepoAdmin(ctx, pr.BaseRepo, doer); errCheckAdmin != nil {
|
||||||
log.Error("Unable to check if %-v is a repo admin in %-v: %v", doer, pr.BaseRepo, errCheckAdmin)
|
log.Error("Unable to check if %-v is a repo admin in %-v: %v", doer, pr.BaseRepo, errCheckAdmin)
|
||||||
return errCheckAdmin
|
return errCheckAdmin
|
||||||
|
|
|
@ -424,63 +424,64 @@ func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p a
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks)
|
// CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks).
|
||||||
func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (err error) {
|
// Returns the protected branch rule when `ErrDisallowedToMerge` is returned as error.
|
||||||
|
func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (protectedBranchRule *git_model.ProtectedBranch, err error) {
|
||||||
if err = pr.LoadBaseRepo(ctx); err != nil {
|
if err = pr.LoadBaseRepo(ctx); err != nil {
|
||||||
return fmt.Errorf("LoadBaseRepo: %w", err)
|
return nil, fmt.Errorf("LoadBaseRepo: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
|
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("LoadProtectedBranch: %v", err)
|
return nil, fmt.Errorf("LoadProtectedBranch: %v", err)
|
||||||
}
|
}
|
||||||
if pb == nil {
|
if pb == nil {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
isPass, err := IsPullCommitStatusPass(ctx, pr)
|
isPass, err := IsPullCommitStatusPass(ctx, pr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !isPass {
|
if !isPass {
|
||||||
return models.ErrDisallowedToMerge{
|
return pb, models.ErrDisallowedToMerge{
|
||||||
Reason: "Not all required status checks successful",
|
Reason: "Not all required status checks successful",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !issues_model.HasEnoughApprovals(ctx, pb, pr) {
|
if !issues_model.HasEnoughApprovals(ctx, pb, pr) {
|
||||||
return models.ErrDisallowedToMerge{
|
return pb, models.ErrDisallowedToMerge{
|
||||||
Reason: "Does not have enough approvals",
|
Reason: "Does not have enough approvals",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if issues_model.MergeBlockedByRejectedReview(ctx, pb, pr) {
|
if issues_model.MergeBlockedByRejectedReview(ctx, pb, pr) {
|
||||||
return models.ErrDisallowedToMerge{
|
return pb, models.ErrDisallowedToMerge{
|
||||||
Reason: "There are requested changes",
|
Reason: "There are requested changes",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pr) {
|
if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pr) {
|
||||||
return models.ErrDisallowedToMerge{
|
return pb, models.ErrDisallowedToMerge{
|
||||||
Reason: "There are official review requests",
|
Reason: "There are official review requests",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if issues_model.MergeBlockedByOutdatedBranch(pb, pr) {
|
if issues_model.MergeBlockedByOutdatedBranch(pb, pr) {
|
||||||
return models.ErrDisallowedToMerge{
|
return pb, models.ErrDisallowedToMerge{
|
||||||
Reason: "The head branch is behind the base branch",
|
Reason: "The head branch is behind the base branch",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if skipProtectedFilesCheck {
|
if skipProtectedFilesCheck {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if pb.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) {
|
if pb.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) {
|
||||||
return models.ErrDisallowedToMerge{
|
return pb, models.ErrDisallowedToMerge{
|
||||||
Reason: "Changed protected files",
|
Reason: "Changed protected files",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergedManually mark pr as merged manually
|
// MergedManually mark pr as merged manually
|
||||||
|
|
|
@ -158,7 +158,7 @@
|
||||||
{{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not .RequiredStatusCheckState.IsSuccess))}}
|
{{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not .RequiredStatusCheckState.IsSuccess))}}
|
||||||
|
|
||||||
{{/* admin can merge without checks, writer can merge when checks succeed */}}
|
{{/* admin can merge without checks, writer can merge when checks succeed */}}
|
||||||
{{$canMergeNow := and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}}
|
{{$canMergeNow := and (or (and $.IsRepoAdmin (not .ProtectedBranch.ApplyToAdmins)) (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}}
|
||||||
{{/* admin and writer both can make an auto merge schedule */}}
|
{{/* admin and writer both can make an auto merge schedule */}}
|
||||||
|
|
||||||
{{if $canMergeNow}}
|
{{if $canMergeNow}}
|
||||||
|
|
|
@ -260,6 +260,14 @@
|
||||||
<p class="help">{{ctx.Locale.Tr "repo.settings.block_outdated_branch_desc"}}</p>
|
<p class="help">{{ctx.Locale.Tr "repo.settings.block_outdated_branch_desc"}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.event_pull_request_enforcement"}}</h5>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="apply_to_admins" type="checkbox" {{if .Rule.ApplyToAdmins}}checked{{end}}>
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.enforce_on_admins"}}</label>
|
||||||
|
<p class="help">{{ctx.Locale.Tr "repo.settings.enforce_on_admins_desc"}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
12
templates/swagger/v1_json.tmpl
generated
12
templates/swagger/v1_json.tmpl
generated
|
@ -17756,6 +17756,10 @@
|
||||||
"description": "BranchProtection represents a branch protection for a repository",
|
"description": "BranchProtection represents a branch protection for a repository",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"apply_to_admins": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "ApplyToAdmins"
|
||||||
|
},
|
||||||
"approvals_whitelist_teams": {
|
"approvals_whitelist_teams": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
@ -18406,6 +18410,10 @@
|
||||||
"description": "CreateBranchProtectionOption options for creating a branch protection",
|
"description": "CreateBranchProtectionOption options for creating a branch protection",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"apply_to_admins": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "ApplyToAdmins"
|
||||||
|
},
|
||||||
"approvals_whitelist_teams": {
|
"approvals_whitelist_teams": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
@ -19577,6 +19585,10 @@
|
||||||
"description": "EditBranchProtectionOption options for editing a branch protection",
|
"description": "EditBranchProtectionOption options for editing a branch protection",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"apply_to_admins": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "ApplyToAdmins"
|
||||||
|
},
|
||||||
"approvals_whitelist_teams": {
|
"approvals_whitelist_teams": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
|
87
tests/integration/proctected_branch_test.go
Normal file
87
tests/integration/proctected_branch_test.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProtectedBranch_AdminEnforcement(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
session := loginUser(t, "user1")
|
||||||
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
||||||
|
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "add-readme", "README.md", "WIP")
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 1, Name: "repo1"})
|
||||||
|
|
||||||
|
req := NewRequestWithValues(t, "POST", "user1/repo1/compare/master...add-readme", map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, "user1/repo1/compare/master...add-readme"),
|
||||||
|
"title": "pull request",
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
t.Run("No protected branch", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", "/user1/repo1/pulls/1")
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
text := strings.TrimSpace(doc.doc.Find(".merge-section").Text())
|
||||||
|
assert.Contains(t, text, "This pull request can be merged automatically.")
|
||||||
|
assert.Contains(t, text, "'canMergeNow': true")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Without admin enforcement", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithValues(t, "POST", "/user1/repo1/settings/branches/edit", map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, "/user1/repo1/settings/branches/edit"),
|
||||||
|
"rule_name": "master",
|
||||||
|
"required_approvals": "1",
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", "/user1/repo1/pulls/1")
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
text := strings.TrimSpace(doc.doc.Find(".merge-section").Text())
|
||||||
|
assert.Contains(t, text, "This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.")
|
||||||
|
assert.Contains(t, text, "'canMergeNow': true")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("With admin enforcement", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
protectedBranch := unittest.AssertExistsAndLoadBean(t, &git_model.ProtectedBranch{RuleName: "master", RepoID: repo.ID})
|
||||||
|
req := NewRequestWithValues(t, "POST", "/user1/repo1/settings/branches/edit", map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, "/user1/repo1/settings/branches/edit"),
|
||||||
|
"rule_name": "master",
|
||||||
|
"rule_id": strconv.FormatInt(protectedBranch.ID, 10),
|
||||||
|
"required_approvals": "1",
|
||||||
|
"apply_to_admins": "true",
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", "/user1/repo1/pulls/1")
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
text := strings.TrimSpace(doc.doc.Find(".merge-section").Text())
|
||||||
|
assert.Contains(t, text, "This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.")
|
||||||
|
assert.Contains(t, text, "'canMergeNow': false")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue