From 9f9f4882f92fb8f015663dca5726937fa7830b70 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 17 Aug 2022 18:10:58 +0800 Subject: [PATCH 01/34] Make database supports glob branch name protected branch --- models/git/branches.go | 423 +-------------------------- models/git/protected_branch.go | 436 ++++++++++++++++++++++++++++ models/git/protected_branch_list.go | 38 +++ models/issues/pull.go | 33 +-- models/issues/review.go | 17 +- modules/context/repo.go | 5 +- routers/api/v1/repo/branch.go | 16 +- routers/private/hook_pre_receive.go | 2 +- routers/web/repo/issue.go | 21 +- routers/web/repo/pull.go | 20 +- services/asymkey/sign.go | 2 +- services/pull/check.go | 6 +- services/pull/commit_status.go | 12 +- services/pull/merge.go | 19 +- services/pull/patch.go | 13 +- services/pull/update.go | 7 +- services/repository/files/patch.go | 2 +- services/repository/files/update.go | 2 +- 18 files changed, 573 insertions(+), 501 deletions(-) create mode 100644 models/git/protected_branch.go create mode 100644 models/git/protected_branch_list.go diff --git a/models/git/branches.go b/models/git/branches.go index 7f05a566769b..8556bb4a7e6d 100644 --- a/models/git/branches.go +++ b/models/git/branches.go @@ -7,433 +7,15 @@ package git import ( "context" "fmt" - "strings" "time" "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" ) -// 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(userID int64) bool { - if !protectBranch.CanPush { - return false - } - - if !protectBranch.EnableWhitelist { - if user, err := user_model.GetUserByID(userID); err != nil { - log.Error("GetUserByID: %v", err) - return false - } else if repo, err := repo_model.GetRepositoryByID(protectBranch.RepoID); err != nil { - log.Error("repo_model.GetRepositoryByID: %v", err) - return false - } else if writeAccess, err := access_model.HasAccessUnit(db.DefaultContext, 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(db.DefaultContext, 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(protectBranch *ProtectedBranch, user *user_model.User) (bool, error) { - return IsUserOfficialReviewerCtx(db.DefaultContext, protectBranch, user) -} - -// IsUserOfficialReviewerCtx check if user is official reviewer for the branch (counts towards required approvals) -func IsUserOfficialReviewerCtx(ctx context.Context, protectBranch *ProtectedBranch, user *user_model.User) (bool, error) { - repo, err := repo_model.GetRepositoryByIDCtx(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: %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 -} - -// GetProtectedBranches get all protected branches -func GetProtectedBranches(repoID int64) ([]*ProtectedBranch, error) { - protectedBranches := make([]*ProtectedBranch, 0) - return protectedBranches, db.GetEngine(db.DefaultContext).Find(&protectedBranches, &ProtectedBranch{RepoID: repoID}) -} - -// IsProtectedBranch checks if branch is protected -func IsProtectedBranch(repoID int64, branchName string) (bool, error) { - protectedBranch := &ProtectedBranch{ - RepoID: repoID, - BranchName: branchName, - } - - has, err := db.GetEngine(db.DefaultContext).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.IsSliceInt64Eq(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.IsSliceInt64Eq(currentWhitelist, newWhitelist) - if !hasUsersChanged { - return currentWhitelist, nil - } - - whitelist = make([]int64, 0, len(newWhitelist)) - for _, userID := range newWhitelist { - user, err := user_model.GetUserByIDCtx(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.IsSliceInt64Eq(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.IsInt64InSlice(teams[i].ID, newWhitelist) { - whitelist = append(whitelist, teams[i].ID) - } - } - - return whitelist, err -} - -// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository. -func DeleteProtectedBranch(repoID, id int64) (err error) { - protectedBranch := &ProtectedBranch{ - RepoID: repoID, - ID: id, - } - - if affected, err := db.GetEngine(db.DefaultContext).Delete(protectedBranch); err != nil { - return err - } else if affected != 1 { - return fmt.Errorf("delete protected branch ID(%v) failed", id) - } - - return nil -} - // DeletedBranch struct type DeletedBranch struct { ID int64 `xorm:"pk autoincr"` @@ -445,6 +27,11 @@ type DeletedBranch struct { DeletedUnix timeutil.TimeStamp `xorm:"INDEX created"` } +func init() { + db.RegisterModel(new(DeletedBranch)) + db.RegisterModel(new(RenamedBranch)) +} + // AddDeletedBranch adds a deleted branch to the database func AddDeletedBranch(repoID int64, branchName, commit string, deletedByID int64) error { deletedBranch := &DeletedBranch{ diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go new file mode 100644 index 000000000000..16108999aea1 --- /dev/null +++ b/models/git/protected_branch.go @@ -0,0 +1,436 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package git + +import ( + "context" + "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" +) + +// 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)) +} + +// IsProtected returns if the branch is protected +func (protectBranch *ProtectedBranch) IsProtected() bool { + return protectBranch.ID > 0 +} + +// Match tests if branchName matches the rule +func (protectBranch *ProtectedBranch) Match(branchName string) bool { + if strings.EqualFold(protectBranch.BranchName, branchName) { + return true + } + return glob.MustCompile(protectBranch.BranchName).Match(branchName) +} + +// CanUserPush returns if some user could push to this protected branch +func (protectBranch *ProtectedBranch) CanUserPush(userID int64) bool { + if !protectBranch.CanPush { + return false + } + + if !protectBranch.EnableWhitelist { + if user, err := user_model.GetUserByID(userID); err != nil { + log.Error("GetUserByID: %v", err) + return false + } else if repo, err := repo_model.GetRepositoryByID(protectBranch.RepoID); err != nil { + log.Error("repo_model.GetRepositoryByID: %v", err) + return false + } else if writeAccess, err := access_model.HasAccessUnit(db.DefaultContext, 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(db.DefaultContext, 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.GetRepositoryByIDCtx(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: %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 +} + +// GetProtectedBranches get all protected branches +func GetProtectedBranches(repoID int64) ([]*ProtectedBranch, error) { + protectedBranches := make([]*ProtectedBranch, 0) + return protectedBranches, db.GetEngine(db.DefaultContext).Find(&protectedBranches, &ProtectedBranch{RepoID: repoID}) +} + +// IsProtectedBranch checks if branch is protected +func IsProtectedBranch(repoID int64, branchName string) (bool, error) { + protectedBranch := &ProtectedBranch{ + RepoID: repoID, + BranchName: branchName, + } + + has, err := db.GetEngine(db.DefaultContext).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.IsSliceInt64Eq(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.IsSliceInt64Eq(currentWhitelist, newWhitelist) + if !hasUsersChanged { + return currentWhitelist, nil + } + + whitelist = make([]int64, 0, len(newWhitelist)) + for _, userID := range newWhitelist { + user, err := user_model.GetUserByIDCtx(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.IsSliceInt64Eq(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.IsInt64InSlice(teams[i].ID, newWhitelist) { + whitelist = append(whitelist, teams[i].ID) + } + } + + return whitelist, err +} + +// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository. +func DeleteProtectedBranch(repoID, id int64) (err error) { + protectedBranch := &ProtectedBranch{ + RepoID: repoID, + ID: id, + } + + if affected, err := db.GetEngine(db.DefaultContext).Delete(protectedBranch); err != nil { + return err + } else if affected != 1 { + return fmt.Errorf("delete protected branch ID(%v) failed", id) + } + + return nil +} diff --git a/models/git/protected_branch_list.go b/models/git/protected_branch_list.go new file mode 100644 index 000000000000..fce049da0c79 --- /dev/null +++ b/models/git/protected_branch_list.go @@ -0,0 +1,38 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package git + +import ( + "context" + + "code.gitea.io/gitea/models/db" +) + +type ProtectedBranchRules []*ProtectedBranch + +func (rules ProtectedBranchRules) GetFirstMatched(branchName string) *ProtectedBranch { + for _, rule := range rules { + if rule.Match(branchName) { + return rule + } + } + return nil +} + +// FindMatchedProtectedBranchRules load all repository's protected rules +func FindMatchedProtectedBranchRules(ctx context.Context, repoID int64) (ProtectedBranchRules, error) { + var rules []*ProtectedBranch + err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Asc("created_unix").Find(&rules) + return rules, err +} + +// GetFirstMatchProtectedBranchRule returns the first matched rules +func GetFirstMatchProtectedBranchRule(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) { + rules, err := FindMatchedProtectedBranchRules(ctx, repoID) + if err != nil { + return nil, err + } + return rules.GetFirstMatched(branchName), nil +} diff --git a/models/issues/pull.go b/models/issues/pull.go index f96b03445e91..fd86ceed68d1 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -150,16 +150,16 @@ type PullRequest struct { Issue *Issue `xorm:"-"` Index int64 - HeadRepoID int64 `xorm:"INDEX"` - HeadRepo *repo_model.Repository `xorm:"-"` - BaseRepoID int64 `xorm:"INDEX"` - BaseRepo *repo_model.Repository `xorm:"-"` - HeadBranch string - HeadCommitID string `xorm:"-"` - BaseBranch string - ProtectedBranch *git_model.ProtectedBranch `xorm:"-"` - MergeBase string `xorm:"VARCHAR(40)"` - AllowMaintainerEdit bool `xorm:"NOT NULL DEFAULT false"` + HeadRepoID int64 `xorm:"INDEX"` + HeadRepo *repo_model.Repository `xorm:"-"` + BaseRepoID int64 `xorm:"INDEX"` + BaseRepo *repo_model.Repository `xorm:"-"` + HeadBranch string + HeadCommitID string `xorm:"-"` + BaseBranch string + // ProtectedBranch *git_model.ProtectedBranch `xorm:"-"` + MergeBase string `xorm:"VARCHAR(40)"` + AllowMaintainerEdit bool `xorm:"NOT NULL DEFAULT false"` HasMerged bool `xorm:"INDEX"` MergedCommitID string `xorm:"VARCHAR(40)"` @@ -305,13 +305,8 @@ func (pr *PullRequest) LoadIssueCtx(ctx context.Context) (err error) { return err } -// LoadProtectedBranch loads the protected branch of the base branch -func (pr *PullRequest) LoadProtectedBranch() (err error) { - return pr.LoadProtectedBranchCtx(db.DefaultContext) -} - -// LoadProtectedBranchCtx loads the protected branch of the base branch -func (pr *PullRequest) LoadProtectedBranchCtx(ctx context.Context) (err error) { +// LoadProtectedBranchRules loads the protected branch of the base branch +/*func (pr *PullRequest) LoadProtectedBranchRules(ctx context.Context) (err error) { if pr.ProtectedBranch == nil { if pr.BaseRepo == nil { if pr.BaseRepoID == 0 { @@ -322,10 +317,10 @@ func (pr *PullRequest) LoadProtectedBranchCtx(ctx context.Context) (err error) { return } } - pr.ProtectedBranch, err = git_model.GetProtectedBranchBy(ctx, pr.BaseRepo.ID, pr.BaseBranch) + pr.ProtectedBranch, err = git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepo.ID, pr.BaseBranch) } return err -} +}*/ // ReviewCount represents a count of Reviews type ReviewCount struct { diff --git a/models/issues/review.go b/models/issues/review.go index 583590080177..7df926efe16a 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -264,15 +264,17 @@ func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewers ...*user_mo if err != nil { return false, err } - if err = pr.LoadProtectedBranchCtx(ctx); err != nil { + + rule, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) + if err != nil { return false, err } - if pr.ProtectedBranch == nil { + if rule == nil { return false, nil } for _, reviewer := range reviewers { - official, err := git_model.IsUserOfficialReviewerCtx(ctx, pr.ProtectedBranch, reviewer) + official, err := git_model.IsUserOfficialReviewer(ctx, rule, reviewer) if official || err != nil { return official, err } @@ -287,18 +289,19 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio if err != nil { return false, err } - if err = pr.LoadProtectedBranchCtx(ctx); err != nil { + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) + if err != nil { return false, err } - if pr.ProtectedBranch == nil { + if pb == nil { return false, nil } - if !pr.ProtectedBranch.EnableApprovalsWhitelist { + if !pb.EnableApprovalsWhitelist { return team.UnitAccessModeCtx(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 diff --git a/modules/context/repo.go b/modules/context/repo.go index ea4054206999..185e7c47a191 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -118,9 +118,10 @@ type CanCommitToBranchResults struct { } // CanCommitToBranch returns true if repository is editable and user has proper access level -// 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) { - 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 { return CanCommitToBranchResults{}, err } diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 84a172e92bf0..2923502c9531 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -71,7 +71,7 @@ func GetBranch(ctx *context.APIContext) { 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 { ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err) return @@ -207,7 +207,7 @@ func CreateBranch(ctx *context.APIContext) { 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 { ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err) return @@ -260,6 +260,12 @@ func ListBranches(ctx *context.APIContext) { return } + rules, err := git_model.FindMatchedProtectedBranchRules(ctx, ctx.Repo.Repository.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "FindMatchedProtectedBranchRules", err) + return + } + apiBranches := make([]*api.Branch, 0, len(branches)) for i := range branches { c, err := branches[i].GetCommit() @@ -272,11 +278,7 @@ func ListBranches(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "GetCommit", err) return } - branchProtection, err := git_model.GetProtectedBranchBy(ctx, ctx.Repo.Repository.ID, branches[i].Name) - if err != nil { - ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err) - return - } + branchProtection := rules.GetFirstMatched(branches[i].Name) apiBranch, err := convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) if err != nil { ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index 3e7d1fe9ef69..53c0a03fa1b8 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -157,7 +157,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN return } - protectBranch, err := git_model.GetProtectedBranchBy(ctx, repo.ID, branchName) + protectBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName) if err != nil { log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err) ctx.JSON(http.StatusInternalServerError, private.Response{ diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index ad25a94e13b1..baa4ad9f5a70 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1630,22 +1630,23 @@ func ViewIssue(ctx *context.Context) { } ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage - if err = pull.LoadProtectedBranch(); err != nil { + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch) + if err != nil { ctx.ServerError("LoadProtectedBranch", err) return } ctx.Data["ShowMergeInstructions"] = true - if pull.ProtectedBranch != nil { + if pb != nil { var showMergeInstructions bool if ctx.Doer != nil { - showMergeInstructions = pull.ProtectedBranch.CanUserPush(ctx.Doer.ID) - } - ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pull.ProtectedBranch, pull) - ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pull.ProtectedBranch, pull) - ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pull.ProtectedBranch, pull) - ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pull.ProtectedBranch, pull) - ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pull.ProtectedBranch, pull) - ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits + showMergeInstructions = pb.CanUserPush(ctx.Doer.ID) + } + ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull) + ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull) + ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull) + ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pb, pull) + ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pb, pull) + ctx.Data["RequireSigned"] = pb.RequireSignedCommits ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0 ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 7c140a4e5991..6ac2e082a998 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -414,11 +414,12 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C setMergeTarget(ctx, pull) - if err := pull.LoadProtectedBranch(); err != nil { + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pull.BaseBranch) + if err != nil { ctx.ServerError("LoadProtectedBranch", err) return nil } - ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck + ctx.Data["EnableStatusCheck"] = pb != nil && pb.EnableStatusCheck var baseGitRepo *git.Repository if pull.BaseRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil { @@ -544,16 +545,16 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C 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 { - for _, c := range pull.ProtectedBranch.StatusCheckContexts { + for _, c := range pb.StatusCheckContexts { if c == context { return true } } 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 @@ -726,16 +727,17 @@ func ViewPullFiles(ctx *context.Context) { return } - if err = pull.LoadProtectedBranch(); err != nil { + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch) + if err != nil { ctx.ServerError("LoadProtectedBranch", err) return } - if pull.ProtectedBranch != nil { - glob := pull.ProtectedBranch.GetProtectedFilePatterns() + if pb != nil { + glob := pb.GetProtectedFilePatterns() if len(glob) != 0 { for _, file := range diff.Files { - file.IsProtected = pull.ProtectedBranch.IsProtectedFile(glob, file.Name) + file.IsProtected = pb.IsProtectedFile(glob, file.Name) } } } diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index edfd0f6cadf2..30370d7e2d78 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -311,7 +311,7 @@ Loop: return false, "", nil, &ErrWontSign{twofa} } 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 { return false, "", nil, err } diff --git a/services/pull/check.go b/services/pull/check.go index 288f4dc0b73b..1e0330c0c943 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -15,6 +15,7 @@ import ( "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" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -127,11 +128,12 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce // isSignedIfRequired check if merge will be signed if required func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) (bool, error) { - if err := pr.LoadProtectedBranchCtx(ctx); err != nil { + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) + if err != nil { return false, err } - if pr.ProtectedBranch == nil || !pr.ProtectedBranch.RequireSignedCommits { + if pb == nil || !pb.RequireSignedCommits { return true, nil } diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index 5d846129f6ce..6cd6c800a658 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -84,10 +84,11 @@ func IsCommitStatusContextSuccess(commitStatuses []*git_model.CommitStatus, requ // IsPullCommitStatusPass returns if all required status checks PASS func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) { - if err := pr.LoadProtectedBranchCtx(ctx); err != nil { + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) + if err != nil { return false, errors.Wrap(err, "GetLatestCommitStatus") } - if pr.ProtectedBranch == nil || !pr.ProtectedBranch.EnableStatusCheck { + if pb == nil || !pb.EnableStatusCheck { return true, nil } @@ -138,12 +139,13 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR return "", errors.Wrap(err, "GetLatestCommitStatus") } - if err := pr.LoadProtectedBranchCtx(ctx); err != nil { + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) + if err != nil { return "", errors.Wrap(err, "LoadProtectedBranch") } var requiredContexts []string - if pr.ProtectedBranch != nil { - requiredContexts = pr.ProtectedBranch.StatusCheckContexts + if pb != nil { + requiredContexts = pb.StatusCheckContexts } return MergeRequiredContextsCommitStatus(commitStatuses, requiredContexts), nil diff --git a/services/pull/merge.go b/services/pull/merge.go index 4cd4e3bd7e05..1a9b05da2a60 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -744,12 +744,12 @@ func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p a return false, nil } - err := pr.LoadProtectedBranchCtx(ctx) + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) if err != nil { 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 } @@ -762,10 +762,11 @@ func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullReques return fmt.Errorf("LoadBaseRepo: %v", err) } - if err = pr.LoadProtectedBranchCtx(ctx); err != nil { + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) + if err != nil { return fmt.Errorf("LoadProtectedBranch: %v", err) } - if pr.ProtectedBranch == nil { + if pb == nil { return nil } @@ -779,23 +780,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{ Reason: "Does not have enough approvals", } } - if issues_model.MergeBlockedByRejectedReview(ctx, pr.ProtectedBranch, pr) { + if issues_model.MergeBlockedByRejectedReview(ctx, pb, pr) { return models.ErrDisallowedToMerge{ Reason: "There are requested changes", } } - if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pr.ProtectedBranch, pr) { + if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pr) { return models.ErrDisallowedToMerge{ Reason: "There are official review requests", } } - if issues_model.MergeBlockedByOutdatedBranch(pr.ProtectedBranch, pr) { + if issues_model.MergeBlockedByOutdatedBranch(pb, pr) { return models.ErrDisallowedToMerge{ Reason: "The head branch is behind the base branch", } @@ -805,7 +806,7 @@ func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullReques return nil } - if pr.ProtectedBranch.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) { + if pb.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) { return models.ErrDisallowedToMerge{ Reason: "Changed protected files", } diff --git a/services/pull/patch.go b/services/pull/patch.go index 32895b2e784f..8a80d530c857 100644 --- a/services/pull/patch.go +++ b/services/pull/patch.go @@ -15,6 +15,7 @@ import ( "strings" "code.gitea.io/gitea/models" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/git" @@ -102,7 +103,7 @@ func TestPatch(pr *issues_model.PullRequest) error { } // 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(): %v", err) } @@ -527,23 +528,23 @@ func CheckUnprotectedFiles(repo *git.Repository, oldCommitID, newCommitID string } // 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 { pr.ChangedProtectedFiles = nil return nil } - if err := pr.LoadProtectedBranch(); err != nil { + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) + if err != nil { return err } - if pr.ProtectedBranch == nil { + if pb == nil { pr.ChangedProtectedFiles = nil return nil } - var err error - pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.MergeBase, "tracking", pr.ProtectedBranch.GetProtectedFilePatterns(), 10, os.Environ()) + pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.MergeBase, "tracking", pb.GetProtectedFilePatterns(), 10, os.Environ()) if err != nil && !models.IsErrFilePathProtected(err) { return err } diff --git a/services/pull/update.go b/services/pull/update.go index e5e26462e5aa..01ed833b0fca 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -9,6 +9,7 @@ import ( "fmt" "code.gitea.io/gitea/models" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -97,13 +98,13 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, BaseBranch: pull.HeadBranch, } - err = pr.LoadProtectedBranch() + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch) if err != nil { return false, false, err } // can't do rebase on protected branch because need force push - if pr.ProtectedBranch == nil { + if pb == nil { prUnit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests) if err != nil { log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err) @@ -113,7 +114,7 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, } // Update function need push permission - if pr.ProtectedBranch != nil && !pr.ProtectedBranch.CanUserPush(user.ID) { + if pb != nil && !pb.CanUserPush(user.ID) { return false, false, nil } diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go index d55d793f28cd..979f265ca0af 100644 --- a/services/repository/files/patch.go +++ b/services/repository/files/patch.go @@ -67,7 +67,7 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode return err } } else { - protectedBranch, err := git_model.GetProtectedBranchBy(ctx, repo.ID, opts.OldBranch) + protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, opts.OldBranch) if err != nil { return err } diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 4615a9153a68..b56704dc0a2a 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -463,7 +463,7 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do // 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 { - protectedBranch, err := git_model.GetProtectedBranchBy(ctx, repo.ID, branchName) + protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName) if err != nil { return err } From 1dfa78538e61254a94d1e3f6e833f35e59e1697d Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 17 Aug 2022 18:20:40 +0800 Subject: [PATCH 02/34] Fix bug --- models/issues/pull.go | 32 +++++---------------- routers/web/repo/issue.go | 1 + templates/repo/issue/view_content/pull.tmpl | 4 +-- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/models/issues/pull.go b/models/issues/pull.go index fd86ceed68d1..f411623ee295 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -150,14 +150,13 @@ type PullRequest struct { Issue *Issue `xorm:"-"` Index int64 - HeadRepoID int64 `xorm:"INDEX"` - HeadRepo *repo_model.Repository `xorm:"-"` - BaseRepoID int64 `xorm:"INDEX"` - BaseRepo *repo_model.Repository `xorm:"-"` - HeadBranch string - HeadCommitID string `xorm:"-"` - BaseBranch string - // ProtectedBranch *git_model.ProtectedBranch `xorm:"-"` + HeadRepoID int64 `xorm:"INDEX"` + HeadRepo *repo_model.Repository `xorm:"-"` + BaseRepoID int64 `xorm:"INDEX"` + BaseRepo *repo_model.Repository `xorm:"-"` + HeadBranch string + HeadCommitID string `xorm:"-"` + BaseBranch string MergeBase string `xorm:"VARCHAR(40)"` AllowMaintainerEdit bool `xorm:"NOT NULL DEFAULT false"` @@ -305,23 +304,6 @@ func (pr *PullRequest) LoadIssueCtx(ctx context.Context) (err error) { return err } -// LoadProtectedBranchRules loads the protected branch of the base branch -/*func (pr *PullRequest) LoadProtectedBranchRules(ctx context.Context) (err error) { - if pr.ProtectedBranch == nil { - if pr.BaseRepo == nil { - if pr.BaseRepoID == 0 { - return nil - } - pr.BaseRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.BaseRepoID) - if err != nil { - return - } - } - pr.ProtectedBranch, err = git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepo.ID, pr.BaseBranch) - } - return err -}*/ - // ReviewCount represents a count of Reviews type ReviewCount struct { IssueID int64 diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index baa4ad9f5a70..96a9f33dd3b3 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1641,6 +1641,7 @@ func ViewIssue(ctx *context.Context) { if ctx.Doer != nil { showMergeInstructions = pb.CanUserPush(ctx.Doer.ID) } + ctx.Data["ProtectedBranch"] = pb ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull) ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull) ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull) diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index fd901f013ebd..30771defdb2a 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -204,7 +204,7 @@ {{if .IsBlockedByApprovals}}
{{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}}
{{else if .IsBlockedByRejection}}
@@ -440,7 +440,7 @@ {{if .IsBlockedByApprovals}}
{{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}}
{{else if .IsBlockedByRejection}}
From bf27be688a63f934a18bce335f858c937a3d2486 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 17 Aug 2022 21:27:35 +0800 Subject: [PATCH 03/34] Fix fmt --- models/git/protected_branch.go | 1 - 1 file changed, 1 deletion(-) diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index 16108999aea1..0fb4835d0987 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -252,7 +252,6 @@ func GetProtectedBranchBy(ctx context.Context, repoID int64, branchName string) return rel, nil } - // WhitelistOptions represent all sorts of whitelists used for protected branches type WhitelistOptions struct { UserIDs []int64 From 588c5b74268ac61f1ee81d30d8027b7bff9d379d Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 18 Aug 2022 15:01:04 +0800 Subject: [PATCH 04/34] Most UI changes --- models/git/protected_branch.go | 24 +- models/git/protected_branch_list.go | 10 +- options/locale/locale_en-US.ini | 4 + routers/api/v1/repo/branch.go | 4 +- routers/private/hook_pre_receive.go | 2 +- routers/web/repo/branch.go | 8 +- routers/web/repo/setting.go | 1 - routers/web/repo/setting_protected_branch.go | 274 +++++++++--------- routers/web/web.go | 4 +- services/forms/repo_form.go | 3 +- templates/repo/settings/branches.tmpl | 14 +- templates/repo/settings/protected_branch.tmpl | 76 ++--- 12 files changed, 209 insertions(+), 215 deletions(-) diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index 0fb4835d0987..dc3b9de5b81f 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -59,11 +59,6 @@ func init() { db.RegisterModel(new(ProtectedBranch)) } -// IsProtected returns if the branch is protected -func (protectBranch *ProtectedBranch) IsProtected() bool { - return protectBranch.ID > 0 -} - // Match tests if branchName matches the rule func (protectBranch *ProtectedBranch) Match(branchName string) bool { if strings.EqualFold(protectBranch.BranchName, branchName) { @@ -252,6 +247,19 @@ func GetProtectedBranchBy(ctx context.Context, repoID int64, branchName string) return rel, nil } +// GetProtectedBranchRuleByID getting protected branch by ID/Name +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 @@ -325,12 +333,6 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote return nil } -// GetProtectedBranches get all protected branches -func GetProtectedBranches(repoID int64) ([]*ProtectedBranch, error) { - protectedBranches := make([]*ProtectedBranch, 0) - return protectedBranches, db.GetEngine(db.DefaultContext).Find(&protectedBranches, &ProtectedBranch{RepoID: repoID}) -} - // IsProtectedBranch checks if branch is protected func IsProtectedBranch(repoID int64, branchName string) (bool, error) { protectedBranch := &ProtectedBranch{ diff --git a/models/git/protected_branch_list.go b/models/git/protected_branch_list.go index fce049da0c79..9c8cce45d4d6 100644 --- a/models/git/protected_branch_list.go +++ b/models/git/protected_branch_list.go @@ -21,18 +21,18 @@ func (rules ProtectedBranchRules) GetFirstMatched(branchName string) *ProtectedB return nil } -// FindMatchedProtectedBranchRules load all repository's protected rules -func FindMatchedProtectedBranchRules(ctx context.Context, repoID int64) (ProtectedBranchRules, error) { +// FindRepoProtectedBranchRules load all repository's protected rules +func FindRepoProtectedBranchRules(ctx context.Context, repoID int64) (ProtectedBranchRules, error) { var rules []*ProtectedBranch err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Asc("created_unix").Find(&rules) return rules, err } // GetFirstMatchProtectedBranchRule returns the first matched rules -func GetFirstMatchProtectedBranchRule(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) { - rules, err := FindMatchedProtectedBranchRules(ctx, repoID) +func GetFirstMatchProtectedBranchRule(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) { + rules, err := FindRepoProtectedBranchRules(ctx, repoID) if err != nil { return nil, err } - return rules.GetFirstMatched(branchName), nil + return rules.GetFirstMatched(ruleName), nil } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 0e309279d29b..9ec8ab11538e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1791,6 +1791,7 @@ settings.mirror_sync_in_progress = Mirror synchronization is in progress. Check settings.site = Website settings.update_settings = Update Settings settings.branches.update_default_branch = Update Default Branch +settings.branches.add_new_rule = Add New Rule settings.advanced_settings = Advanced Settings settings.wiki_desc = Enable Repository Wiki settings.use_internal_wiki = Use Built-In Wiki @@ -2031,6 +2032,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.branches = Branches 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_yes = You can push settings.protected_branch_can_push_no = You can not push @@ -2065,6 +2068,7 @@ 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.require_signed_commits = Require Signed Commits 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_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 github.com/gobwas/glob documentation for pattern syntax. Examples: .drone.yml, /docs/**/*.txt. settings.protect_unprotected_file_patterns = Unprotected file patterns (separated using semicolon '\;'): diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 2923502c9531..e876fab7ffc7 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -260,7 +260,7 @@ func ListBranches(ctx *context.APIContext) { return } - rules, err := git_model.FindMatchedProtectedBranchRules(ctx, ctx.Repo.Repository.ID) + rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.Error(http.StatusInternalServerError, "FindMatchedProtectedBranchRules", err) return @@ -359,7 +359,7 @@ func ListBranchProtections(ctx *context.APIContext) { // "$ref": "#/responses/BranchProtectionList" repo := ctx.Repo.Repository - bps, err := git_model.GetProtectedBranches(repo.ID) + bps, err := git_model.FindRepoProtectedBranchRules(ctx, repo.ID) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedBranches", err) return diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index 53c0a03fa1b8..19933a3247b2 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -167,7 +167,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN } // Allow pushes to non-protected branches - if protectBranch == nil || !protectBranch.IsProtected() { + if protectBranch == nil { return } diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index d14ba6cbe9a4..b0e67bfd2294 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -186,9 +186,9 @@ func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, in return nil, nil, 0 } - protectedBranches, err := git_model.GetProtectedBranches(ctx.Repo.Repository.ID) + rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID) if err != nil { - ctx.ServerError("GetProtectedBranches", err) + ctx.ServerError("FindRepoProtectedBranchRules", err) return nil, nil, 0 } @@ -205,7 +205,7 @@ func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, in continue } - branch := loadOneBranch(ctx, rawBranches[i], defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo) + branch := loadOneBranch(ctx, rawBranches[i], defaultBranch, rules, repoIDToRepo, repoIDToGitRepo) if branch == nil { return nil, nil, 0 } @@ -217,7 +217,7 @@ func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, in if defaultBranch != nil { // Always add the default branch 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) } diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index a59824cecdb4..931e81a26a8a 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -55,7 +55,6 @@ const ( tplGithooks base.TplName = "repo/settings/githooks" tplGithookEdit base.TplName = "repo/settings/githook_edit" 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 diff --git a/routers/web/repo/setting_protected_branch.go b/routers/web/repo/setting_protected_branch.go index c4cd3486aaa6..e6c8d267a505 100644 --- a/routers/web/repo/setting_protected_branch.go +++ b/routers/web/repo/setting_protected_branch.go @@ -20,41 +20,27 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" pull_service "code.gitea.io/gitea/services/pull" "code.gitea.io/gitea/services/repository" ) +const ( + tplProtectedBranch base.TplName = "repo/settings/protected_branch" +) + // ProtectedBranch render the page to protect the repository func ProtectedBranch(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["PageIsSettingsBranches"] = true - protectedBranches, err := git_model.GetProtectedBranches(ctx.Repo.Repository.ID) + rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("GetProtectedBranches", err) return } - ctx.Data["ProtectedBranches"] = protectedBranches - - 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.Data["ProtectedBranches"] = rules ctx.HTML(http.StatusOK, tplBranches) } @@ -102,41 +88,36 @@ func ProtectedBranchPost(ctx *context.Context) { // SettingsProtectedBranch renders the protected branch setting page func SettingsProtectedBranch(c *context.Context) { - branch := c.Params("*") - if !c.Repo.GitRepo.IsBranchExist(branch) { - c.NotFound("IsBranchExist", nil) - return - } - - 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) { + ruleID := c.ParamsInt64("id") + var rule *git_model.ProtectedBranch + if ruleID > 0 { + var err error + rule, err = git_model.GetProtectedBranchRuleByID(c, c.Repo.Repository.ID, ruleID) + if err != nil { c.ServerError("GetProtectBranchOfRepoByName", err) return } } - if protectBranch == nil { + if rule == nil { // No options found, create defaults. - protectBranch = &git_model.ProtectedBranch{ - BranchName: branch, - } + rule = &git_model.ProtectedBranch{} } + c.Data["PageIsSettingsBranches"] = true + c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + rule.BranchName + users, err := access_model.GetRepoReaders(c.Repo.Repository) if err != nil { c.ServerError("Repo.Repository.GetReaders", err) return } c.Data["Users"] = users - c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",") - c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistUserIDs), ",") - c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistUserIDs), ",") + c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(rule.WhitelistUserIDs), ",") + c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistUserIDs), ",") + c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistUserIDs), ",") contexts, _ := git_model.FindRepoRecentCommitStatusContexts(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 for i := range contexts { if contexts[i] == ctx { @@ -151,7 +132,7 @@ func SettingsProtectedBranch(c *context.Context) { c.Data["branch_status_check_contexts"] = contexts c.Data["is_context_required"] = func(context string) bool { - for _, c := range protectBranch.StatusCheckContexts { + for _, c := range rule.StatusCheckContexts { if c == context { return true } @@ -166,130 +147,139 @@ func SettingsProtectedBranch(c *context.Context) { return } c.Data["Teams"] = teams - c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",") - c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistTeamIDs), ",") - c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistTeamIDs), ",") + c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.WhitelistTeamIDs), ",") + c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistTeamIDs), ",") + c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistTeamIDs), ",") } - c.Data["Branch"] = protectBranch + c.Data["Rule"] = rule c.HTML(http.StatusOK, tplProtectedBranch) } // SettingsProtectedBranchPost updates the protected branch settings func SettingsProtectedBranchPost(ctx *context.Context) { f := web.GetForm(ctx).(*forms.ProtectBranchForm) - branch := ctx.Params("*") - if !ctx.Repo.GitRepo.IsBranchExist(branch) { - ctx.NotFound("IsBranchExist", nil) - return - } - - protectBranch, err := git_model.GetProtectedBranchBy(ctx, ctx.Repo.Repository.ID, branch) - if err != nil { - if !git.IsErrBranchNotExist(err) { + var protectBranch *git_model.ProtectedBranch + if f.RuleID > 0 { + var err error + protectBranch, err = git_model.GetProtectedBranchRuleByID(ctx, ctx.Repo.Repository.ID, f.RuleID) + if err != nil { ctx.ServerError("GetProtectBranchOfRepoByName", err) return } - } - if f.Protected { - if protectBranch == nil { - // No options found, create defaults. - protectBranch = &git_model.ProtectedBranch{ - RepoID: ctx.Repo.Repository.ID, - BranchName: branch, - } - } - if f.RequiredApprovals < 0 { - ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min")) - ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, util.PathEscapeSegments(branch))) + } else { + // No options found, create defaults. + protectBranch = &git_model.ProtectedBranch{ + RepoID: ctx.Repo.Repository.ID, } + } - var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64 - 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 strings.TrimSpace(f.WhitelistTeams) != "" { - whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ",")) - } - default: - protectBranch.CanPush = false - protectBranch.EnableWhitelist = false - protectBranch.WhitelistDeployKeys = false - } + var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64 + protectBranch.BranchName = f.RuleName + if f.RequiredApprovals < 0 { + ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min")) + ctx.Redirect(fmt.Sprintf("%s/settings/branches/%d", ctx.Repo.RepoLink, f.RuleID)) + return + } - protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist - 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, ",")) - } + 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, ",")) } - - protectBranch.EnableStatusCheck = f.EnableStatusCheck - if f.EnableStatusCheck { - protectBranch.StatusCheckContexts = f.StatusCheckContexts - } else { - protectBranch.StatusCheckContexts = nil + if strings.TrimSpace(f.WhitelistTeams) != "" { + whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ",")) } + default: + protectBranch.CanPush = false + protectBranch.EnableWhitelist = false + protectBranch.WhitelistDeployKeys = false + } - protectBranch.RequiredApprovals = f.RequiredApprovals - protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist - if f.EnableApprovalsWhitelist { - if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" { - approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ",")) - } - if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" { - approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ",")) - } + protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist + if f.EnableMergeWhitelist { + if strings.TrimSpace(f.MergeWhitelistUsers) != "" { + mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ",")) } - 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 + if strings.TrimSpace(f.MergeWhitelistTeams) != "" { + mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ",")) } - if err = pull_service.CheckPrsForBaseBranch(ctx.Repo.Repository, protectBranch.BranchName); err != nil { - ctx.ServerError("CheckPrsForBaseBranch", err) - 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))) + } + + protectBranch.EnableStatusCheck = f.EnableStatusCheck + if f.EnableStatusCheck { + protectBranch.StatusCheckContexts = f.StatusCheckContexts } else { - if protectBranch != nil { - if err := git_model.DeleteProtectedBranch(ctx.Repo.Repository.ID, protectBranch.ID); err != nil { - ctx.ServerError("DeleteProtectedBranch", err) - return - } + protectBranch.StatusCheckContexts = nil + } + + protectBranch.RequiredApprovals = f.RequiredApprovals + protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist + if f.EnableApprovalsWhitelist { + if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" { + approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ",")) + } + if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" { + approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ",")) } - ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branch)) - ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink)) } + 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 + } + if err = pull_service.CheckPrsForBaseBranch(ctx.Repo.Repository, protectBranch.BranchName); err != nil { + ctx.ServerError("CheckPrsForBaseBranch", err) + return + } + ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", protectBranch.BranchName)) + ctx.Redirect(fmt.Sprintf("%s/settings/branches/%d", ctx.Repo.RepoLink, protectBranch.ID)) +} + +func DeleteProtectedBranchRulePost(ctx *context.Context) { + ruleID := ctx.ParamsInt64("id") + if ruleID <= 0 { + return + } + rule, err := git_model.GetProtectedBranchRuleByID(ctx, ctx.Repo.Repository.ID, ruleID) + if err != nil { + return + } + + if rule == nil { + return + } + + if err := git_model.DeleteProtectedBranch(ctx.Repo.Repository.ID, ruleID); err != nil { + ctx.ServerError("DeleteProtectedBranch", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", rule.BranchName)) + ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink)) } // RenameBranchPost responses for rename a branch diff --git a/routers/web/web.go b/routers/web/web.go index 34d3de6fde0a..c0b628acffcc 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -761,7 +761,9 @@ func RegisterRoutes(m *web.Route) { m.Group("/branches", func() { m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost) - m.Combo("/*").Get(repo.SettingsProtectedBranch). + m.Combo("/new").Get(repo.SettingsProtectedBranch). + Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost) + m.Combo("/{id}").Get(repo.SettingsProtectedBranch). Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost) }, repo.MustBeNotEmpty) m.Post("/rename_branch", bindIgnErr(forms.RenameBranchForm{}), context.RepoMustNotBeArchived(), repo.RenameBranchPost) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 7a4a2123ebd2..a7da8730c3ed 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -186,7 +186,8 @@ func (f *RepoSettingForm) Validate(req *http.Request, errs binding.Errors) bindi // ProtectBranchForm form for changing protected branch settings type ProtectBranchForm struct { - Protected bool + RuleID int64 + RuleName string `binding:"Required"` EnablePush string WhitelistUsers string WhitelistTeams string diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl index cc8fc604267d..170aa4ceaf43 100644 --- a/templates/repo/settings/branches.tmpl +++ b/templates/repo/settings/branches.tmpl @@ -47,16 +47,8 @@
-
- +
@@ -67,7 +59,7 @@ {{range .ProtectedBranches}}
{{.BranchName}}
- {{$.locale.Tr "repo.settings.edit_protected_branch"}} + {{$.locale.Tr "repo.settings.edit_protected_branch"}} {{else}} {{.locale.Tr "repo.settings.no_protected_branch"}} diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl index ff93218b2018..1e36a4f68438 100644 --- a/templates/repo/settings/protected_branch.tmpl +++ b/templates/repo/settings/protected_branch.tmpl @@ -4,42 +4,43 @@ {{template "repo/settings/navbar" .}}
{{template "base/alert" .}} -

- {{.locale.Tr "repo.settings.branch_protection" (.Branch.BranchName|Escape) | Str2html}} -

-
-
- {{.CsrfTokenHtml}} -
-
- - -

{{.locale.Tr "repo.settings.protect_this_branch_desc"}}

-
+ +

+ {{.locale.Tr "repo.settings.branch_protection" (.Rule.BranchName|Escape) | Str2html}} +

+
+
+ + +
-
+ +
+ + {{.CsrfTokenHtml}} +
- +

{{.locale.Tr "repo.settings.protect_disable_push_desc"}}

- +

{{.locale.Tr "repo.settings.protect_enable_push_desc"}}

- +

{{.locale.Tr "repo.settings.protect_whitelist_committers_desc"}}

-
+
+
+
- +

{{.locale.Tr "repo.settings.protect_merge_whitelist_committers_desc"}}

-
+