Supports wildcard protected branch (#20825)

This PR introduce glob match for protected branch name. The separator is
`/` and you can use `*` matching non-separator chars and use `**` across
separator.

It also supports input an exist or non-exist branch name as matching
condition and branch name condition has high priority than glob rule.

Should fix #2529 and #15705

screenshots

<img width="1160" alt="image"
src="https://user-images.githubusercontent.com/81045/205651179-ebb5492a-4ade-4bb4-a13c-965e8c927063.png">

Co-authored-by: zeripath <art27@cantab.net>
forgejo
Lunny Xiao 1 year ago committed by GitHub
parent cc1f8cbe96
commit 2782c14396
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -6,428 +6,15 @@ package git
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob"
) )
// ProtectedBranch struct
type ProtectedBranch struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s)"`
BranchName string `xorm:"UNIQUE(s)"`
CanPush bool `xorm:"NOT NULL DEFAULT false"`
EnableWhitelist bool
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
StatusCheckContexts []string `xorm:"JSON TEXT"`
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
BlockOnOfficialReviewRequests 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"`
UnprotectedFilePatterns string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(ProtectedBranch))
db.RegisterModel(new(DeletedBranch))
db.RegisterModel(new(RenamedBranch))
}
// IsProtected returns if the branch is protected
func (protectBranch *ProtectedBranch) IsProtected() bool {
return protectBranch.ID > 0
}
// CanUserPush returns if some user could push to this protected branch
func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, userID int64) bool {
if !protectBranch.CanPush {
return false
}
if !protectBranch.EnableWhitelist {
if user, err := user_model.GetUserByID(ctx, userID); err != nil {
log.Error("GetUserByID: %v", err)
return false
} else if repo, err := repo_model.GetRepositoryByID(ctx, protectBranch.RepoID); err != nil {
log.Error("repo_model.GetRepositoryByID: %v", err)
return false
} else if writeAccess, err := access_model.HasAccessUnit(ctx, user, repo, unit.TypeCode, perm.AccessModeWrite); err != nil {
log.Error("HasAccessUnit: %v", err)
return false
} else {
return writeAccess
}
}
if base.Int64sContains(protectBranch.WhitelistUserIDs, userID) {
return true
}
if len(protectBranch.WhitelistTeamIDs) == 0 {
return false
}
in, err := organization.IsUserInTeams(ctx, userID, protectBranch.WhitelistTeamIDs)
if err != nil {
log.Error("IsUserInTeams: %v", err)
return false
}
return in
}
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
func IsUserMergeWhitelisted(ctx context.Context, protectBranch *ProtectedBranch, userID int64, permissionInRepo access_model.Permission) bool {
if !protectBranch.EnableMergeWhitelist {
// Then we need to fall back on whether the user has write permission
return permissionInRepo.CanWrite(unit.TypeCode)
}
if base.Int64sContains(protectBranch.MergeWhitelistUserIDs, userID) {
return true
}
if len(protectBranch.MergeWhitelistTeamIDs) == 0 {
return false
}
in, err := organization.IsUserInTeams(ctx, userID, protectBranch.MergeWhitelistTeamIDs)
if err != nil {
log.Error("IsUserInTeams: %v", err)
return false
}
return in
}
// IsUserOfficialReviewer check if user is official reviewer for the branch (counts towards required approvals)
func IsUserOfficialReviewer(ctx context.Context, protectBranch *ProtectedBranch, user *user_model.User) (bool, error) {
repo, err := repo_model.GetRepositoryByID(ctx, protectBranch.RepoID)
if err != nil {
return false, err
}
if !protectBranch.EnableApprovalsWhitelist {
// Anyone with write access is considered official reviewer
writeAccess, err := access_model.HasAccessUnit(ctx, user, repo, unit.TypeCode, perm.AccessModeWrite)
if err != nil {
return false, err
}
return writeAccess, nil
}
if base.Int64sContains(protectBranch.ApprovalsWhitelistUserIDs, user.ID) {
return true, nil
}
inTeam, err := organization.IsUserInTeams(ctx, user.ID, protectBranch.ApprovalsWhitelistTeamIDs)
if err != nil {
return false, err
}
return inTeam, nil
}
// GetProtectedFilePatterns parses a semicolon separated list of protected file patterns and returns a glob.Glob slice
func (protectBranch *ProtectedBranch) GetProtectedFilePatterns() []glob.Glob {
return getFilePatterns(protectBranch.ProtectedFilePatterns)
}
// GetUnprotectedFilePatterns parses a semicolon separated list of unprotected file patterns and returns a glob.Glob slice
func (protectBranch *ProtectedBranch) GetUnprotectedFilePatterns() []glob.Glob {
return getFilePatterns(protectBranch.UnprotectedFilePatterns)
}
func getFilePatterns(filePatterns string) []glob.Glob {
extarr := make([]glob.Glob, 0, 10)
for _, expr := range strings.Split(strings.ToLower(filePatterns), ";") {
expr = strings.TrimSpace(expr)
if expr != "" {
if g, err := glob.Compile(expr, '.', '/'); err != nil {
log.Info("Invalid glob expression '%s' (skipped): %v", expr, err)
} else {
extarr = append(extarr, g)
}
}
}
return extarr
}
// MergeBlockedByProtectedFiles returns true if merge is blocked by protected files change
func (protectBranch *ProtectedBranch) MergeBlockedByProtectedFiles(changedProtectedFiles []string) bool {
glob := protectBranch.GetProtectedFilePatterns()
if len(glob) == 0 {
return false
}
return len(changedProtectedFiles) > 0
}
// IsProtectedFile return if path is protected
func (protectBranch *ProtectedBranch) IsProtectedFile(patterns []glob.Glob, path string) bool {
if len(patterns) == 0 {
patterns = protectBranch.GetProtectedFilePatterns()
if len(patterns) == 0 {
return false
}
}
lpath := strings.ToLower(strings.TrimSpace(path))
r := false
for _, pat := range patterns {
if pat.Match(lpath) {
r = true
break
}
}
return r
}
// IsUnprotectedFile return if path is unprotected
func (protectBranch *ProtectedBranch) IsUnprotectedFile(patterns []glob.Glob, path string) bool {
if len(patterns) == 0 {
patterns = protectBranch.GetUnprotectedFilePatterns()
if len(patterns) == 0 {
return false
}
}
lpath := strings.ToLower(strings.TrimSpace(path))
r := false
for _, pat := range patterns {
if pat.Match(lpath) {
r = true
break
}
}
return r
}
// GetProtectedBranchBy getting protected branch by ID/Name
func GetProtectedBranchBy(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) {
rel := &ProtectedBranch{RepoID: repoID, BranchName: branchName}
has, err := db.GetByBean(ctx, rel)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return rel, nil
}
// WhitelistOptions represent all sorts of whitelists used for protected branches
type WhitelistOptions struct {
UserIDs []int64
TeamIDs []int64
MergeUserIDs []int64
MergeTeamIDs []int64
ApprovalsUserIDs []int64
ApprovalsTeamIDs []int64
}
// UpdateProtectBranch saves branch protection options of repository.
// If ID is 0, it creates a new record. Otherwise, updates existing record.
// This function also performs check if whitelist user and team's IDs have been changed
// to avoid unnecessary whitelist delete and regenerate.
func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, protectBranch *ProtectedBranch, opts WhitelistOptions) (err error) {
if err = repo.GetOwner(ctx); err != nil {
return fmt.Errorf("GetOwner: %w", err)
}
whitelist, err := updateUserWhitelist(ctx, repo, protectBranch.WhitelistUserIDs, opts.UserIDs)
if err != nil {
return err
}
protectBranch.WhitelistUserIDs = whitelist
whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs)
if err != nil {
return err
}
protectBranch.MergeWhitelistUserIDs = whitelist
whitelist, err = updateApprovalWhitelist(ctx, repo, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs)
if err != nil {
return err
}
protectBranch.ApprovalsWhitelistUserIDs = whitelist
// if the repo is in an organization
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.WhitelistTeamIDs, opts.TeamIDs)
if err != nil {
return err
}
protectBranch.WhitelistTeamIDs = whitelist
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs)
if err != nil {
return err
}
protectBranch.MergeWhitelistTeamIDs = whitelist
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.ApprovalsWhitelistTeamIDs, opts.ApprovalsTeamIDs)
if err != nil {
return err
}
protectBranch.ApprovalsWhitelistTeamIDs = whitelist
// Make sure protectBranch.ID is not 0 for whitelists
if protectBranch.ID == 0 {
if _, err = db.GetEngine(ctx).Insert(protectBranch); err != nil {
return fmt.Errorf("Insert: %w", err)
}
return nil
}
if _, err = db.GetEngine(ctx).ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
return fmt.Errorf("Update: %w", err)
}
return nil
}
// GetProtectedBranches get all protected branches
func GetProtectedBranches(ctx context.Context, repoID int64) ([]*ProtectedBranch, error) {
protectedBranches := make([]*ProtectedBranch, 0)
return protectedBranches, db.GetEngine(ctx).Find(&protectedBranches, &ProtectedBranch{RepoID: repoID})
}
// IsProtectedBranch checks if branch is protected
func IsProtectedBranch(ctx context.Context, repoID int64, branchName string) (bool, error) {
protectedBranch := &ProtectedBranch{
RepoID: repoID,
BranchName: branchName,
}
has, err := db.GetEngine(ctx).Exist(protectedBranch)
if err != nil {
return true, err
}
return has, nil
}
// updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which have explicit read or write access to the repo.
func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasUsersChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist)
if !hasUsersChanged {
return currentWhitelist, nil
}
whitelist = make([]int64, 0, len(newWhitelist))
for _, userID := range newWhitelist {
if reader, err := access_model.IsRepoReader(ctx, repo, userID); err != nil {
return nil, err
} else if !reader {
continue
}
whitelist = append(whitelist, userID)
}
return whitelist, err
}
// updateUserWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which have write access to the repo.
func updateUserWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasUsersChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist)
if !hasUsersChanged {
return currentWhitelist, nil
}
whitelist = make([]int64, 0, len(newWhitelist))
for _, userID := range newWhitelist {
user, err := user_model.GetUserByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("GetUserByID [user_id: %d, repo_id: %d]: %w", userID, repo.ID, err)
}
perm, err := access_model.GetUserRepoPermission(ctx, repo, user)
if err != nil {
return nil, fmt.Errorf("GetUserRepoPermission [user_id: %d, repo_id: %d]: %w", userID, repo.ID, err)
}
if !perm.CanWrite(unit.TypeCode) {
continue // Drop invalid user ID
}
whitelist = append(whitelist, userID)
}
return whitelist, err
}
// updateTeamWhitelist checks whether the team whitelist changed and returns a whitelist with
// the teams from newWhitelist which have write access to the repo.
func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasTeamsChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist)
if !hasTeamsChanged {
return currentWhitelist, nil
}
teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
if err != nil {
return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %w", repo.OwnerID, repo.ID, err)
}
whitelist = make([]int64, 0, len(teams))
for i := range teams {
if util.SliceContains(newWhitelist, teams[i].ID) {
whitelist = append(whitelist, teams[i].ID)
}
}
return whitelist, err
}
// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
func DeleteProtectedBranch(ctx context.Context, repoID, id int64) (err error) {
protectedBranch := &ProtectedBranch{
RepoID: repoID,
ID: id,
}
if affected, err := db.GetEngine(ctx).Delete(protectedBranch); err != nil {
return err
} else if affected != 1 {
return fmt.Errorf("delete protected branch ID(%v) failed", id)
}
return nil
}
// DeletedBranch struct // DeletedBranch struct
type DeletedBranch struct { type DeletedBranch struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
@ -439,6 +26,11 @@ type DeletedBranch struct {
DeletedUnix timeutil.TimeStamp `xorm:"INDEX created"` DeletedUnix timeutil.TimeStamp `xorm:"INDEX created"`
} }
func init() {
db.RegisterModel(new(DeletedBranch))
db.RegisterModel(new(RenamedBranch))
}
// AddDeletedBranch adds a deleted branch to the database // AddDeletedBranch adds a deleted branch to the database
func AddDeletedBranch(ctx context.Context, repoID int64, branchName, commit string, deletedByID int64) error { func AddDeletedBranch(ctx context.Context, repoID int64, branchName, commit string, deletedByID int64) error {
deletedBranch := &DeletedBranch{ deletedBranch := &DeletedBranch{
@ -556,17 +148,25 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
} }
// 2. Update protected branch if needed // 2. Update protected branch if needed
protectedBranch, err := GetProtectedBranchBy(ctx, repo.ID, from) protectedBranch, err := GetProtectedBranchRuleByName(ctx, repo.ID, from)
if err != nil { if err != nil {
return err return err
} }
if protectedBranch != nil { if protectedBranch != nil {
protectedBranch.BranchName = to protectedBranch.RuleName = to
_, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch) _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch)
if err != nil { if err != nil {
return err return err
} }
} else {
protected, err := IsBranchProtected(ctx, repo.ID, from)
if err != nil {
return err
}
if protected {
return ErrBranchIsProtected
}
} }
// 3. Update all not merged pull request base branch name // 3. Update all not merged pull request base branch name

@ -105,8 +105,8 @@ func TestRenameBranch(t *testing.T) {
defer committer.Close() defer committer.Close()
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, git_model.UpdateProtectBranch(ctx, repo1, &git_model.ProtectedBranch{ assert.NoError(t, git_model.UpdateProtectBranch(ctx, repo1, &git_model.ProtectedBranch{
RepoID: repo1.ID, RepoID: repo1.ID,
BranchName: "master", RuleName: "master",
}, git_model.WhitelistOptions{})) }, git_model.WhitelistOptions{}))
assert.NoError(t, committer.Commit()) assert.NoError(t, committer.Commit())
@ -131,8 +131,8 @@ func TestRenameBranch(t *testing.T) {
assert.Equal(t, int64(1), renamedBranch.RepoID) assert.Equal(t, int64(1), renamedBranch.RepoID)
unittest.AssertExistsAndLoadBean(t, &git_model.ProtectedBranch{ unittest.AssertExistsAndLoadBean(t, &git_model.ProtectedBranch{
RepoID: repo1.ID, RepoID: repo1.ID,
BranchName: "main", RuleName: "main",
}) })
} }

