Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 190 additions & 7 deletions models/git/protected_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ var ErrBranchIsProtected = errors.New("branch is protected")
// ProtectedBranch struct
type ProtectedBranch struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s)"`
RepoID int64 `xorm:"INDEX DEFAULT 0"`
OwnerID int64 `xorm:"INDEX DEFAULT 0"`
Repo *repo_model.Repository `xorm:"-"`
RuleName string `xorm:"'branch_name' UNIQUE(s)"` // a branch name or a glob match to branch name
RuleName string `xorm:"'branch_name'"` // a branch name or a glob match to branch name
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
globRule glob.Glob `xorm:"-"`
isPlainName bool `xorm:"-"`
Expand Down Expand Up @@ -313,9 +314,9 @@ func (protectBranch *ProtectedBranch) IsUnprotectedFile(patterns []glob.Glob, pa
}

// GetProtectedBranchRuleByName getting protected branch rule by name
func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) {
func GetOrgProtectedBranchRuleByName(ctx context.Context, ownerID int64, ruleName string) (*ProtectedBranch, error) {
// branch_name is legacy name, it actually is rule name
rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "branch_name": ruleName})
rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"owner_id": ownerID, "branch_name": ruleName})
if err != nil {
return nil, err
} else if !exist {
Expand All @@ -324,15 +325,53 @@ func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName st
return rel, nil
}

// GetProtectedBranchRuleByName getting protected branch rule by name
func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return nil, fmt.Errorf("GetRepositoryByID: %w", err)
}
if err := repo.LoadOwner(ctx); err != nil {
return nil, fmt.Errorf("LoadOwner: %w", err)
}
if repo.Owner.IsOrganization() {
if rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"owner_id": repo.OwnerID, "branch_name": ruleName}); err != nil {
return nil, err
} else if exist {
return rel, nil
}
}
// branch_name is legacy name, it actually is rule name
rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "branch_name": ruleName})
if err != nil {
return nil, err
}
return util.Iif(exist, rel, nil), nil
}

// GetProtectedBranchRuleByID getting protected branch rule by rule ID
func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*ProtectedBranch, error) {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return nil, fmt.Errorf("GetRepositoryByID: %w", err)
}
if err := repo.LoadOwner(ctx); err != nil {
return nil, fmt.Errorf("LoadOwner: %w", err)
}
if repo.Owner.IsOrganization() {
if rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"owner_id": repo.OwnerID, "id": ruleID}); err != nil {
return nil, err
} else if exist {
return rel, nil
}
}

rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "id": ruleID})
if err != nil {
return nil, err
} else if !exist {
return nil, nil
}
return rel, nil

return util.Iif(exist, rel, nil), nil
}

// WhitelistOptions represent all sorts of whitelists used for protected branches
Expand Down Expand Up @@ -441,6 +480,70 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
return nil
}

// UpdateOrgProtectBranch saves branch protection options for an organization.
// 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 UpdateOrgProtectBranch(ctx context.Context, org *organization.Organization, protectBranch *ProtectedBranch, opts WhitelistOptions) (err error) {
whitelist, err := updateOrgUserWhitelist(ctx, org, protectBranch.WhitelistUserIDs, opts.UserIDs)
if err != nil {
return err
}
protectBranch.WhitelistUserIDs = whitelist

whitelist, err = updateOrgUserWhitelist(ctx, org, protectBranch.ForcePushAllowlistUserIDs, opts.ForcePushUserIDs)
if err != nil {
return err
}
protectBranch.ForcePushAllowlistUserIDs = whitelist

whitelist, err = updateOrgUserWhitelist(ctx, org, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs)
if err != nil {
return err
}
protectBranch.MergeWhitelistUserIDs = whitelist

whitelist, err = updateOrgApprovalWhitelist(ctx, org, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs)
if err != nil {
return err
}
protectBranch.ApprovalsWhitelistUserIDs = whitelist

whitelist, err = updateOrgTeamWhitelist(ctx, org, protectBranch.WhitelistTeamIDs, opts.TeamIDs)
if err != nil {
return err
}
protectBranch.WhitelistTeamIDs = whitelist

whitelist, err = updateOrgTeamWhitelist(ctx, org, protectBranch.ForcePushAllowlistTeamIDs, opts.ForcePushTeamIDs)
if err != nil {
return err
}
protectBranch.ForcePushAllowlistTeamIDs = whitelist

whitelist, err = updateOrgTeamWhitelist(ctx, org, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs)
if err != nil {
return err
}
protectBranch.MergeWhitelistTeamIDs = whitelist

whitelist, err = updateOrgTeamWhitelist(ctx, org, protectBranch.ApprovalsWhitelistTeamIDs, opts.ApprovalsTeamIDs)
if err != nil {
return err
}
protectBranch.ApprovalsWhitelistTeamIDs = whitelist

if protectBranch.ID == 0 {
_, err = db.GetEngine(ctx).Insert(protectBranch)
return err
}

if _, err = db.GetEngine(ctx).ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
return fmt.Errorf("Update: %v", err)
}
return nil
}