@ -0,0 +1,501 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"errors"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob"
"github.com/gobwas/glob/syntax"
)
var ErrBranchIsProtected = errors.New("branch is protected")
// ProtectedBranch struct
type ProtectedBranch struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s)"`
Repo *repo_model.Repository `xorm:"-"`
RuleName string `xorm:"'branch_name' UNIQUE(s)"` // a branch name or a glob match to branch name
globRule glob.Glob `xorm:"-"`
isPlainName bool `xorm:"-"`
CanPush bool `xorm:"NOT NULL DEFAULT false"`
EnableWhitelist bool
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
StatusCheckContexts []string `xorm:"JSON TEXT"`
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"`
BlockOnOfficialReviewRequests 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"`
UnprotectedFilePatterns string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(ProtectedBranch))
}
// IsRuleNameSpecial return true if it contains special character
func IsRuleNameSpecial(ruleName string) bool {
for i := 0; i < len(ruleName); i++ {
if syntax.Special(ruleName[i]) {
return true
}
}
return false
}
func (protectBranch *ProtectedBranch) loadGlob() {
if protectBranch.globRule == nil {
var err error
protectBranch.globRule, err = glob.Compile(protectBranch.RuleName, '/')
if err != nil {
log.Warn("Invalid glob rule for ProtectedBranch[%d]: %s %v", protectBranch.ID, protectBranch.RuleName, err)
protectBranch.globRule = glob.MustCompile(glob.QuoteMeta(protectBranch.RuleName), '/')
}
protectBranch.isPlainName = !IsRuleNameSpecial(protectBranch.RuleName)
}
}
// Match tests if branchName matches the rule
func (protectBranch *ProtectedBranch) Match(branchName string) bool {
protectBranch.loadGlob()
if protectBranch.isPlainName {
return strings.EqualFold(protectBranch.RuleName, branchName)
}
return protectBranch.globRule.Match(branchName)
}
func (protectBranch *ProtectedBranch) LoadRepo(ctx context.Context) (err error) {
if protectBranch.Repo != nil {
return nil
}
protectBranch.Repo, err = repo_model.GetRepositoryByID(ctx, protectBranch.RepoID)
return err
}
// CanUserPush returns if some user could push to this protected branch
func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *user_model.User) bool {
if !protectBranch.CanPush {
return false
}
if !protectBranch.EnableWhitelist {
if err := protectBranch.LoadRepo(ctx); err != nil {
log.Error("LoadRepo: %v", err)
return false
}
writeAccess, err := access_model.HasAccessUnit(ctx, user, protectBranch.Repo, unit.TypeCode, perm.AccessModeWrite)
if err != nil {
log.Error("HasAccessUnit: %v", err)
return false
}
return writeAccess
}
if base.Int64sContains(protectBranch.WhitelistUserIDs, user.ID) {
return true
}
if len(protectBranch.WhitelistTeamIDs) == 0 {
return false
}
in, err := organization.IsUserInTeams(ctx, user.ID, protectBranch.WhitelistTeamIDs)
if err != nil {
log.Error("IsUserInTeams: %v", err)
return false
}
return in
}
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
func IsUserMergeWhitelisted(ctx context.Context, protectBranch *ProtectedBranch, userID int64, permissionInRepo access_model.Permission) bool {
if !protectBranch.EnableMergeWhitelist {
// Then we need to fall back on whether the user has write permission
return permissionInRepo.CanWrite(unit.TypeCode)
}
if base.Int64sContains(protectBranch.MergeWhitelistUserIDs, userID) {
return true
}
if len(protectBranch.MergeWhitelistTeamIDs) == 0 {
return false
}
in, err := organization.IsUserInTeams(ctx, userID, protectBranch.MergeWhitelistTeamIDs)
if err != nil {
log.Error("IsUserInTeams: %v", err)
return false
}
return in
}
// IsUserOfficialReviewer check if user is official reviewer for the branch (counts towards required approvals)
func IsUserOfficialReviewer(ctx context.Context, protectBranch *ProtectedBranch, user *user_model.User) (bool, error) {
repo, err := repo_model.GetRepositoryByID(ctx, protectBranch.RepoID)
if err != nil {
return false, err
}
if !protectBranch.EnableApprovalsWhitelist {
// Anyone with write access is considered official reviewer
writeAccess, err := access_model.HasAccessUnit(ctx, user, repo, unit.TypeCode, perm.AccessModeWrite)
if err != nil {
return false, err
}
return writeAccess, nil
}
if base.Int64sContains(protectBranch.ApprovalsWhitelistUserIDs, user.ID) {
return true, nil
}
inTeam, err := organization.IsUserInTeams(ctx, user.ID, protectBranch.ApprovalsWhitelistTeamIDs)
if err != nil {
return false, err
}
return inTeam, nil
}
// GetProtectedFilePatterns parses a semicolon separated list of protected file patterns and returns a glob.Glob slice
func (protectBranch *ProtectedBranch) GetProtectedFilePatterns() []glob.Glob {
return getFilePatterns(protectBranch.ProtectedFilePatterns)
}
// GetUnprotectedFilePatterns parses a semicolon separated list of unprotected file patterns and returns a glob.Glob slice
func (protectBranch *ProtectedBranch) GetUnprotectedFilePatterns() []glob.Glob {
return getFilePatterns(protectBranch.UnprotectedFilePatterns)
}
func getFilePatterns(filePatterns string) []glob.Glob {
extarr := make([]glob.Glob, 0, 10)
for _, expr := range strings.Split(strings.ToLower(filePatterns), ";") {
expr = strings.TrimSpace(expr)
if expr != "" {
if g, err := glob.Compile(expr, '.', '/'); err != nil {
log.Info("Invalid glob expression '%s' (skipped): %v", expr, err)
} else {
extarr = append(extarr, g)
}
}
}
return extarr
}
// MergeBlockedByProtectedFiles returns true if merge is blocked by protected files change
func (protectBranch *ProtectedBranch) MergeBlockedByProtectedFiles(changedProtectedFiles []string) bool {
glob := protectBranch.GetProtectedFilePatterns()
if len(glob) == 0 {
return false
}
return len(changedProtectedFiles) > 0
}
// IsProtectedFile return if path is protected
func (protectBranch *ProtectedBranch) IsProtectedFile(patterns []glob.Glob, path string) bool {
if len(patterns) == 0 {
patterns = protectBranch.GetProtectedFilePatterns()
if len(patterns) == 0 {
return false
}
}
lpath := strings.ToLower(strings.TrimSpace(path))
r := false
for _, pat := range patterns {
if pat.Match(lpath) {
r = true
break
}
}
return r
}
// IsUnprotectedFile return if path is unprotected
func (protectBranch *ProtectedBranch) IsUnprotectedFile(patterns []glob.Glob, path string) bool {
if len(patterns) == 0 {
patterns = protectBranch.GetUnprotectedFilePatterns()
if len(patterns) == 0 {
return false
}
}
lpath := strings.ToLower(strings.TrimSpace(path))
r := false
for _, pat := range patterns {
if pat.Match(lpath) {
r = true
break
}
}
return r
}
// GetProtectedBranchRuleByName getting protected branch rule by name
func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) {
rel := &ProtectedBranch{RepoID: repoID, RuleName: ruleName}
has, err := db.GetByBean(ctx, rel)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return rel, nil
}
// GetProtectedBranchRuleByID getting protected branch rule by rule ID
func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*ProtectedBranch, error) {
rel := &ProtectedBranch{ID: ruleID, RepoID: repoID}
has, err := db.GetByBean(ctx, rel)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return rel, nil
}
// WhitelistOptions represent all sorts of whitelists used for protected branches
type WhitelistOptions struct {
UserIDs []int64
TeamIDs []int64
MergeUserIDs []int64
MergeTeamIDs []int64
ApprovalsUserIDs []int64
ApprovalsTeamIDs []int64
}
// UpdateProtectBranch saves branch protection options of repository.
// If ID is 0, it creates a new record. Otherwise, updates existing record.
// This function also performs check if whitelist user and team's IDs have been changed
// to avoid unnecessary whitelist delete and regenerate.
func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, protectBranch *ProtectedBranch, opts WhitelistOptions) (err error) {
if err = repo.GetOwner(ctx); err != nil {
return fmt.Errorf("GetOwner: %v", err)
}
whitelist, err := updateUserWhitelist(ctx, repo, protectBranch.WhitelistUserIDs, opts.UserIDs)
if err != nil {
return err
}
protectBranch.WhitelistUserIDs = whitelist
whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs)
if err != nil {
return err
}
protectBranch.MergeWhitelistUserIDs = whitelist
whitelist, err = updateApprovalWhitelist(ctx, repo, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs)
if err != nil {
return err
}
protectBranch.ApprovalsWhitelistUserIDs = whitelist
// if the repo is in an organization
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.WhitelistTeamIDs, opts.TeamIDs)
if err != nil {
return err
}
protectBranch.WhitelistTeamIDs = whitelist
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs)
if err != nil {
return err
}
protectBranch.MergeWhitelistTeamIDs = whitelist
whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.ApprovalsWhitelistTeamIDs, opts.ApprovalsTeamIDs)
if err != nil {
return err
}
protectBranch.ApprovalsWhitelistTeamIDs = whitelist
// Make sure protectBranch.ID is not 0 for whitelists
if protectBranch.ID == 0 {
if _, err = db.GetEngine(ctx).Insert(protectBranch); err != nil {
return fmt.Errorf("Insert: %v", err)
}
return nil
}
if _, err = db.GetEngine(ctx).ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
return fmt.Errorf("Update: %v", err)
}
return nil
}
// updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which have explicit read or write access to the repo.
func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasUsersChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist)
if !hasUsersChanged {
return currentWhitelist, nil
}
whitelist = make([]int64, 0, len(newWhitelist))
for _, userID := range newWhitelist {
if reader, err := access_model.IsRepoReader(ctx, repo, userID); err != nil {
return nil, err
} else if !reader {
continue
}
whitelist = append(whitelist, userID)
}
return whitelist, err
}
// updateUserWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which have write access to the repo.
func updateUserWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasUsersChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist)
if !hasUsersChanged {
return currentWhitelist, nil
}
whitelist = make([]int64, 0, len(newWhitelist))
for _, userID := range newWhitelist {
user, err := user_model.GetUserByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("GetUserByID [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
}
perm, err := access_model.GetUserRepoPermission(ctx, repo, user)
if err != nil {
return nil, fmt.Errorf("GetUserRepoPermission [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
}
if !perm.CanWrite(unit.TypeCode) {
continue // Drop invalid user ID
}
whitelist = append(whitelist, userID)
}
return whitelist, err
}
// updateTeamWhitelist checks whether the team whitelist changed and returns a whitelist with
// the teams from newWhitelist which have write access to the repo.
func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasTeamsChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist)
if !hasTeamsChanged {
return currentWhitelist, nil
}
teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
if err != nil {
return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
}
whitelist = make([]int64, 0, len(teams))
for i := range teams {
if util.SliceContains(newWhitelist, teams[i].ID) {
whitelist = append(whitelist, teams[i].ID)
}
}
return whitelist, err
}
// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
func DeleteProtectedBranch(ctx context.Context, repoID, id int64) (err error) {
protectedBranch := &ProtectedBranch{
RepoID: repoID,
ID: id,
}
if affected, err := db.GetEngine(ctx).Delete(protectedBranch); err != nil {
return err
} else if affected != 1 {
return fmt.Errorf("delete protected branch ID(%v) failed", id)
}
return nil
}
// RemoveUserIDFromProtectedBranch remove all user ids from protected branch options
func RemoveUserIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID int64) error {
lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistUserIDs), len(p.ApprovalsWhitelistUserIDs), len(p.MergeWhitelistUserIDs)
p.WhitelistUserIDs = util.SliceRemoveAll(p.WhitelistUserIDs, userID)
p.ApprovalsWhitelistUserIDs = util.SliceRemoveAll(p.ApprovalsWhitelistUserIDs, userID)
p.MergeWhitelistUserIDs = util.SliceRemoveAll(p.MergeWhitelistUserIDs, userID)
if lenIDs != len(p.WhitelistUserIDs) || lenApprovalIDs != len(p.ApprovalsWhitelistUserIDs) ||
lenMergeIDs != len(p.MergeWhitelistUserIDs) {
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(
"whitelist_user_i_ds",
"merge_whitelist_user_i_ds",
"approvals_whitelist_user_i_ds",
).Update(p); err != nil {
return fmt.Errorf("updateProtectedBranches: %v", err)
}
}
return nil
}
// RemoveTeamIDFromProtectedBranch remove all team ids from protected branch options
func RemoveTeamIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, teamID int64) error {
lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistTeamIDs), len(p.ApprovalsWhitelistTeamIDs), len(p.MergeWhitelistTeamIDs)
p.WhitelistTeamIDs = util.SliceRemoveAll(p.WhitelistTeamIDs, teamID)
p.ApprovalsWhitelistTeamIDs = util.SliceRemoveAll(p.ApprovalsWhitelistTeamIDs, teamID)
p.MergeWhitelistTeamIDs = util.SliceRemoveAll(p.MergeWhitelistTeamIDs, teamID)
if lenIDs != len(p.WhitelistTeamIDs) ||
lenApprovalIDs != len(p.ApprovalsWhitelistTeamIDs) ||
lenMergeIDs != len(p.MergeWhitelistTeamIDs) {
if _, err := db.GetEngine(ctx).ID(p.ID).Cols(
"whitelist_team_i_ds",
"merge_whitelist_team_i_ds",
"approvals_whitelist_team_i_ds",
).Update(p); err != nil {
return fmt.Errorf("updateProtectedBranches: %v", err)
}
}
return nil
}

@ -0,0 +1,86 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"sort"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/git"
"github.com/gobwas/glob"
)
type ProtectedBranchRules []*ProtectedBranch
func (rules ProtectedBranchRules) GetFirstMatched(branchName string) *ProtectedBranch {
for _, rule := range rules {
if rule.Match(branchName) {
return rule
}
}
return nil
}
func (rules ProtectedBranchRules) sort() {
sort.Slice(rules, func(i, j int) bool {
rules[i].loadGlob()
rules[j].loadGlob()
if rules[i].isPlainName {
if !rules[j].isPlainName {
return true
}
} else if rules[j].isPlainName {
return true
}
return rules[i].CreatedUnix < rules[j].CreatedUnix
})
}
// FindRepoProtectedBranchRules load all repository's protected rules
func FindRepoProtectedBranchRules(ctx context.Context, repoID int64) (ProtectedBranchRules, error) {
var rules ProtectedBranchRules
err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Asc("created_unix").Find(&rules)
if err != nil {
return nil, err
}
rules.sort()
return rules, nil
}
// FindAllMatchedBranches find all matched branches
func FindAllMatchedBranches(ctx context.Context, gitRepo *git.Repository, ruleName string) ([]string, error) {
// FIXME: how many should we get?
branches, _, err := gitRepo.GetBranchNames(0, 9999999)
if err != nil {
return nil, err
}
rule := glob.MustCompile(ruleName)
results := make([]string, 0, len(branches))
for _, branch := range branches {
if rule.Match(branch) {
results = append(results, branch)
}
}
return results, nil
}
// GetFirstMatchProtectedBranchRule returns the first matched rules
func GetFirstMatchProtectedBranchRule(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) {
rules, err := FindRepoProtectedBranchRules(ctx, repoID)
if err != nil {
return nil, err
}
return rules.GetFirstMatched(branchName), nil
}
// IsBranchProtected checks if branch is protected
func IsBranchProtected(ctx context.Context, repoID int64, branchName string) (bool, error) {
rule, err := GetFirstMatchProtectedBranchRule(ctx, repoID, branchName)
if err != nil {
return false, err
}
return rule != nil, nil
}

@ -0,0 +1,78 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestBranchRuleMatch(t *testing.T) {
kases := []struct {
Rule string
BranchName string
ExpectedMatch bool
}{
{
Rule: "release/*",
BranchName: "release/v1.17",
ExpectedMatch: true,
},
{
Rule: "release/**/v1.17",
BranchName: "release/test/v1.17",
ExpectedMatch: true,
},
{
Rule: "release/**/v1.17",
BranchName: "release/test/1/v1.17",
ExpectedMatch: true,
},
{
Rule: "release/*/v1.17",
BranchName: "release/test/1/v1.17",
ExpectedMatch: false,
},
{
Rule: "release/v*",
BranchName: "release/v1.16",
ExpectedMatch: true,
},
{
Rule: "*",
BranchName: "release/v1.16",
ExpectedMatch: false,
},
{
Rule: "**",
BranchName: "release/v1.16",
ExpectedMatch: true,
},
{
Rule: "main",
BranchName: "main",
ExpectedMatch: true,
},
{
Rule: "master",
BranchName: "main",
ExpectedMatch: false,
},
}
for _, kase := range kases {
pb := ProtectedBranch{RuleName: kase.Rule}
var should, infact string
if !kase.ExpectedMatch {
should = " not"
} else {
infact = " not"
}
assert.EqualValues(t, kase.ExpectedMatch, pb.Match(kase.BranchName),
fmt.Sprintf("%s should%s match %s but it is%s", kase.BranchName, should, kase.Rule, infact),
)
}
}

@ -164,9 +164,8 @@ type PullRequest struct {
HeadBranch string HeadBranch string
HeadCommitID string `xorm:"-"` HeadCommitID string `xorm:"-"`
BaseBranch string BaseBranch string
ProtectedBranch *git_model.ProtectedBranch `xorm:"-"` MergeBase string `xorm:"VARCHAR(40)"`
MergeBase string `xorm:"VARCHAR(40)"` AllowMaintainerEdit bool `xorm:"NOT NULL DEFAULT false"`
AllowMaintainerEdit bool `xorm:"NOT NULL DEFAULT false"`
HasMerged bool `xorm:"INDEX"` HasMerged bool `xorm:"INDEX"`
MergedCommitID string `xorm:"VARCHAR(40)"` MergedCommitID string `xorm:"VARCHAR(40)"`
@ -293,23 +292,6 @@ func (pr *PullRequest) LoadIssue(ctx context.Context) (err error) {
return err return err
} }
// LoadProtectedBranch loads the protected branch of the base branch
func (pr *PullRequest) LoadProtectedBranch(ctx context.Context) (err error) {
if pr.ProtectedBranch == nil {
if pr.BaseRepo == nil {
if pr.BaseRepoID == 0 {
return nil
}
pr.BaseRepo, err = repo_model.GetRepositoryByID(ctx, pr.BaseRepoID)
if err != nil {
return
}
}
pr.ProtectedBranch, err = git_model.GetProtectedBranchBy(ctx, pr.BaseRepo.ID, pr.BaseBranch)
}
return err
}
// ReviewCount represents a count of Reviews // ReviewCount represents a count of Reviews
type ReviewCount struct { type ReviewCount struct {
IssueID int64 IssueID int64

@ -263,15 +263,17 @@ func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewers ...*user_mo
if err != nil { if err != nil {
return false, err return false, err
} }
if err = pr.LoadProtectedBranch(ctx); err != nil {
rule, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return false, err return false, err
} }
if pr.ProtectedBranch == nil { if rule == nil {
return false, nil return false, nil
} }
for _, reviewer := range reviewers { for _, reviewer := range reviewers {
official, err := git_model.IsUserOfficialReviewer(ctx, pr.ProtectedBranch, reviewer) official, err := git_model.IsUserOfficialReviewer(ctx, rule, reviewer)
if official || err != nil { if official || err != nil {
return official, err return official, err
} }
@ -286,18 +288,19 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio
if err != nil { if err != nil {
return false, err return false, err
} }
if err = pr.LoadProtectedBranch(ctx); err != nil { pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return false, err return false, err
} }
if pr.ProtectedBranch == nil { if pb == nil {
return false, nil return false, nil
} }
if !pr.ProtectedBranch.EnableApprovalsWhitelist { if !pb.EnableApprovalsWhitelist {
return team.UnitAccessMode(ctx, unit.TypeCode) >= perm.AccessModeWrite, nil return team.UnitAccessMode(ctx, unit.TypeCode) >= perm.AccessModeWrite, nil
} }
return base.Int64sContains(pr.ProtectedBranch.ApprovalsWhitelistTeamIDs, team.ID), nil return base.Int64sContains(pb.ApprovalsWhitelistTeamIDs, team.ID), nil
} }
// CreateReview creates a new review based on opts // CreateReview creates a new review based on opts

@ -378,7 +378,6 @@ func DeleteTeam(t *organization.Team) error {
return err return err
} }
defer committer.Close() defer committer.Close()
sess := db.GetEngine(ctx)
if err := t.LoadRepositories(ctx); err != nil { if err := t.LoadRepositories(ctx); err != nil {
return err return err
@ -391,27 +390,15 @@ func DeleteTeam(t *organization.Team) error {
// update branch protections // update branch protections
{ {
protections := make([]*git_model.ProtectedBranch, 0, 10) protections := make([]*git_model.ProtectedBranch, 0, 10)
err := sess.In("repo_id", err := db.GetEngine(ctx).In("repo_id",
builder.Select("id").From("repository").Where(builder.Eq{"owner_id": t.OrgID})). builder.Select("id").From("repository").Where(builder.Eq{"owner_id": t.OrgID})).
Find(&protections) Find(&protections)
if err != nil { if err != nil {
return fmt.Errorf("findProtectedBranches: %w", err) return fmt.Errorf("findProtectedBranches: %w", err)
} }
for _, p := range protections { for _, p := range protections {
lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistTeamIDs), len(p.ApprovalsWhitelistTeamIDs), len(p.MergeWhitelistTeamIDs) if err := git_model.RemoveTeamIDFromProtectedBranch(ctx, p, t.ID); err != nil {
p.WhitelistTeamIDs = util.SliceRemoveAll(p.WhitelistTeamIDs, t.ID) return err
p.ApprovalsWhitelistTeamIDs = util.SliceRemoveAll(p.ApprovalsWhitelistTeamIDs, t.ID)
p.MergeWhitelistTeamIDs = util.SliceRemoveAll(p.MergeWhitelistTeamIDs, t.ID)
if lenIDs != len(p.WhitelistTeamIDs) ||
lenApprovalIDs != len(p.ApprovalsWhitelistTeamIDs) ||
lenMergeIDs != len(p.MergeWhitelistTeamIDs) {
if _, err = sess.ID(p.ID).Cols(
"whitelist_team_i_ds",
"merge_whitelist_team_i_ds",
"approvals_whitelist_team_i_ds",
).Update(p); err != nil {
return fmt.Errorf("updateProtectedBranches: %w", err)
}
} }
} }
} }
@ -432,7 +419,7 @@ func DeleteTeam(t *organization.Team) error {
} }
// Update organization number of teams. // Update organization number of teams.
if _, err := sess.Exec("UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil { if _, err := db.Exec(ctx, "UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil {
return err return err
} }

@ -23,7 +23,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
) )
// DeleteUser deletes models associated to an user. // DeleteUser deletes models associated to an user.
@ -141,20 +140,8 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
break break
} }
for _, p := range protections { for _, p := range protections {
lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistUserIDs), len(p.ApprovalsWhitelistUserIDs), len(p.MergeWhitelistUserIDs) if err := git_model.RemoveUserIDFromProtectedBranch(ctx, p, u.ID); err != nil {
p.WhitelistUserIDs = util.SliceRemoveAll(p.WhitelistUserIDs, u.ID) return err
p.ApprovalsWhitelistUserIDs = util.SliceRemoveAll(p.ApprovalsWhitelistUserIDs, u.ID)
p.MergeWhitelistUserIDs = util.SliceRemoveAll(p.MergeWhitelistUserIDs, u.ID)
if lenIDs != len(p.WhitelistUserIDs) ||
lenApprovalIDs != len(p.ApprovalsWhitelistUserIDs) ||
lenMergeIDs != len(p.MergeWhitelistUserIDs) {
if _, err = e.ID(p.ID).Cols(
"whitelist_user_i_ds",
"merge_whitelist_user_i_ds",
"approvals_whitelist_user_i_ds",
).Update(p); err != nil {
return fmt.Errorf("updateProtectedBranches: %w", err)
}
} }
} }
} }

@ -119,14 +119,15 @@ type CanCommitToBranchResults struct {
// //
// and branch is not protected for push // and branch is not protected for push
func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.User) (CanCommitToBranchResults, error) { func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.User) (CanCommitToBranchResults, error) {
protectedBranch, err := git_model.GetProtectedBranchBy(ctx, r.Repository.ID, r.BranchName) protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName)
if err != nil { if err != nil {
return CanCommitToBranchResults{}, err return CanCommitToBranchResults{}, err
} }
userCanPush := true userCanPush := true
requireSigned := false requireSigned := false
if protectedBranch != nil { if protectedBranch != nil {
userCanPush = protectedBranch.CanUserPush(ctx, doer.ID) protectedBranch.Repo = r.Repository
userCanPush = protectedBranch.CanUserPush(ctx, doer)
requireSigned = protectedBranch.RequireSignedCommits requireSigned = protectedBranch.RequireSignedCommits
} }