func UpdateProtectBranchPriorities(ctx context.Context, repo *repo_model.Repository, ids []int64) error {
prio := int64(1)
return db.WithTx(ctx, func(ctx context.Context) error {
Expand Down Expand Up @@ -480,6 +583,27 @@ func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, c
return whitelist, err
}

// updateOrgApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which are members of the org.
func updateOrgApprovalWhitelist(ctx context.Context, org *organization.Organization, 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 {
isMember, err := organization.IsOrganizationMember(ctx, org.ID, userID)
if err != nil {
return nil, err
}
if isMember {
whitelist = append(whitelist, userID)
}
}
return whitelist, nil
}

// 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) {
Expand Down Expand Up @@ -532,6 +656,65 @@ func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, curre
return whitelist, err
}

// updateOrgUserWhitelist checks whether the user whitelist changed and returns a whitelist with
// the users from newWhitelist which are members of the org.
func updateOrgUserWhitelist(ctx context.Context, org *organization.Organization, 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 {
isMember, err := organization.IsOrganizationMember(ctx, org.ID, userID)
if err != nil {
return nil, err
}
if isMember {
whitelist = append(whitelist, userID)
}
}
return whitelist, nil
}

// updateOrgTeamWhitelist checks whether the team whitelist changed and returns a whitelist with
// the teams from newWhitelist which belong to the org.
func updateOrgTeamWhitelist(ctx context.Context, org *organization.Organization, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
hasTeamsChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist)
if !hasTeamsChanged {
return currentWhitelist, nil
}

teams, err := organization.GetTeamsByIDs(ctx, newWhitelist)
if err != nil {
return nil, fmt.Errorf("GetTeamsByIDs [team_ids: %d]: %v", newWhitelist, err)
}

whitelist = make([]int64, 0, len(teams))
for _, team := range teams {
if team.OrgID == org.ID {
whitelist = append(whitelist, team.ID)
}
}
return whitelist, nil
}

// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
func DeleteOrgProtectedBranch(ctx context.Context, org *organization.Organization, id int64) (err error) {
protectedBranch := &ProtectedBranch{
OwnerID: org.ID,
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
}

// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository.
func DeleteProtectedBranch(ctx context.Context, repo *repo_model.Repository, id int64) (err error) {
err = repo.MustNotBeArchived()
Expand Down
26 changes: 26 additions & 0 deletions models/git/protected_branch_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"context"
"sort"

repo_model "code.gitea.io/gitea/models/repo"

"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/optional"
Expand Down Expand Up @@ -41,13 +43,37 @@ func (rules ProtectedBranchRules) sort() {
})
}

// FindOrgProtectedBranchRules load all repository's protected rules
func FindOrgProtectedBranchRules(ctx context.Context, ownerID int64) (ProtectedBranchRules, error) {
var rules ProtectedBranchRules
err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Asc("created_unix").Find(&rules)
if err != nil {
return nil, err
}

rules.sort() // to make non-glob rules have higher priority, and for same glob/non-glob rules, first created rules have higher priority
return rules, nil
}

// 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
}

// if no repo-level rules matched, try to find owner-level rules
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return nil, err
}

err = db.GetEngine(ctx).Where("owner_id = ?", repo.OwnerID).Asc("created_unix").Find(&rules)
if err != nil {
return nil, err
}

rules.sort() // to make non-glob rules have higher priority, and for same glob/non-glob rules, first created rules have higher priority
return rules, nil
}
Expand Down
1 change: 1 addition & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ func prepareMigrationTasks() []*migration {
newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs),
newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength),
newMigration(323, "Add support for actions concurrency", v1_25.AddActionsConcurrency),
newMigration(324, "Add owner_id to protected_branch", v1_25.AddOwnerIDToProtectedBranch),
}
return preparedMigrations
}
Expand Down
48 changes: 48 additions & 0 deletions models/migrations/v1_25/v324.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_25

import (
"code.gitea.io/gitea/modules/setting"

"xorm.io/xorm"
)

func AddOwnerIDToProtectedBranch(x *xorm.Engine) error {
type ProtectedBranch struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX DEFAULT 0"`
OwnerID int64 `xorm:"INDEX DEFAULT 0"`
}

if err := x.Sync(new(ProtectedBranch)); err != nil {
return err
}

db := x.NewSession()
defer db.Close()

if err := db.Begin(); err != nil {
return err
}

// Drop old unique index if it exists, ignoring errors if it doesn't.
if _, err := db.Exec("DROP INDEX `UQE_protected_branch_s`"); err != nil {
return err
//log.Warn("Could not drop index UQE_protected_branch_s: %v", err)
}

// These partial indexes might not be supported on all database versions, but they are the correct approach.
// We will assume modern database versions. The WHERE clause might need adjustment for MSSQL.
if !setting.Database.Type.IsMSSQL() {
if _, err := db.Exec("CREATE UNIQUE INDEX `UQE_protected_branch_repo_id_branch_name` ON `protected_branch` (`repo_id`, `branch_name`) WHERE `repo_id` != 0"); err != nil {
return err
}
if _, err := db.Exec("CREATE UNIQUE INDEX `UQE_protected_branch_owner_id_branch_name` ON `protected_branch` (`owner_id`, `branch_name`) WHERE `owner_id` != 0"); err != nil {
return err
}
}

return db.Commit()
}
Loading