@ -22,7 +22,9 @@ type Branch struct {
// BranchProtection represents a branch protection for a repository // BranchProtection represents a branch protection for a repository
type BranchProtection struct { type BranchProtection struct {
// Deprecated: true
BranchName string `json:"branch_name"` BranchName string `json:"branch_name"`
RuleName string `json:"rule_name"`
EnablePush bool `json:"enable_push"` EnablePush bool `json:"enable_push"`
EnablePushWhitelist bool `json:"enable_push_whitelist"` EnablePushWhitelist bool `json:"enable_push_whitelist"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
@ -52,7 +54,9 @@ type BranchProtection struct {
// CreateBranchProtectionOption options for creating a branch protection // CreateBranchProtectionOption options for creating a branch protection
type CreateBranchProtectionOption struct { type CreateBranchProtectionOption struct {
// Deprecated: true
BranchName string `json:"branch_name"` BranchName string `json:"branch_name"`
RuleName string `json:"rule_name"`
EnablePush bool `json:"enable_push"` EnablePush bool `json:"enable_push"`
EnablePushWhitelist bool `json:"enable_push_whitelist"` EnablePushWhitelist bool `json:"enable_push_whitelist"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"` PushWhitelistUsernames []string `json:"push_whitelist_usernames"`

@ -1824,6 +1824,7 @@ settings.mirror_sync_in_progress = Mirror synchronization is in progress. Check
settings.site = Website settings.site = Website
settings.update_settings = Update Settings settings.update_settings = Update Settings
settings.branches.update_default_branch = Update Default Branch settings.branches.update_default_branch = Update Default Branch
settings.branches.add_new_rule = Add New Rule
settings.advanced_settings = Advanced Settings settings.advanced_settings = Advanced Settings
settings.wiki_desc = Enable Repository Wiki settings.wiki_desc = Enable Repository Wiki
settings.use_internal_wiki = Use Built-In Wiki settings.use_internal_wiki = Use Built-In Wiki
@ -2069,6 +2070,8 @@ settings.deploy_key_deletion_desc = Removing a deploy key will revoke its access
settings.deploy_key_deletion_success = The deploy key has been removed. settings.deploy_key_deletion_success = The deploy key has been removed.
settings.branches = Branches settings.branches = Branches
settings.protected_branch = Branch Protection settings.protected_branch = Branch Protection
settings.protected_branch.save_rule = Save Rule
settings.protected_branch.delete_rule = Delete Rule
settings.protected_branch_can_push = Allow push? settings.protected_branch_can_push = Allow push?
settings.protected_branch_can_push_yes = You can push settings.protected_branch_can_push_yes = You can push
settings.protected_branch_can_push_no = You cannot push settings.protected_branch_can_push_no = You cannot push
@ -2103,15 +2106,17 @@ settings.dismiss_stale_approvals = Dismiss stale approvals
settings.dismiss_stale_approvals_desc = When new commits that change the content of the pull request are pushed to the branch, old approvals will be dismissed. settings.dismiss_stale_approvals_desc = When new commits that change the content of the pull request are pushed to the branch, old approvals will be dismissed.
settings.require_signed_commits = Require Signed Commits settings.require_signed_commits = Require Signed Commits
settings.require_signed_commits_desc = Reject pushes to this branch if they are unsigned or unverifiable. settings.require_signed_commits_desc = Reject pushes to this branch if they are unsigned or unverifiable.
settings.protect_branch_name_pattern = Protected Branch Name Pattern
settings.protect_protected_file_patterns = Protected file patterns (separated using semicolon '\;'): settings.protect_protected_file_patterns = Protected file patterns (separated using semicolon '\;'):
settings.protect_protected_file_patterns_desc = Protected files that are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. Multiple patterns can be separated using semicolon ('\;'). See <a href="https://pkg.go.dev/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> documentation for pattern syntax. Examples: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. settings.protect_protected_file_patterns_desc = Protected files that are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. Multiple patterns can be separated using semicolon ('\;'). See <a href="https://pkg.go.dev/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> documentation for pattern syntax. Examples: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
settings.protect_unprotected_file_patterns = Unprotected file patterns (separated using semicolon '\;'): settings.protect_unprotected_file_patterns = Unprotected file patterns (separated using semicolon '\;'):
settings.protect_unprotected_file_patterns_desc = Unprotected files that are allowed to be changed directly if user has write access, bypassing push restriction. Multiple patterns can be separated using semicolon ('\;'). See <a href="https://pkg.go.dev/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> documentation for pattern syntax. Examples: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. settings.protect_unprotected_file_patterns_desc = Unprotected files that are allowed to be changed directly if user has write access, bypassing push restriction. Multiple patterns can be separated using semicolon ('\;'). See <a href="https://pkg.go.dev/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> documentation for pattern syntax. Examples: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
settings.add_protected_branch = Enable protection settings.add_protected_branch = Enable protection
settings.delete_protected_branch = Disable protection settings.delete_protected_branch = Disable protection
settings.update_protect_branch_success = Branch protection for branch '%s' has been updated. settings.update_protect_branch_success = Branch protection for rule '%s' has been updated.
settings.remove_protected_branch_success = Branch protection for branch '%s' has been disabled. settings.remove_protected_branch_success = Branch protection for rule '%s' has been removed.
settings.protected_branch_deletion = Disable Branch Protection settings.remove_protected_branch_failed = Removing branch protection rule '%s' failed.
settings.protected_branch_deletion = Delete Branch Protection
settings.protected_branch_deletion_desc = Disabling branch protection allows users with write permission to push to the branch. Continue? 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 = 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_rejected_reviews_desc = Merging will not be possible when changes are requested by official reviewers, even if there are enough approvals.
@ -2124,6 +2129,7 @@ settings.default_merge_style_desc = Default merge style for pull requests:
settings.choose_branch = Choose a branch… settings.choose_branch = Choose a branch…
settings.no_protected_branch = There are no protected branches. settings.no_protected_branch = There are no protected branches.
settings.edit_protected_branch = Edit settings.edit_protected_branch = Edit
settings.protected_branch_required_rule_name = Required rule name
settings.protected_branch_required_approvals_min = Required approvals cannot be negative. settings.protected_branch_required_approvals_min = Required approvals cannot be negative.
settings.tags = Tags settings.tags = Tags
settings.tags.protection = Tag Protection settings.tags.protection = Tag Protection

@ -70,7 +70,7 @@ func GetBranch(ctx *context.APIContext) {
return return
} }
branchProtection, err := git_model.GetProtectedBranchBy(ctx, ctx.Repo.Repository.ID, branchName) branchProtection, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, branchName)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err) ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err)
return return
@ -124,7 +124,7 @@ func DeleteBranch(ctx *context.APIContext) {
ctx.NotFound(err) ctx.NotFound(err)
case errors.Is(err, repo_service.ErrBranchIsDefault): case errors.Is(err, repo_service.ErrBranchIsDefault):
ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch")) ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch"))
case errors.Is(err, repo_service.ErrBranchIsProtected): case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected")) ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected"))
default: default:
ctx.Error(http.StatusInternalServerError, "DeleteBranch", err) ctx.Error(http.StatusInternalServerError, "DeleteBranch", err)
@ -206,7 +206,7 @@ func CreateBranch(ctx *context.APIContext) {
return return
} }
branchProtection, err := git_model.GetProtectedBranchBy(ctx, ctx.Repo.Repository.ID, branch.Name) branchProtection, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, branch.Name)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err) ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err)
return return
@ -257,6 +257,12 @@ func ListBranches(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx) listOptions := utils.GetListOptions(ctx)
if !ctx.Repo.Repository.IsEmpty && ctx.Repo.GitRepo != nil { if !ctx.Repo.Repository.IsEmpty && ctx.Repo.GitRepo != nil {
rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindMatchedProtectedBranchRules", err)
return
}
skip, _ := listOptions.GetStartEnd() skip, _ := listOptions.GetStartEnd()
branches, total, err := ctx.Repo.GitRepo.GetBranches(skip, listOptions.PageSize) branches, total, err := ctx.Repo.GitRepo.GetBranches(skip, listOptions.PageSize)
if err != nil { if err != nil {
@ -276,11 +282,8 @@ func ListBranches(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "GetCommit", err) ctx.Error(http.StatusInternalServerError, "GetCommit", err)
return return
} }
branchProtection, err := git_model.GetProtectedBranchBy(ctx, ctx.Repo.Repository.ID, branches[i].Name)
if err != nil { branchProtection := rules.GetFirstMatched(branches[i].Name)
ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err)
return
}
apiBranch, err := convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) apiBranch, err := convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
@ -328,7 +331,7 @@ func GetBranchProtection(ctx *context.APIContext) {
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
bpName := ctx.Params(":name") bpName := ctx.Params(":name")
bp, err := git_model.GetProtectedBranchBy(ctx, repo.ID, bpName) bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
return return
@ -364,7 +367,7 @@ func ListBranchProtections(ctx *context.APIContext) {
// "$ref": "#/responses/BranchProtectionList" // "$ref": "#/responses/BranchProtectionList"
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
bps, err := git_model.GetProtectedBranches(ctx, repo.ID) bps, err := git_model.FindRepoProtectedBranchRules(ctx, repo.ID)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedBranches", err) ctx.Error(http.StatusInternalServerError, "GetProtectedBranches", err)
return return
@ -414,13 +417,18 @@ func CreateBranchProtection(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateBranchProtectionOption) form := web.GetForm(ctx).(*api.CreateBranchProtectionOption)
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
// Currently protection must match an actual branch ruleName := form.RuleName
if !git.IsBranchExist(ctx.Req.Context(), ctx.Repo.Repository.RepoPath(), form.BranchName) { if ruleName == "" {
ctx.NotFound() ruleName = form.BranchName //nolint
return }
isPlainRule := !git_model.IsRuleNameSpecial(ruleName)
var isBranchExist bool
if isPlainRule {
isBranchExist = git.IsBranchExist(ctx.Req.Context(), ctx.Repo.Repository.RepoPath(), ruleName)
} }
protectBranch, err := git_model.GetProtectedBranchBy(ctx, repo.ID, form.BranchName) protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, ruleName)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectBranchOfRepoByName", err) ctx.Error(http.StatusInternalServerError, "GetProtectBranchOfRepoByName", err)
return return
@ -494,7 +502,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
protectBranch = &git_model.ProtectedBranch{ protectBranch = &git_model.ProtectedBranch{
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
BranchName: form.BranchName, RuleName: form.RuleName,
CanPush: form.EnablePush, CanPush: form.EnablePush,
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist, EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
EnableMergeWhitelist: form.EnableMergeWhitelist, EnableMergeWhitelist: form.EnableMergeWhitelist,
@ -525,13 +533,42 @@ func CreateBranchProtection(ctx *context.APIContext) {
return return
} }
if err = pull_service.CheckPrsForBaseBranch(ctx.Repo.Repository, protectBranch.BranchName); err != nil { if isBranchExist {
ctx.Error(http.StatusInternalServerError, "CheckPrsForBaseBranch", err) if err = pull_service.CheckPRsForBaseBranch(ctx.Repo.Repository, form.RuleName); err != nil {
return ctx.Error(http.StatusInternalServerError, "CheckPRsForBaseBranch", err)
return
}
} else {
if !isPlainRule {
if ctx.Repo.GitRepo == nil {
ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return
}
defer func() {
ctx.Repo.GitRepo.Close()
ctx.Repo.GitRepo = nil
}()
}
// FIXME: since we only need to recheck files protected rules, we could improve this
matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.GitRepo, form.RuleName)
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err)
return
}
for _, branchName := range matchedBranches {
if err = pull_service.CheckPRsForBaseBranch(ctx.Repo.Repository, branchName); err != nil {
ctx.Error(http.StatusInternalServerError, "CheckPRsForBaseBranch", err)
return
}
}
}
} }
// Reload from db to get all whitelists // Reload from db to get all whitelists
bp, err := git_model.GetProtectedBranchBy(ctx, ctx.Repo.Repository.ID, form.BranchName) bp, err := git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, form.RuleName)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
return return
@ -583,7 +620,7 @@ func EditBranchProtection(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditBranchProtectionOption) form := web.GetForm(ctx).(*api.EditBranchProtectionOption)
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
bpName := ctx.Params(":name") bpName := ctx.Params(":name")
protectBranch, err := git_model.GetProtectedBranchBy(ctx, repo.ID, bpName) protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
return return
@ -760,13 +797,49 @@ func EditBranchProtection(ctx *context.APIContext) {
return return
} }
if err = pull_service.CheckPrsForBaseBranch(ctx.Repo.Repository, protectBranch.BranchName); err != nil { isPlainRule := !git_model.IsRuleNameSpecial(bpName)
ctx.Error(http.StatusInternalServerError, "CheckPrsForBaseBranch", err) var isBranchExist bool
return if isPlainRule {
isBranchExist = git.IsBranchExist(ctx.Req.Context(), ctx.Repo.Repository.RepoPath(), bpName)
}
if isBranchExist {
if err = pull_service.CheckPRsForBaseBranch(ctx.Repo.Repository, bpName); err != nil {
ctx.Error(http.StatusInternalServerError, "CheckPrsForBaseBranch", err)
return
}
} else {
if !isPlainRule {
if ctx.Repo.GitRepo == nil {
ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return
}
defer func() {
ctx.Repo.GitRepo.Close()
ctx.Repo.GitRepo = nil
}()
}
// FIXME: since we only need to recheck files protected rules, we could improve this
matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.GitRepo, protectBranch.RuleName)
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err)
return
}
for _, branchName := range matchedBranches {
if err = pull_service.CheckPRsForBaseBranch(ctx.Repo.Repository, branchName); err != nil {
ctx.Error(http.StatusInternalServerError, "CheckPrsForBaseBranch", err)
return
}
}
}
} }
// Reload from db to ensure get all whitelists // Reload from db to ensure get all whitelists
bp, err := git_model.GetProtectedBranchBy(ctx, repo.ID, bpName) bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err) ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err)
return return
@ -810,7 +883,7 @@ func DeleteBranchProtection(ctx *context.APIContext) {
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
bpName := ctx.Params(":name") bpName := ctx.Params(":name")
bp, err := git_model.GetProtectedBranchBy(ctx, repo.ID, bpName) bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
return return

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
pull_model "code.gitea.io/gitea/models/pull" pull_model "code.gitea.io/gitea/models/pull"
@ -902,7 +903,7 @@ func MergePullRequest(ctx *context.APIContext) {
ctx.NotFound(err) ctx.NotFound(err)
case errors.Is(err, repo_service.ErrBranchIsDefault): case errors.Is(err, repo_service.ErrBranchIsDefault):
ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch")) ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch"))
case errors.Is(err, repo_service.ErrBranchIsProtected): case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected")) ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected"))
default: default:
ctx.Error(http.StatusInternalServerError, "DeleteBranch", err) ctx.Error(http.StatusInternalServerError, "DeleteBranch", err)

@ -156,7 +156,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
return return
} }
protectBranch, err := git_model.GetProtectedBranchBy(ctx, repo.ID, branchName) protectBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
if err != nil { if err != nil {
log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err) log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{ ctx.JSON(http.StatusInternalServerError, private.Response{
@ -166,9 +166,10 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
} }
// Allow pushes to non-protected branches // Allow pushes to non-protected branches
if protectBranch == nil || !protectBranch.IsProtected() { if protectBranch == nil {
return return
} }
protectBranch.Repo = repo
// This ref is a protected branch. // This ref is a protected branch.
// //
@ -238,7 +239,6 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err), Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
}) })
return return
} }
changedProtectedfiles = true changedProtectedfiles = true
@ -251,7 +251,15 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
if ctx.opts.DeployKeyID != 0 { if ctx.opts.DeployKeyID != 0 {
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
} else { } else {
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, ctx.opts.UserID) user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
if err != nil {
log.Error("Unable to GetUserByID for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to GetUserByID for commits from %s to %s: %v", oldCommitID, newCommitID, err),
})
return
}
canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user)
} }
// 6. If we're not allowed to push directly // 6. If we're not allowed to push directly

@ -99,7 +99,7 @@ func DeleteBranchPost(ctx *context.Context) {
case errors.Is(err, repo_service.ErrBranchIsDefault): case errors.Is(err, repo_service.ErrBranchIsDefault):
log.Debug("DeleteBranch: Can't delete default branch '%s'", branchName) log.Debug("DeleteBranch: Can't delete default branch '%s'", branchName)
ctx.Flash.Error(ctx.Tr("repo.branch.default_deletion_failed", branchName)) ctx.Flash.Error(ctx.Tr("repo.branch.default_deletion_failed", branchName))
case errors.Is(err, repo_service.ErrBranchIsProtected): case errors.Is(err, git_model.ErrBranchIsProtected):
log.Debug("DeleteBranch: Can't delete protected branch '%s'", branchName) log.Debug("DeleteBranch: Can't delete protected branch '%s'", branchName)
ctx.Flash.Error(ctx.Tr("repo.branch.protected_deletion_failed", branchName)) ctx.Flash.Error(ctx.Tr("repo.branch.protected_deletion_failed", branchName))
default: default:
@ -189,9 +189,9 @@ func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, in
return nil, nil, 0 return nil, nil, 0
} }
protectedBranches, err := git_model.GetProtectedBranches(ctx, ctx.Repo.Repository.ID) rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID)
if err != nil { if err != nil {
ctx.ServerError("GetProtectedBranches", err) ctx.ServerError("FindRepoProtectedBranchRules", err)
return nil, nil, 0 return nil, nil, 0
} }
@ -208,7 +208,7 @@ func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, in
continue continue
} }
branch := loadOneBranch(ctx, rawBranches[i], defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo) branch := loadOneBranch(ctx, rawBranches[i], defaultBranch, &rules, repoIDToRepo, repoIDToGitRepo)
if branch == nil { if branch == nil {
return nil, nil, 0 return nil, nil, 0
} }
@ -220,7 +220,7 @@ func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, in
if defaultBranch != nil { if defaultBranch != nil {
// Always add the default branch // Always add the default branch
log.Debug("loadOneBranch: load default: '%s'", defaultBranch.Name) log.Debug("loadOneBranch: load default: '%s'", defaultBranch.Name)
defaultBranchBranch = loadOneBranch(ctx, defaultBranch, defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo) defaultBranchBranch = loadOneBranch(ctx, defaultBranch, defaultBranch, &rules, repoIDToRepo, repoIDToGitRepo)
branches = append(branches, defaultBranchBranch) branches = append(branches, defaultBranchBranch)
} }
@ -236,7 +236,7 @@ func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, in
return defaultBranchBranch, branches, totalNumOfBranches return defaultBranchBranch, branches, totalNumOfBranches
} }
func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, protectedBranches []*git_model.ProtectedBranch, func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, protectedBranches *git_model.ProtectedBranchRules,
repoIDToRepo map[int64]*repo_model.Repository, repoIDToRepo map[int64]*repo_model.Repository,
repoIDToGitRepo map[int64]*git.Repository, repoIDToGitRepo map[int64]*git.Repository,
) *Branch { ) *Branch {
@ -249,13 +249,8 @@ func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, p
} }
branchName := rawBranch.Name branchName := rawBranch.Name
var isProtected bool p := protectedBranches.GetFirstMatched(branchName)
for _, b := range protectedBranches { isProtected := p != nil
if b.BranchName == branchName {
isProtected = true
break
}
}
divergence := &git.DivergeObject{ divergence := &git.DivergeObject{
Ahead: -1, Ahead: -1,

@ -1604,7 +1604,7 @@ func ViewIssue(ctx *context.Context) {
if perm.CanWrite(unit.TypeCode) { if perm.CanWrite(unit.TypeCode) {
// Check if branch is not protected // Check if branch is not protected
if pull.HeadBranch != pull.HeadRepo.DefaultBranch { if pull.HeadBranch != pull.HeadRepo.DefaultBranch {
if protected, err := git_model.IsProtectedBranch(ctx, pull.HeadRepo.ID, pull.HeadBranch); err != nil { if protected, err := git_model.IsBranchProtected(ctx, pull.HeadRepo.ID, pull.HeadBranch); err != nil {
log.Error("IsProtectedBranch: %v", err) log.Error("IsProtectedBranch: %v", err)
} else if !protected { } else if !protected {
canDelete = true canDelete = true
@ -1680,22 +1680,25 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage
ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody
if err = pull.LoadProtectedBranch(ctx); err != nil { pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
if err != nil {
ctx.ServerError("LoadProtectedBranch", err) ctx.ServerError("LoadProtectedBranch", err)
return return
} }
ctx.Data["ShowMergeInstructions"] = true ctx.Data["ShowMergeInstructions"] = true
if pull.ProtectedBranch != nil { if pb != nil {
pb.Repo = pull.BaseRepo
var showMergeInstructions bool var showMergeInstructions bool
if ctx.Doer != nil { if ctx.Doer != nil {
showMergeInstructions = pull.ProtectedBranch.CanUserPush(ctx, ctx.Doer.ID) showMergeInstructions = pb.CanUserPush(ctx, ctx.Doer)
} }
ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pull.ProtectedBranch, pull) ctx.Data["ProtectedBranch"] = pb
ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pull.ProtectedBranch, pull) ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull)
ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pull.ProtectedBranch, pull) ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull)
ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pull.ProtectedBranch, pull) ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull)
ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pull.ProtectedBranch, pull) ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pb, pull)
ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pb, pull)
ctx.Data["RequireSigned"] = pb.RequireSignedCommits
ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0 ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles) ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)

@ -440,11 +440,12 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
setMergeTarget(ctx, pull) setMergeTarget(ctx, pull)
if err := pull.LoadProtectedBranch(ctx); err != nil { pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pull.BaseBranch)
if err != nil {
ctx.ServerError("LoadProtectedBranch", err) ctx.ServerError("LoadProtectedBranch", err)
return nil return nil
} }
ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck ctx.Data["EnableStatusCheck"] = pb != nil && pb.EnableStatusCheck
var baseGitRepo *git.Repository var baseGitRepo *git.Repository
if pull.BaseRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil { if pull.BaseRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil {
@ -570,16 +571,16 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses) ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
} }
if pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck { if pb != nil && pb.EnableStatusCheck {
ctx.Data["is_context_required"] = func(context string) bool { ctx.Data["is_context_required"] = func(context string) bool {
for _, c := range pull.ProtectedBranch.StatusCheckContexts { for _, c := range pb.StatusCheckContexts {
if c == context { if c == context {
return true return true
} }
} }
return false return false
} }
ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pull.ProtectedBranch.StatusCheckContexts) ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pb.StatusCheckContexts)
} }
ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha
@ -752,16 +753,17 @@ func ViewPullFiles(ctx *context.Context) {
return return
} }
if err = pull.LoadProtectedBranch(ctx); err != nil { pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
if err != nil {
ctx.ServerError("LoadProtectedBranch", err) ctx.ServerError("LoadProtectedBranch", err)
return return
} }
if pull.ProtectedBranch != nil { if pb != nil {
glob := pull.ProtectedBranch.GetProtectedFilePatterns() glob := pb.GetProtectedFilePatterns()
if len(glob) != 0 { if len(glob) != 0 {
for _, file := range diff.Files { for _, file := range diff.Files {
file.IsProtected = pull.ProtectedBranch.IsProtectedFile(glob, file.Name) file.IsProtected = pb.IsProtectedFile(glob, file.Name)
} }
} }
} }
@ -1400,7 +1402,7 @@ func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *g
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName)) ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
case errors.Is(err, repo_service.ErrBranchIsDefault): case errors.Is(err, repo_service.ErrBranchIsDefault):
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName)) ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
case errors.Is(err, repo_service.ErrBranchIsProtected): case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName)) ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
default: default:
log.Error("DeleteBranch: %v", err) log.Error("DeleteBranch: %v", err)

@ -56,7 +56,6 @@ const (
tplGithooks base.TplName = "repo/settings/githooks" tplGithooks base.TplName = "repo/settings/githooks"
tplGithookEdit base.TplName = "repo/settings/githook_edit" tplGithookEdit base.TplName = "repo/settings/githook_edit"
tplDeployKeys base.TplName = "repo/settings/deploy_keys" tplDeployKeys base.TplName = "repo/settings/deploy_keys"
tplProtectedBranch base.TplName = "repo/settings/protected_branch"
) )
// SettingsCtxData is a middleware that sets all the general context data for the // SettingsCtxData is a middleware that sets all the general context data for the

@ -19,47 +19,33 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
"code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/services/repository"
) )
// ProtectedBranch render the page to protect the repository const (
func ProtectedBranch(ctx *context.Context) { tplProtectedBranch base.TplName = "repo/settings/protected_branch"
)
// ProtectedBranchRules render the page to protect the repository
func ProtectedBranchRules(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsBranches"] = true ctx.Data["PageIsSettingsBranches"] = true
protectedBranches, err := git_model.GetProtectedBranches(ctx, ctx.Repo.Repository.ID) rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID)
if err != nil { if err != nil {
ctx.ServerError("GetProtectedBranches", err) ctx.ServerError("GetProtectedBranches", err)
return return
} }
ctx.Data["ProtectedBranches"] = protectedBranches ctx.Data["ProtectedBranches"] = rules
branches := ctx.Data["Branches"].([]string)
leftBranches := make([]string, 0, len(branches)-len(protectedBranches))
for _, b := range branches {
var protected bool
for _, pb := range protectedBranches {
if b == pb.BranchName {
protected = true
break
}
}
if !protected {
leftBranches = append(leftBranches, b)
}
}
ctx.Data["LeftBranches"] = leftBranches
ctx.HTML(http.StatusOK, tplBranches) ctx.HTML(http.StatusOK, tplBranches)
} }
// ProtectedBranchPost response for protect for a branch of a repository // SetDefaultBranchPost set default branch
func ProtectedBranchPost(ctx *context.Context) { func SetDefaultBranchPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsBranches"] = true ctx.Data["PageIsSettingsBranches"] = true
@ -101,41 +87,36 @@ func ProtectedBranchPost(ctx *context.Context) {
// SettingsProtectedBranch renders the protected branch setting page // SettingsProtectedBranch renders the protected branch setting page
func SettingsProtectedBranch(c *context.Context) { func SettingsProtectedBranch(c *context.Context) {
branch := c.Params("*") ruleName := c.FormString("rule_name")
if !c.Repo.GitRepo.IsBranchExist(branch) { var rule *git_model.ProtectedBranch
c.NotFound("IsBranchExist", nil) if ruleName != "" {
return var err error
} rule, err = git_model.GetProtectedBranchRuleByName(c, c.Repo.Repository.ID, ruleName)
if err != nil {
c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + branch
c.Data["PageIsSettingsBranches"] = true
protectBranch, err := git_model.GetProtectedBranchBy(c, c.Repo.Repository.ID, branch)
if err != nil {
if !git.IsErrBranchNotExist(err) {
c.ServerError("GetProtectBranchOfRepoByName", err) c.ServerError("GetProtectBranchOfRepoByName", err)
return return
} }
} }
if protectBranch == nil { if rule == nil {
// No options found, create defaults. // No options found, create defaults.
protectBranch = &git_model.ProtectedBranch{ rule = &git_model.ProtectedBranch{}
BranchName: branch,
}
} }
c.Data["PageIsSettingsBranches"] = true
c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + rule.RuleName
users, err := access_model.GetRepoReaders(c.Repo.Repository) users, err := access_model.GetRepoReaders(c.Repo.Repository)
if err != nil { if err != nil {
c.ServerError("Repo.Repository.GetReaders", err) c.ServerError("Repo.Repository.GetReaders", err)
return return
} }
c.Data["Users"] = users c.Data["Users"] = users
c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",") c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(rule.WhitelistUserIDs), ",")
c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistUserIDs), ",") c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistUserIDs), ",")
c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistUserIDs), ",") c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistUserIDs), ",")
contexts, _ := git_model.FindRepoRecentCommitStatusContexts(c, c.Repo.Repository.ID, 7*24*time.Hour) // Find last week status check contexts contexts, _ := git_model.FindRepoRecentCommitStatusContexts(c, c.Repo.Repository.ID, 7*24*time.Hour) // Find last week status check contexts
for _, ctx := range protectBranch.StatusCheckContexts { for _, ctx := range rule.StatusCheckContexts {
var found bool var found bool
for i := range contexts { for i := range contexts {
if contexts[i] == ctx { if contexts[i] == ctx {
@ -150,7 +131,7 @@ func SettingsProtectedBranch(c *context.Context) {
c.Data["branch_status_check_contexts"] = contexts c.Data["branch_status_check_contexts"] = contexts
c.Data["is_context_required"] = func(context string) bool { c.Data["is_context_required"] = func(context string) bool {
for _, c := range protectBranch.StatusCheckContexts { for _, c := range rule.StatusCheckContexts {
if c == context { if c == context {
return true return true
} }
@ -165,130 +146,173 @@ func SettingsProtectedBranch(c *context.Context) {
return return
} }
c.Data["Teams"] = teams c.Data["Teams"] = teams
c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",") c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.WhitelistTeamIDs), ",")
c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistTeamIDs), ",") c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistTeamIDs), ",")
c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistTeamIDs), ",") c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistTeamIDs), ",")
} }
c.Data["Branch"] = protectBranch c.Data["Rule"] = rule
c.HTML(http.StatusOK, tplProtectedBranch) c.HTML(http.StatusOK, tplProtectedBranch)
} }
// SettingsProtectedBranchPost updates the protected branch settings // SettingsProtectedBranchPost updates the protected branch settings
func SettingsProtectedBranchPost(ctx *context.Context) { func SettingsProtectedBranchPost(ctx *context.Context) {
f := web.GetForm(ctx).(*forms.ProtectBranchForm) f := web.GetForm(ctx).(*forms.ProtectBranchForm)
branch := ctx.Params("*") var protectBranch *git_model.ProtectedBranch
if !ctx.Repo.GitRepo.IsBranchExist(branch) { if f.RuleName == "" {
ctx.NotFound("IsBranchExist", nil) ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_rule_name"))
ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit", ctx.Repo.RepoLink))
return return
} }
protectBranch, err := git_model.GetProtectedBranchBy(ctx, ctx.Repo.Repository.ID, branch) var err error
protectBranch, err = git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, f.RuleName)
if err != nil { if err != nil {
if !git.IsErrBranchNotExist(err) { ctx.ServerError("GetProtectBranchOfRepoByName", err)
ctx.ServerError("GetProtectBranchOfRepoByName", err) return
return }
if protectBranch == nil {
// No options found, create defaults.
protectBranch = &git_model.ProtectedBranch{
RepoID: ctx.Repo.Repository.ID,
RuleName: f.RuleName,
} }
} }
if f.Protected { var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
if protectBranch == nil { protectBranch.RuleName = f.RuleName
// No options found, create defaults. if f.RequiredApprovals < 0 {
protectBranch = &git_model.ProtectedBranch{ ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min"))
RepoID: ctx.Repo.Repository.ID, ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit?rule_name=%s", ctx.Repo.RepoLink, f.RuleName))
BranchName: branch, return
} }
switch f.EnablePush {
case "all":
protectBranch.CanPush = true
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
case "whitelist":
protectBranch.CanPush = true
protectBranch.EnableWhitelist = true
protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
if strings.TrimSpace(f.WhitelistUsers) != "" {
whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
} }
if f.RequiredApprovals < 0 { if strings.TrimSpace(f.WhitelistTeams) != "" {
ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min")) whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, util.PathEscapeSegments(branch)))
} }
default:
protectBranch.CanPush = false
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
}
var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64 protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
switch f.EnablePush { if f.EnableMergeWhitelist {
case "all": if strings.TrimSpace(f.MergeWhitelistUsers) != "" {
protectBranch.CanPush = true mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
case "whitelist":
protectBranch.CanPush = true
protectBranch.EnableWhitelist = true
protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
if strings.TrimSpace(f.WhitelistUsers) != "" {
whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
}
if strings.TrimSpace(f.WhitelistTeams) != "" {
whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
}
default:
protectBranch.CanPush = false
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
} }
if strings.TrimSpace(f.MergeWhitelistTeams) != "" {
protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
if f.EnableMergeWhitelist {
if strings.TrimSpace(f.MergeWhitelistUsers) != "" {
mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
}
if strings.TrimSpace(f.MergeWhitelistTeams) != "" {
mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
}
} }
}
protectBranch.EnableStatusCheck = f.EnableStatusCheck protectBranch.EnableStatusCheck = f.EnableStatusCheck
if f.EnableStatusCheck { if f.EnableStatusCheck {
protectBranch.StatusCheckContexts = f.StatusCheckContexts protectBranch.StatusCheckContexts = f.StatusCheckContexts
} else { } else {
protectBranch.StatusCheckContexts = nil protectBranch.StatusCheckContexts = nil
} }
protectBranch.RequiredApprovals = f.RequiredApprovals protectBranch.RequiredApprovals = f.RequiredApprovals
protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist
if f.EnableApprovalsWhitelist { if f.EnableApprovalsWhitelist {
if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" { if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" {
approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ",")) approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ","))
}
if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" {
approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
}
} }
protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" {
protectBranch.BlockOnOfficialReviewRequests = f.BlockOnOfficialReviewRequests approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
protectBranch.RequireSignedCommits = f.RequireSignedCommits
protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
protectBranch.UnprotectedFilePatterns = f.UnprotectedFilePatterns
protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
UserIDs: whitelistUsers,
TeamIDs: whitelistTeams,
MergeUserIDs: mergeWhitelistUsers,
MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers,
ApprovalsTeamIDs: approvalsWhitelistTeams,
})
if err != nil {
ctx.ServerError("UpdateProtectBranch", err)
return
} }
if err = pull_service.CheckPrsForBaseBranch(ctx.Repo.Repository, protectBranch.BranchName); err != nil { }
ctx.ServerError("CheckPrsForBaseBranch", err) protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews
protectBranch.BlockOnOfficialReviewRequests = f.BlockOnOfficialReviewRequests
protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
protectBranch.RequireSignedCommits = f.RequireSignedCommits
protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
protectBranch.UnprotectedFilePatterns = f.UnprotectedFilePatterns
protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
UserIDs: whitelistUsers,
TeamIDs: whitelistTeams,
MergeUserIDs: mergeWhitelistUsers,
MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers,
ApprovalsTeamIDs: approvalsWhitelistTeams,
})
if err != nil {
ctx.ServerError("UpdateProtectBranch", err)
return
}
// FIXME: since we only need to recheck files protected rules, we could improve this
matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.GitRepo, protectBranch.RuleName)
if err != nil {
ctx.ServerError("FindAllMatchedBranches", err)
return
}
for _, branchName := range matchedBranches {
if err = pull_service.CheckPRsForBaseBranch(ctx.Repo.Repository, branchName); err != nil {
ctx.ServerError("CheckPRsForBaseBranch", err)
return return
} }
ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", branch))
ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, util.PathEscapeSegments(branch)))
} else {
if protectBranch != nil {
if err := git_model.DeleteProtectedBranch(ctx, ctx.Repo.Repository.ID, protectBranch.ID); err != nil {
ctx.ServerError("DeleteProtectedBranch", err)
return
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branch))
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
} }
ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", protectBranch.RuleName))
ctx.Redirect(fmt.Sprintf("%s/settings/branches?rule_name=%s", ctx.Repo.RepoLink, protectBranch.RuleName))
}
// DeleteProtectedBranchRulePost delete protected branch rule by id
func DeleteProtectedBranchRulePost(ctx *context.Context) {
ruleID := ctx.ParamsInt64("id")
if ruleID <= 0 {
ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink),
})
return
}
rule, err := git_model.GetProtectedBranchRuleByID(ctx, ctx.Repo.Repository.ID, ruleID)
if err != nil {
ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink),
})
return
}
if rule == nil {
ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink),
})
return
}
if err := git_model.DeleteProtectedBranch(ctx, ctx.Repo.Repository.ID, ruleID); err != nil {
ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", rule.RuleName))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink),
})
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", rule.RuleName))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink),
})
} }
// RenameBranchPost responses for rename a branch // RenameBranchPost responses for rename a branch

@ -861,10 +861,16 @@ func RegisterRoutes(m *web.Route) {
}) })
m.Group("/branches", func() { m.Group("/branches", func() {
m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost) m.Post("/", repo.SetDefaultBranchPost)
m.Combo("/*").Get(repo.SettingsProtectedBranch). }, repo.MustBeNotEmpty)
m.Group("/branches", func() {
m.Get("/", repo.ProtectedBranchRules)
m.Combo("/edit").Get(repo.SettingsProtectedBranch).
Post(web.Bind(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost) Post(web.Bind(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost)
m.Post("/{id}/delete", repo.DeleteProtectedBranchRulePost)
}, repo.MustBeNotEmpty) }, repo.MustBeNotEmpty)
m.Post("/rename_branch", web.Bind(forms.RenameBranchForm{}), context.RepoMustNotBeArchived(), repo.RenameBranchPost) m.Post("/rename_branch", web.Bind(forms.RenameBranchForm{}), context.RepoMustNotBeArchived(), repo.RenameBranchPost)
m.Group("/tags", func() { m.Group("/tags", func() {

@ -310,7 +310,7 @@ Loop:
return false, "", nil, &ErrWontSign{twofa} return false, "", nil, &ErrWontSign{twofa}
} }
case approved: case approved:
protectedBranch, err := git_model.GetProtectedBranchBy(ctx, repo.ID, pr.BaseBranch) protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pr.BaseBranch)
if err != nil { if err != nil {
return false, "", nil, err return false, "", nil, err
} }

@ -79,7 +79,7 @@ func ToBranch(repo *repo_model.Repository, b *git.Branch, c *git.Commit, bp *git
} }
if isRepoAdmin { if isRepoAdmin {
branch.EffectiveBranchProtectionName = bp.BranchName branch.EffectiveBranchProtectionName = bp.RuleName
} }
if user != nil { if user != nil {
@ -87,7 +87,8 @@ func ToBranch(repo *repo_model.Repository, b *git.Branch, c *git.Commit, bp *git
if err != nil { if err != nil {
return nil, err return nil, err
} }
branch.UserCanPush = bp.CanUserPush(db.DefaultContext, user.ID) bp.Repo = repo
branch.UserCanPush = bp.CanUserPush(db.DefaultContext, user)
branch.UserCanMerge = git_model.IsUserMergeWhitelisted(db.DefaultContext, bp, user.ID, permission) branch.UserCanMerge = git_model.IsUserMergeWhitelisted(db.DefaultContext, bp, user.ID, permission)
} }
@ -121,8 +122,14 @@ func ToBranchProtection(bp *git_model.ProtectedBranch) *api.BranchProtection {
log.Error("GetTeamNamesByID (ApprovalsWhitelistTeamIDs): %v", err) log.Error("GetTeamNamesByID (ApprovalsWhitelistTeamIDs): %v", err)
} }
branchName := ""
if !git_model.IsRuleNameSpecial(bp.RuleName) {
branchName = bp.RuleName
}
return &api.BranchProtection{ return &api.BranchProtection{
BranchName: bp.BranchName, BranchName: branchName,
RuleName: bp.RuleName,
EnablePush: bp.CanPush, EnablePush: bp.CanPush,
EnablePushWhitelist: bp.EnableWhitelist, EnablePushWhitelist: bp.EnableWhitelist,
PushWhitelistUsernames: pushWhitelistUsernames, PushWhitelistUsernames: pushWhitelistUsernames,

@ -186,7 +186,7 @@ func (f *RepoSettingForm) Validate(req *http.Request, errs binding.Errors) bindi
// ProtectBranchForm form for changing protected branch settings // ProtectBranchForm form for changing protected branch settings
type ProtectBranchForm struct { type ProtectBranchForm struct {
Protected bool RuleName string `binding:"Required"`
EnablePush string EnablePush string
WhitelistUsers string WhitelistUsers string
WhitelistTeams string WhitelistTeams string

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
@ -126,11 +127,12 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce
// isSignedIfRequired check if merge will be signed if required // isSignedIfRequired check if merge will be signed if required
func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) (bool, error) { func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) (bool, error) {
if err := pr.LoadProtectedBranch(ctx); err != nil { pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return false, err return false, err
} }
if pr.ProtectedBranch == nil || !pr.ProtectedBranch.RequireSignedCommits { if pb == nil || !pb.RequireSignedCommits {
return true, nil return true, nil
} }
@ -348,8 +350,8 @@ func testPR(id int64) {
checkAndUpdateStatus(ctx, pr) checkAndUpdateStatus(ctx, pr)
} }
// CheckPrsForBaseBranch check all pulls with bseBrannch // CheckPRsForBaseBranch check all pulls with baseBrannch
func CheckPrsForBaseBranch(baseRepo *repo_model.Repository, baseBranchName string) error { func CheckPRsForBaseBranch(baseRepo *repo_model.Repository, baseBranchName string) error {
prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(baseRepo.ID, baseBranchName) prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(baseRepo.ID, baseBranchName)
if err != nil { if err != nil {
return err return err

@ -83,10 +83,11 @@ func IsCommitStatusContextSuccess(commitStatuses []*git_model.CommitStatus, requ
// IsPullCommitStatusPass returns if all required status checks PASS // IsPullCommitStatusPass returns if all required status checks PASS
func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) { func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) {
if err := pr.LoadProtectedBranch(ctx); err != nil { pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return false, errors.Wrap(err, "GetLatestCommitStatus") return false, errors.Wrap(err, "GetLatestCommitStatus")
} }
if pr.ProtectedBranch == nil || !pr.ProtectedBranch.EnableStatusCheck { if pb == nil || !pb.EnableStatusCheck {
return true, nil return true, nil
} }
@ -137,12 +138,13 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR
return "", errors.Wrap(err, "GetLatestCommitStatus") return "", errors.Wrap(err, "GetLatestCommitStatus")
} }
if err := pr.LoadProtectedBranch(ctx); err != nil { pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return "", errors.Wrap(err, "LoadProtectedBranch") return "", errors.Wrap(err, "LoadProtectedBranch")
} }
var requiredContexts []string var requiredContexts []string
if pr.ProtectedBranch != nil { if pb != nil {
requiredContexts = pr.ProtectedBranch.StatusCheckContexts requiredContexts = pb.StatusCheckContexts
} }
return MergeRequiredContextsCommitStatus(commitStatuses, requiredContexts), nil return MergeRequiredContextsCommitStatus(commitStatuses, requiredContexts), nil

@ -760,12 +760,12 @@ func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p a
return false, nil return false, nil
} }
err := pr.LoadProtectedBranch(ctx) pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil { if err != nil {
return false, err return false, err
} }
if (p.CanWrite(unit.TypeCode) && pr.ProtectedBranch == nil) || (pr.ProtectedBranch != nil && git_model.IsUserMergeWhitelisted(ctx, pr.ProtectedBranch, user.ID, p)) { if (p.CanWrite(unit.TypeCode) && pb == nil) || (pb != nil && git_model.IsUserMergeWhitelisted(ctx, pb, user.ID, p)) {
return true, nil return true, nil
} }
@ -778,10 +778,11 @@ func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullReques
return fmt.Errorf("LoadBaseRepo: %w", err) return fmt.Errorf("LoadBaseRepo: %w", err)
} }
if err = pr.LoadProtectedBranch(ctx); err != nil { pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
return fmt.Errorf("LoadProtectedBranch: %w", err) if err != nil {
return fmt.Errorf("LoadProtectedBranch: %v", err)
} }
if pr.ProtectedBranch == nil { if pb == nil {
return nil return nil
} }
@ -795,23 +796,23 @@ func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullReques
} }
} }
if !issues_model.HasEnoughApprovals(ctx, pr.ProtectedBranch, pr) { if !issues_model.HasEnoughApprovals(ctx, pb, pr) {
return models.ErrDisallowedToMerge{ return models.ErrDisallowedToMerge{
Reason: "Does not have enough approvals", Reason: "Does not have enough approvals",
} }
} }
if issues_model.MergeBlockedByRejectedReview(ctx, pr.ProtectedBranch, pr) { if issues_model.MergeBlockedByRejectedReview(ctx, pb, pr) {
return models.ErrDisallowedToMerge{ return models.ErrDisallowedToMerge{
Reason: "There are requested changes", Reason: "There are requested changes",
} }
} }
if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pr.ProtectedBranch, pr) { if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pr) {
return models.ErrDisallowedToMerge{ return models.ErrDisallowedToMerge{
Reason: "There are official review requests", Reason: "There are official review requests",
} }
} }
if issues_model.MergeBlockedByOutdatedBranch(pr.ProtectedBranch, pr) { if issues_model.MergeBlockedByOutdatedBranch(pb, pr) {
return models.ErrDisallowedToMerge{ return models.ErrDisallowedToMerge{
Reason: "The head branch is behind the base branch", Reason: "The head branch is behind the base branch",
} }
@ -821,7 +822,7 @@ func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullReques
return nil return nil
} }
if pr.ProtectedBranch.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) { if pb.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) {
return models.ErrDisallowedToMerge{ return models.ErrDisallowedToMerge{
Reason: "Changed protected files", Reason: "Changed protected files",
} }
@ -836,6 +837,9 @@ func MergedManually(pr *issues_model.PullRequest, doer *user_model.User, baseGit
defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID)) defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID))
if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error { if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error {
if err := pr.LoadBaseRepo(ctx); err != nil {
return err
}
prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests) prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
if err != nil { if err != nil {
return err return err

@ -14,7 +14,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
@ -106,8 +106,8 @@ func TestPatch(pr *issues_model.PullRequest) error {
} }
// 3. Check for protected files changes // 3. Check for protected files changes
if err = checkPullFilesProtection(pr, gitRepo); err != nil { if err = checkPullFilesProtection(ctx, pr, gitRepo); err != nil {
return fmt.Errorf("pr.CheckPullFilesProtection(): %w", err) return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err)
} }
if len(pr.ChangedProtectedFiles) > 0 { if len(pr.ChangedProtectedFiles) > 0 {
@ -544,23 +544,23 @@ func CheckUnprotectedFiles(repo *git.Repository, oldCommitID, newCommitID string
} }
// checkPullFilesProtection check if pr changed protected files and save results // checkPullFilesProtection check if pr changed protected files and save results
func checkPullFilesProtection(pr *issues_model.PullRequest, gitRepo *git.Repository) error { func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) error {
if pr.Status == issues_model.PullRequestStatusEmpty { if pr.Status == issues_model.PullRequestStatusEmpty {
pr.ChangedProtectedFiles = nil pr.ChangedProtectedFiles = nil
return nil return nil
} }
if err := pr.LoadProtectedBranch(db.DefaultContext); err != nil { pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return err return err
} }
if pr.ProtectedBranch == nil { if pb == nil {
pr.ChangedProtectedFiles = nil pr.ChangedProtectedFiles = nil
return nil return nil
} }
var err error pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.MergeBase, "tracking", pb.GetProtectedFilePatterns(), 10, os.Environ())
pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.MergeBase, "tracking", pr.ProtectedBranch.GetProtectedFilePatterns(), 10, os.Environ())
if err != nil && !models.IsErrFilePathProtected(err) { if err != nil && !models.IsErrFilePathProtected(err) {
return err return err
} }

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
@ -92,20 +93,29 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest,
return false, false, err return false, false, err
} }
if err := pull.LoadBaseRepo(ctx); err != nil {
return false, false, err
}
pr := &issues_model.PullRequest{ pr := &issues_model.PullRequest{
HeadRepoID: pull.BaseRepoID, HeadRepoID: pull.BaseRepoID,
HeadRepo: pull.BaseRepo,
BaseRepoID: pull.HeadRepoID, BaseRepoID: pull.HeadRepoID,
BaseRepo: pull.HeadRepo,
HeadBranch: pull.BaseBranch, HeadBranch: pull.BaseBranch,
BaseBranch: pull.HeadBranch, BaseBranch: pull.HeadBranch,
} }
err = pr.LoadProtectedBranch(ctx) pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
if err != nil { if err != nil {
return false, false, err return false, false, err
} }
// can't do rebase on protected branch because need force push // can't do rebase on protected branch because need force push
if pr.ProtectedBranch == nil { if pb == nil {
if err := pr.LoadBaseRepo(ctx); err != nil {
return false, false, err
}
prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests) prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
if err != nil { if err != nil {
log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err) log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err)
@ -115,8 +125,11 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest,
} }
// Update function need push permission // Update function need push permission
if pr.ProtectedBranch != nil && !pr.ProtectedBranch.CanUserPush(ctx, user.ID) { if pb != nil {
return false, false, nil pb.Repo = pull.BaseRepo
if !pb.CanUserPush(ctx, user) {
return false, false, nil
}
} }
baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, user) baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, user)

@ -149,8 +149,7 @@ func RenameBranch(repo *repo_model.Repository, doer *user_model.User, gitRepo *g
// enmuerates all branch related errors // enmuerates all branch related errors
var ( var (
ErrBranchIsDefault = errors.New("branch is default") ErrBranchIsDefault = errors.New("branch is default")
ErrBranchIsProtected = errors.New("branch is protected")
) )
// DeleteBranch delete branch // DeleteBranch delete branch
@ -159,13 +158,12 @@ func DeleteBranch(doer *user_model.User, repo *repo_model.Repository, gitRepo *g
return ErrBranchIsDefault return ErrBranchIsDefault
} }
isProtected, err := git_model.IsProtectedBranch(db.DefaultContext, repo.ID, branchName) isProtected, err := git_model.IsBranchProtected(db.DefaultContext, repo.ID, branchName)
if err != nil { if err != nil {
return err return err
} }
if isProtected { if isProtected {
return ErrBranchIsProtected return git_model.ErrBranchIsProtected
} }
commit, err := gitRepo.GetBranchCommit(branchName) commit, err := gitRepo.GetBranchCommit(branchName)

@ -66,13 +66,16 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode
return err return err
} }
} else { } else {
protectedBranch, err := git_model.GetProtectedBranchBy(ctx, repo.ID, opts.OldBranch) protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, opts.OldBranch)
if err != nil { if err != nil {
return err return err
} }
if protectedBranch != nil && !protectedBranch.CanUserPush(ctx, doer.ID) { if protectedBranch != nil {
return models.ErrUserCannotCommit{ protectedBranch.Repo = repo
UserName: doer.LowerName, if !protectedBranch.CanUserPush(ctx, doer) {
return models.ErrUserCannotCommit{
UserName: doer.LowerName,
}
} }
} }
if protectedBranch != nil && protectedBranch.RequireSignedCommits { if protectedBranch != nil && protectedBranch.RequireSignedCommits {

@ -463,17 +463,18 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
// VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch // VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch
func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName, treePath string) error { func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName, treePath string) error {
protectedBranch, err := git_model.GetProtectedBranchBy(ctx, repo.ID, branchName) protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
if err != nil { if err != nil {
return err return err
} }
if protectedBranch != nil { if protectedBranch != nil {
protectedBranch.Repo = repo
isUnprotectedFile := false isUnprotectedFile := false
glob := protectedBranch.GetUnprotectedFilePatterns() glob := protectedBranch.GetUnprotectedFilePatterns()
if len(glob) != 0 { if len(glob) != 0 {
isUnprotectedFile = protectedBranch.IsUnprotectedFile(glob, treePath) isUnprotectedFile = protectedBranch.IsUnprotectedFile(glob, treePath)
} }
if !protectedBranch.CanUserPush(ctx, doer.ID) && !isUnprotectedFile { if !protectedBranch.CanUserPush(ctx, doer) && !isUnprotectedFile {
return models.ErrUserCannotCommit{ return models.ErrUserCannotCommit{
UserName: doer.LowerName, UserName: doer.LowerName,
} }

@ -204,7 +204,7 @@
{{if .IsBlockedByApprovals}} {{if .IsBlockedByApprovals}}
<div class="item"> <div class="item">
<i class="icon icon-octicon">{{svg "octicon-x"}}</i> <i class="icon icon-octicon">{{svg "octicon-x"}}</i>
{{$.locale.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .Issue.PullRequest.ProtectedBranch.RequiredApprovals}} {{$.locale.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .ProtectedBranch.RequiredApprovals}}
</div> </div>
{{else if .IsBlockedByRejection}} {{else if .IsBlockedByRejection}}
<div class="item"> <div class="item">
@ -444,7 +444,7 @@
{{if .IsBlockedByApprovals}} {{if .IsBlockedByApprovals}}
<div class="item text red"> <div class="item text red">
{{svg "octicon-x"}} {{svg "octicon-x"}}
{{$.locale.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .Issue.PullRequest.ProtectedBranch.RequiredApprovals}} {{$.locale.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .ProtectedBranch.RequiredApprovals}}
</div> </div>
{{else if .IsBlockedByRejection}} {{else if .IsBlockedByRejection}}
<div class="item text red"> <div class="item text red">

@ -43,31 +43,24 @@
<h4 class="ui top attached header"> <h4 class="ui top attached header">
{{.locale.Tr "repo.settings.protected_branch"}} {{.locale.Tr "repo.settings.protected_branch"}}
<div class="ui right">
<a class="ui primary tiny button" href="{{$.Repository.Link}}/settings/branches/edit">{{$.locale.Tr "repo.settings.branches.add_new_rule"}}</a>
</div>
</h4> </h4>
<div class="ui attached table segment"> <div class="ui attached table segment">
<div class="ui grid padded">
<div class="eight wide column">
<div class="ui fluid dropdown selection" tabindex="0">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="default text">{{.locale.Tr "repo.settings.choose_branch"}}</div>
<div class="menu transition hidden" tabindex="-1" style="display: block !important;">
{{range .LeftBranches}}
<a class="item" href="{{$.Repository.Link}}/settings/branches/{{. | PathEscapeSegments}}">{{.}}</a>
{{end}}
</div>
</div>
</div>
</div>
<div class="ui grid padded"> <div class="ui grid padded">
<div class="sixteen wide column"> <div class="sixteen wide column">
<table class="ui single line table padded"> <table class="ui single line table padded">
<tbody> <tbody>
{{range .ProtectedBranches}} {{range .ProtectedBranches}}
<tr> <tr>
<td><div class="ui basic primary label">{{.BranchName}}</div></td> <td><div class="ui basic primary label">{{.RuleName}}</div></td>
<td class="right aligned"><a class="rm ui button" href="{{$.Repository.Link}}/settings/branches/{{.BranchName | PathEscapeSegments}}">{{$.locale.Tr "repo.settings.edit_protected_branch"}}</a></td> <td class="right aligned">
<a class="rm ui button" href="{{$.Repository.Link}}/settings/branches/edit?rule_name={{.RuleName}}">{{$.locale.Tr "repo.settings.edit_protected_branch"}}</a>
<button class="ui red tiny button delete-button" data-url="{{$.Repository.Link}}/settings/branches/{{.ID}}/delete" data-id="{{.ID}}">
{{$.locale.Tr "repo.settings.protected_branch.delete_rule"}}</button>
</td>
</tr> </tr>
{{else}} {{else}}
<tr class="center aligned"><td>{{.locale.Tr "repo.settings.no_protected_branch"}}</td></tr> <tr class="center aligned"><td>{{.locale.Tr "repo.settings.no_protected_branch"}}</td></tr>
@ -102,4 +95,16 @@
{{end}} {{end}}
</div> </div>
</div> </div>
<div class="ui small basic delete modal">
<div class="ui header">
{{svg "octicon-trash" 16 "mr-2"}}
{{.locale.Tr "repo.settings.protected_branch_deletion"}}
</div>
<div class="content">
<p>{{.locale.Tr "repo.settings.protected_branch_deletion_desc"}}</p>
</div>
{{template "base/delete_modal_actions" .}}
</div>
{{template "base/footer" .}} {{template "base/footer" .}}

@ -4,42 +4,43 @@
{{template "repo/settings/navbar" .}} {{template "repo/settings/navbar" .}}
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
<h4 class="ui top attached header"> <form class="ui form" action="{{.Link}}" method="post">
{{.locale.Tr "repo.settings.branch_protection" (.Branch.BranchName|Escape) | Str2html}} <h4 class="ui top attached header">
</h4> {{.locale.Tr "repo.settings.branch_protection" (.Rule.RuleName|Escape) | Str2html}}
<div class="ui attached segment branch-protection"> </h4>
<form class="ui form" action="{{.Link}}" method="post"> <div class="ui attached segment branch-protection">
{{.CsrfTokenHtml}} <div class="field">
<div class="inline field"> <label for="protected_file_patterns">{{.locale.Tr "repo.settings.protect_branch_name_pattern"}}</label>
<div class="ui checkbox"> <input name="rule_name" type="text" value="{{.Rule.RuleName}}">
<input class="enable-protection" name="protected" type="checkbox" data-target="#protection_box" {{if .Branch.IsProtected}}checked{{end}}> <input name="rule_id" type="hidden" value="{{.Rule.ID}}">
<label>{{.locale.Tr "repo.settings.protect_this_branch"}}</label>
<p class="help">{{.locale.Tr "repo.settings.protect_this_branch_desc"}}</p>
</div>
</div> </div>
<div id="protection_box" class="fields {{if not .Branch.IsProtected}}disabled{{end}}">
<div class="ui divider"></div>
{{.CsrfTokenHtml}}
<div id="protection_box" class="fields">
<div class="field"> <div class="field">
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input name="enable_push" type="radio" value="none" class="disable-whitelist" data-target="#whitelist_box" {{if not .Branch.CanPush}}checked{{end}}> <input name="enable_push" type="radio" value="none" class="disable-whitelist" data-target="#whitelist_box" {{if not .Rule.CanPush}}checked{{end}}>
<label>{{.locale.Tr "repo.settings.protect_disable_push"}}</label> <label>{{.locale.Tr "repo.settings.protect_disable_push"}}</label>
<p class="help">{{.locale.Tr "repo.settings.protect_disable_push_desc"}}</p> <p class="help">{{.locale.Tr "repo.settings.protect_disable_push_desc"}}</p>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input name="enable_push" type="radio" value="all" class="disable-whitelist" data-target="#whitelist_box" {{if and (.Branch.CanPush) (not .Branch.EnableWhitelist)}}checked{{end}}> <input name="enable_push" type="radio" value="all" class="disable-whitelist" data-target="#whitelist_box" {{if and (.Rule.CanPush) (not .Rule.EnableWhitelist)}}checked{{end}}>
<label>{{.locale.Tr "repo.settings.protect_enable_push"}}</label> <label>{{.locale.Tr "repo.settings.protect_enable_push"}}</label>
<p class="help">{{.locale.Tr "repo.settings.protect_enable_push_desc"}}</p> <p class="help">{{.locale.Tr "repo.settings.protect_enable_push_desc"}}</p>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input name="enable_push" type="radio" value="whitelist" class="enable-whitelist" data-target="#whitelist_box" {{if and (.Branch.CanPush) (.Branch.EnableWhitelist)}}checked{{end}}> <input name="enable_push" type="radio" value="whitelist" class="enable-whitelist" data-target="#whitelist_box" {{if and (.Rule.CanPush) (.Rule.EnableWhitelist)}}checked{{end}}>
<label>{{.locale.Tr "repo.settings.protect_whitelist_committers"}}</label> <label>{{.locale.Tr "repo.settings.protect_whitelist_committers"}}</label>
<p class="help">{{.locale.Tr "repo.settings.protect_whitelist_committers_desc"}}</p> <p class="help">{{.locale.Tr "repo.settings.protect_whitelist_committers_desc"}}</p>
</div> </div>
</div> </div>
<div id="whitelist_box" class="fields {{if not .Branch.EnableWhitelist}}disabled{{end}}"> <div id="whitelist_box" class="fields {{if not .Rule.EnableWhitelist}}disabled{{end}}">
<div class="whitelist field"> <div class="whitelist field">
<label>{{.locale.Tr "repo.settings.protect_whitelist_users"}}</label> <label>{{.locale.Tr "repo.settings.protect_whitelist_users"}}</label>
<div class="ui multiple search selection dropdown"> <div class="ui multiple search selection dropdown">
@ -76,20 +77,22 @@
<br> <br>
<div class="whitelist field"> <div class="whitelist field">
<div class="ui checkbox"> <div class="ui checkbox">
<input type="checkbox" name="whitelist_deploy_keys" {{if .Branch.WhitelistDeployKeys}}checked{{end}}> <input type="checkbox" name="whitelist_deploy_keys" {{if .Rule.WhitelistDeployKeys}}checked{{end}}>
<label for="whitelist_deploy_keys">{{.locale.Tr "repo.settings.protect_whitelist_deploy_keys"}}</label> <label for="whitelist_deploy_keys">{{.locale.Tr "repo.settings.protect_whitelist_deploy_keys"}}</label>
</div> </div>
</div> </div>
</div> </div>
<div class="ui divider"></div>
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
<input class="enable-whitelist" name="enable_merge_whitelist" type="checkbox" data-target="#merge_whitelist_box" {{if .Branch.EnableMergeWhitelist}}checked{{end}}> <input class="enable-whitelist" name="enable_merge_whitelist" type="checkbox" data-target="#merge_whitelist_box" {{if .Rule.EnableMergeWhitelist}}checked{{end}}>
<label>{{.locale.Tr "repo.settings.protect_merge_whitelist_committers"}}</label> <label>{{.locale.Tr "repo.settings.protect_merge_whitelist_committers"}}</label>
<p class="help">{{.locale.Tr "repo.settings.protect_merge_whitelist_committers_desc"}}</p> <p class="help">{{.locale.Tr "repo.settings.protect_merge_whitelist_committers_desc"}}</p>
</div> </div>
</div> </div>
<div id="merge_whitelist_box" class="fields {{if not .Branch.EnableMergeWhitelist}}disabled{{end}}"> <div id="merge_whitelist_box" class="fields {{if not .Rule.EnableMergeWhitelist}}disabled{{end}}">
<div class="whitelist field"> <div class="whitelist field">
<label>{{.locale.Tr "repo.settings.protect_merge_whitelist_users"}}</label> <label>{{.locale.Tr "repo.settings.protect_merge_whitelist_users"}}</label>
<div class="ui multiple search selection dropdown"> <div class="ui multiple search selection dropdown">
@ -127,13 +130,13 @@
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
<input class="enable-statuscheck" name="enable_status_check" type="checkbox" data-target="#statuscheck_contexts_box" {{if eq (len .branch_status_check_contexts) 0}}disabled{{end}} {{if .Branch.EnableStatusCheck}}checked{{end}}> <input class="enable-statuscheck" name="enable_status_check" type="checkbox" data-target="#statuscheck_contexts_box" {{if eq (len .branch_status_check_contexts) 0}}disabled{{end}} {{if .Rule.EnableStatusCheck}}checked{{end}}>
<label>{{.locale.Tr "repo.settings.protect_check_status_contexts"}}</label> <label>{{.locale.Tr "repo.settings.protect_check_status_contexts"}}</label>
<p class="help">{{.locale.Tr "repo.settings.protect_check_status_contexts_desc"}}</p> <p class="help">{{.locale.Tr "repo.settings.protect_check_status_contexts_desc"}}</p>
</div> </div>
</div> </div>
<div id="statuscheck_contexts_box" class="fields {{if not .Branch.EnableStatusCheck}}disabled{{end}}"> <div id="statuscheck_contexts_box" class="fields {{if not .Rule.EnableStatusCheck}}disabled{{end}}">
<div class="field"> <div class="field">
<table class="ui celled table six column"> <table class="ui celled table six column">
<thead> <thead>
@ -159,17 +162,17 @@
<div class="field"> <div class="field">
<label for="required-approvals">{{.locale.Tr "repo.settings.protect_required_approvals"}}</label> <label for="required-approvals">{{.locale.Tr "repo.settings.protect_required_approvals"}}</label>
<input name="required_approvals" id="required-approvals" type="number" value="{{.Branch.RequiredApprovals}}"> <input name="required_approvals" id="required-approvals" type="number" value="{{.Rule.RequiredApprovals}}">
<p class="help">{{.locale.Tr "repo.settings.protect_required_approvals_desc"}}</p> <p class="help">{{.locale.Tr "repo.settings.protect_required_approvals_desc"}}</p>
</div> </div>
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
<input class="enable-whitelist" name="enable_approvals_whitelist" type="checkbox" data-target="#approvals_whitelist_box" {{if .Branch.EnableApprovalsWhitelist}}checked{{end}}> <input class="enable-whitelist" name="enable_approvals_whitelist" type="checkbox" data-target="#approvals_whitelist_box" {{if .Rule.EnableApprovalsWhitelist}}checked{{end}}>
<label>{{.locale.Tr "repo.settings.protect_approvals_whitelist_enabled"}}</label> <label>{{.locale.Tr "repo.settings.protect_approvals_whitelist_enabled"}}</label>
<p class="help">{{.locale.Tr "repo.settings.protect_approvals_whitelist_enabled_desc"}}</p> <p class="help">{{.locale.Tr "repo.settings.protect_approvals_whitelist_enabled_desc"}}</p>
</div> </div>
</div> </div>
<div id="approvals_whitelist_box" class="fields {{if not .Branch.EnableApprovalsWhitelist}}disabled{{end}}"> <div id="approvals_whitelist_box" class="fields {{if not .Rule.EnableApprovalsWhitelist}}disabled{{end}}">
<div class="whitelist field"> <div class="whitelist field">
<label>{{.locale.Tr "repo.settings.protect_approvals_whitelist_users"}}</label> <label>{{.locale.Tr "repo.settings.protect_approvals_whitelist_users"}}</label>
<div class="ui multiple search selection dropdown"> <div class="ui multiple search selection dropdown">
@ -206,59 +209,59 @@
</div> </div>
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
<input name="block_on_rejected_reviews" type="checkbox" {{if .Branch.BlockOnRejectedReviews}}checked{{end}}> <input name="block_on_rejected_reviews" type="checkbox" {{if .Rule.BlockOnRejectedReviews}}checked{{end}}>
<label for="block_on_rejected_reviews">{{.locale.Tr "repo.settings.block_rejected_reviews"}}</label> <label for="block_on_rejected_reviews">{{.locale.Tr "repo.settings.block_rejected_reviews"}}</label>
<p class="help">{{.locale.Tr "repo.settings.block_rejected_reviews_desc"}}</p> <p class="help">{{.locale.Tr "repo.settings.block_rejected_reviews_desc"}}</p>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
<input name="block_on_official_review_requests" type="checkbox" {{if .Branch.BlockOnOfficialReviewRequests}}checked{{end}}> <input name="block_on_official_review_requests" type="checkbox" {{if .Rule.BlockOnOfficialReviewRequests}}checked{{end}}>
<label for="block_on_official_review_requests">{{.locale.Tr "repo.settings.block_on_official_review_requests"}}</label> <label for="block_on_official_review_requests">{{.locale.Tr "repo.settings.block_on_official_review_requests"}}</label>
<p class="help">{{.locale.Tr "repo.settings.block_on_official_review_requests_desc"}}</p> <p class="help">{{.locale.Tr "repo.settings.block_on_official_review_requests_desc"}}</p>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
<input name="dismiss_stale_approvals" type="checkbox" {{if .Branch.DismissStaleApprovals}}checked{{end}}> <input name="dismiss_stale_approvals" type="checkbox" {{if .Rule.DismissStaleApprovals}}checked{{end}}>
<label for="dismiss_stale_approvals">{{.locale.Tr "repo.settings.dismiss_stale_approvals"}}</label> <label for="dismiss_stale_approvals">{{.locale.Tr "repo.settings.dismiss_stale_approvals"}}</label>
<p class="help">{{.locale.Tr "repo.settings.dismiss_stale_approvals_desc"}}</p> <p class="help">{{.locale.Tr "repo.settings.dismiss_stale_approvals_desc"}}</p>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
<input name="require_signed_commits" type="checkbox" {{if .Branch.RequireSignedCommits}}checked{{end}}> <input name="require_signed_commits" type="checkbox" {{if .Rule.RequireSignedCommits}}checked{{end}}>
<label for="require_signed_commits">{{.locale.Tr "repo.settings.require_signed_commits"}}</label> <label for="require_signed_commits">{{.locale.Tr "repo.settings.require_signed_commits"}}</label>
<p class="help">{{.locale.Tr "repo.settings.require_signed_commits_desc"}}</p> <p class="help">{{.locale.Tr "repo.settings.require_signed_commits_desc"}}</p>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
<input name="block_on_outdated_branch" type="checkbox" {{if .Branch.BlockOnOutdatedBranch}}checked{{end}}> <input name="block_on_outdated_branch" type="checkbox" {{if .Rule.BlockOnOutdatedBranch}}checked{{end}}>
<label for="block_on_outdated_branch">{{.locale.Tr "repo.settings.block_outdated_branch"}}</label> <label for="block_on_outdated_branch">{{.locale.Tr "repo.settings.block_outdated_branch"}}</label>
<p class="help">{{.locale.Tr "repo.settings.block_outdated_branch_desc"}}</p> <p class="help">{{.locale.Tr "repo.settings.block_outdated_branch_desc"}}</p>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label for="protected_file_patterns">{{.locale.Tr "repo.settings.protect_protected_file_patterns"}}</label> <label for="protected_file_patterns">{{.locale.Tr "repo.settings.protect_protected_file_patterns"}}</label>
<input name="protected_file_patterns" id="protected_file_patterns" type="text" value="{{.Branch.ProtectedFilePatterns}}"> <input name="protected_file_patterns" id="protected_file_patterns" type="text" value="{{.Rule.ProtectedFilePatterns}}">
<p class="help">{{.locale.Tr "repo.settings.protect_protected_file_patterns_desc" | Safe}}</p> <p class="help">{{.locale.Tr "repo.settings.protect_protected_file_patterns_desc" | Safe}}</p>
</div> </div>
<div class="field"> <div class="field">
<label for="unprotected_file_patterns">{{.locale.Tr "repo.settings.protect_unprotected_file_patterns"}}</label> <label for="unprotected_file_patterns">{{.locale.Tr "repo.settings.protect_unprotected_file_patterns"}}</label>
<input name="unprotected_file_patterns" id="unprotected_file_patterns" type="text" value="{{.Branch.UnprotectedFilePatterns}}"> <input name="unprotected_file_patterns" id="unprotected_file_patterns" type="text" value="{{.Rule.UnprotectedFilePatterns}}">
<p class="help">{{.locale.Tr "repo.settings.protect_unprotected_file_patterns_desc" | Safe}}</p> <p class="help">{{.locale.Tr "repo.settings.protect_unprotected_file_patterns_desc" | Safe}}</p>
</div> </div>
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="field"> <div class="field">
<button class="ui green button">{{$.locale.Tr "repo.settings.update_settings"}}</button> <button class="ui green button">{{$.locale.Tr "repo.settings.protected_branch.save_rule"}}</button>
<button class="ui gray button">{{$.locale.Tr "cancel"}}</button>
</div> </div>
</form> </div>
</div> </form>
</div> </div>
</div> </div>
{{template "base/footer" .}} {{template "base/footer" .}}

@ -14233,6 +14233,7 @@
"x-go-name": "BlockOnRejectedReviews" "x-go-name": "BlockOnRejectedReviews"
}, },
"branch_name": { "branch_name": {
"description": "Deprecated: true",
"type": "string", "type": "string",
"x-go-name": "BranchName" "x-go-name": "BranchName"
}, },
@ -14310,6 +14311,10 @@
"format": "int64", "format": "int64",
"x-go-name": "RequiredApprovals" "x-go-name": "RequiredApprovals"
}, },
"rule_name": {
"type": "string",
"x-go-name": "RuleName"
},
"status_check_contexts": { "status_check_contexts": {
"type": "array", "type": "array",
"items": { "items": {
@ -14772,6 +14777,7 @@
"x-go-name": "BlockOnRejectedReviews" "x-go-name": "BlockOnRejectedReviews"
}, },
"branch_name": { "branch_name": {
"description": "Deprecated: true",
"type": "string", "type": "string",
"x-go-name": "BranchName" "x-go-name": "BranchName"
}, },
@ -14844,6 +14850,10 @@
"format": "int64", "format": "int64",
"x-go-name": "RequiredApprovals" "x-go-name": "RequiredApprovals"
}, },
"rule_name": {
"type": "string",
"x-go-name": "RuleName"
},
"status_check_contexts": { "status_check_contexts": {
"type": "array", "type": "array",
"items": { "items": {

@ -38,21 +38,21 @@ func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPSta
if resp.Code == http.StatusOK { if resp.Code == http.StatusOK {
var branchProtection api.BranchProtection var branchProtection api.BranchProtection
DecodeJSON(t, resp, &branchProtection) DecodeJSON(t, resp, &branchProtection)
assert.EqualValues(t, branchName, branchProtection.BranchName) assert.EqualValues(t, branchName, branchProtection.RuleName)
} }
} }
func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) { func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
token := getUserToken(t, "user2") token := getUserToken(t, "user2")
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections?token="+token, &api.BranchProtection{ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections?token="+token, &api.BranchProtection{
BranchName: branchName, RuleName: branchName,
}) })
resp := MakeRequest(t, req, expectedHTTPStatus) resp := MakeRequest(t, req, expectedHTTPStatus)
if resp.Code == http.StatusCreated { if resp.Code == http.StatusCreated {
var branchProtection api.BranchProtection var branchProtection api.BranchProtection
DecodeJSON(t, resp, &branchProtection) DecodeJSON(t, resp, &branchProtection)
assert.EqualValues(t, branchName, branchProtection.BranchName) assert.EqualValues(t, branchName, branchProtection.RuleName)
} }
} }
@ -64,7 +64,7 @@ func testAPIEditBranchProtection(t *testing.T, branchName string, body *api.Bran
if resp.Code == http.StatusOK { if resp.Code == http.StatusOK {
var branchProtection api.BranchProtection var branchProtection api.BranchProtection
DecodeJSON(t, resp, &branchProtection) DecodeJSON(t, resp, &branchProtection)
assert.EqualValues(t, branchName, branchProtection.BranchName) assert.EqualValues(t, branchName, branchProtection.RuleName)
} }
} }
@ -169,8 +169,8 @@ func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBran
func TestAPIBranchProtection(t *testing.T) { func TestAPIBranchProtection(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
// Branch protection only on branch that exist // Branch protection on branch that not exist
testAPICreateBranchProtection(t, "master/doesnotexist", http.StatusNotFound) testAPICreateBranchProtection(t, "master/doesnotexist", http.StatusCreated)
// Get branch protection on branch that exist but not branch protection // Get branch protection on branch that exist but not branch protection
testAPIGetBranchProtection(t, "master", http.StatusNotFound) testAPIGetBranchProtection(t, "master", http.StatusNotFound)

@ -10,6 +10,8 @@ import (
"path" "path"
"testing" "testing"
"code.gitea.io/gitea/modules/json"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -43,15 +45,16 @@ func TestCreateFileOnProtectedBranch(t *testing.T) {
csrf := GetCSRF(t, session, "/user2/repo1/settings/branches") csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
// Change master branch to protected // Change master branch to protected
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/master", map[string]string{ req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
"_csrf": csrf, "_csrf": csrf,
"protected": "on", "rule_name": "master",
"enable_push": "true",
}) })
session.MakeRequest(t, req, http.StatusSeeOther) session.MakeRequest(t, req, http.StatusSeeOther)
// Check if master branch has been locked successfully // Check if master branch has been locked successfully
flashCookie := session.GetCookie("macaron_flash") flashCookie := session.GetCookie("macaron_flash")
assert.NotNil(t, flashCookie) assert.NotNil(t, flashCookie)
assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Bbranch%2B%2527master%2527%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2527master%2527%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
// Request editor page // Request editor page
req = NewRequest(t, "GET", "/user2/repo1/_new/master/") req = NewRequest(t, "GET", "/user2/repo1/_new/master/")
@ -76,16 +79,22 @@ func TestCreateFileOnProtectedBranch(t *testing.T) {
// remove the protected branch // remove the protected branch
csrf = GetCSRF(t, session, "/user2/repo1/settings/branches") csrf = GetCSRF(t, session, "/user2/repo1/settings/branches")
// Change master branch to protected // Change master branch to protected
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/master", map[string]string{ req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/1/delete", map[string]string{
"_csrf": csrf, "_csrf": csrf,
"protected": "off",
}) })
session.MakeRequest(t, req, http.StatusSeeOther)
resp = session.MakeRequest(t, req, http.StatusOK)
res := make(map[string]string)
assert.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
assert.EqualValues(t, "/user2/repo1/settings/branches", res["redirect"])
// Check if master branch has been locked successfully // Check if master branch has been locked successfully
flashCookie = session.GetCookie("macaron_flash") flashCookie = session.GetCookie("macaron_flash")
assert.NotNil(t, flashCookie) assert.NotNil(t, flashCookie)
assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Bbranch%2B%2527master%2527%2Bhas%2Bbeen%2Bdisabled.", flashCookie.Value) assert.EqualValues(t, "error%3DRemoving%2Bbranch%2Bprotection%2Brule%2B%25271%2527%2Bfailed.", flashCookie.Value)
}) })
} }

@ -414,9 +414,9 @@ func doProtectBranch(ctx APITestContext, branch, userToWhitelist, unprotectedFil
if userToWhitelist == "" { if userToWhitelist == "" {
// Change branch to protected // Change branch to protected
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{
"_csrf": csrf, "_csrf": csrf,
"protected": "on", "rule_name": branch,
"unprotected_file_patterns": unprotectedFilePatterns, "unprotected_file_patterns": unprotectedFilePatterns,
}) })
ctx.Session.MakeRequest(t, req, http.StatusSeeOther) ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
@ -424,9 +424,9 @@ func doProtectBranch(ctx APITestContext, branch, userToWhitelist, unprotectedFil
user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelist) user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelist)
assert.NoError(t, err) assert.NoError(t, err)
// Change branch to protected // Change branch to protected
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{
"_csrf": csrf, "_csrf": csrf,
"protected": "on", "rule_name": branch,
"enable_push": "whitelist", "enable_push": "whitelist",
"enable_whitelist": "on", "enable_whitelist": "on",
"whitelist_users": strconv.FormatInt(user.ID, 10), "whitelist_users": strconv.FormatInt(user.ID, 10),
@ -437,7 +437,7 @@ func doProtectBranch(ctx APITestContext, branch, userToWhitelist, unprotectedFil
// Check if master branch has been locked successfully // Check if master branch has been locked successfully
flashCookie := ctx.Session.GetCookie("macaron_flash") flashCookie := ctx.Session.GetCookie("macaron_flash")
assert.NotNil(t, flashCookie) assert.NotNil(t, flashCookie)
assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Bbranch%2B%2527"+url.QueryEscape(branch)+"%2527%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2527"+url.QueryEscape(branch)+"%2527%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
} }
} }

Loading…
Cancel
Save