// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package git import ( "context" "fmt" "strings" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/glob" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil" "xorm.io/builder" ) // OrgProtectedBranch represents an org-level branch protection ruleset. // These rules cascade to all repositories within the organization. type OrgProtectedBranch struct { ID int64 `xorm:"pk autoincr"` OrgID int64 `xorm:"UNIQUE(s) index"` RuleName string `xorm:"UNIQUE(s)"` // branch glob pattern Priority int64 `xorm:"NOT NULL DEFAULT 0"` globRule glob.Glob `xorm:"-"` isPlainName bool `xorm:"-"` CanPush bool `xorm:"NOT NULL DEFAULT false"` EnableWhitelist bool `xorm:"NOT NULL DEFAULT false"` WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"` MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` CanForcePush bool `xorm:"NOT NULL DEFAULT false"` EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"` ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"` StatusCheckContexts []string `xorm:"JSON TEXT"` RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"` EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"` ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` 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"` IgnoreStaleApprovals bool `xorm:"NOT NULL DEFAULT false"` RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"` ProtectedFilePatterns string `xorm:"TEXT"` UnprotectedFilePatterns string `xorm:"TEXT"` BlockAdminMergeOverride bool `xorm:"NOT NULL DEFAULT false"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` } func init() { db.RegisterModel(new(OrgProtectedBranch)) } func (o *OrgProtectedBranch) loadGlob() { if o.isPlainName || o.globRule != nil { return } if !IsRuleNameSpecial(o.RuleName) { o.isPlainName = true return } var err error o.globRule, err = glob.Compile(o.RuleName, '/') if err != nil { log.Warn("Invalid glob rule for OrgProtectedBranch[%d]: %s %v", o.ID, o.RuleName, err) o.globRule = glob.MustCompile(glob.QuoteMeta(o.RuleName), '/') } } // Match tests if branchName matches this org rule func (o *OrgProtectedBranch) Match(branchName string) bool { o.loadGlob() if o.isPlainName { return strings.EqualFold(o.RuleName, branchName) } return o.globRule.Match(branchName) } // ToProtectedBranch converts an org-level rule to a ProtectedBranch for use // in the standard enforcement path. Fields that are user-scoped (WhitelistUserIDs etc.) // are left empty because org rules only reference teams. func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch { return &ProtectedBranch{ ID: o.ID, RuleName: o.RuleName, Priority: o.Priority, CanPush: o.CanPush, EnableWhitelist: o.EnableWhitelist, WhitelistTeamIDs: o.WhitelistTeamIDs, EnableMergeWhitelist: o.EnableMergeWhitelist, MergeWhitelistTeamIDs: o.MergeWhitelistTeamIDs, CanForcePush: o.CanForcePush, EnableForcePushAllowlist: o.EnableForcePushAllowlist, ForcePushAllowlistTeamIDs: o.ForcePushAllowlistTeamIDs, EnableStatusCheck: o.EnableStatusCheck, StatusCheckContexts: o.StatusCheckContexts, RequiredApprovals: o.RequiredApprovals, EnableApprovalsWhitelist: o.EnableApprovalsWhitelist, ApprovalsWhitelistTeamIDs: o.ApprovalsWhitelistTeamIDs, BlockOnRejectedReviews: o.BlockOnRejectedReviews, BlockOnOfficialReviewRequests: o.BlockOnOfficialReviewRequests, BlockOnOutdatedBranch: o.BlockOnOutdatedBranch, DismissStaleApprovals: o.DismissStaleApprovals, IgnoreStaleApprovals: o.IgnoreStaleApprovals, RequireSignedCommits: o.RequireSignedCommits, ProtectedFilePatterns: o.ProtectedFilePatterns, UnprotectedFilePatterns: o.UnprotectedFilePatterns, BlockAdminMergeOverride: o.BlockAdminMergeOverride, CreatedUnix: o.CreatedUnix, UpdatedUnix: o.UpdatedUnix, } } // GetOrgProtectedBranchByName retrieves a single org rule by org ID and rule name func GetOrgProtectedBranchByName(ctx context.Context, orgID int64, ruleName string) (*OrgProtectedBranch, error) { rule, exist, err := db.Get[OrgProtectedBranch](ctx, builder.Eq{"org_id": orgID, "rule_name": ruleName}) if err != nil { return nil, err } else if !exist { return nil, nil //nolint:nilnil } return rule, nil } // GetOrgProtectedBranchByID retrieves a single org rule by ID func GetOrgProtectedBranchByID(ctx context.Context, orgID, id int64) (*OrgProtectedBranch, error) { rule, exist, err := db.Get[OrgProtectedBranch](ctx, builder.Eq{"org_id": orgID, "id": id}) if err != nil { return nil, err } else if !exist { return nil, nil //nolint:nilnil } return rule, nil } // FindOrgProtectedBranchRules loads all org-level branch protection rules func FindOrgProtectedBranchRules(ctx context.Context, orgID int64) ([]*OrgProtectedBranch, error) { var rules []*OrgProtectedBranch err := db.GetEngine(ctx).Where("org_id = ?", orgID).Asc("priority", "created_unix").Find(&rules) return rules, err } // CreateOrgProtectedBranch creates a new org-level branch protection rule func CreateOrgProtectedBranch(ctx context.Context, rule *OrgProtectedBranch) error { if rule.Priority == 0 { var maxPrio int64 if _, err := db.GetEngine(ctx).SQL(`SELECT MAX(priority) FROM org_protected_branch WHERE org_id = ?`, rule.OrgID). Get(&maxPrio); err != nil { return err } rule.Priority = maxPrio + 1 } _, err := db.GetEngine(ctx).Insert(rule) if err != nil { return fmt.Errorf("Insert OrgProtectedBranch: %v", err) } return nil } // UpdateOrgProtectedBranch updates an existing org-level branch protection rule func UpdateOrgProtectedBranch(ctx context.Context, rule *OrgProtectedBranch) error { _, err := db.GetEngine(ctx).ID(rule.ID).AllCols().Update(rule) if err != nil { return fmt.Errorf("Update OrgProtectedBranch: %v", err) } return nil } // DeleteOrgProtectedBranch deletes an org-level branch protection rule func DeleteOrgProtectedBranch(ctx context.Context, orgID, id int64) error { affected, err := db.GetEngine(ctx).Where("org_id = ? AND id = ?", orgID, id).Delete(new(OrgProtectedBranch)) if err != nil { return err } if affected == 0 { return fmt.Errorf("org branch protection rule ID(%d) not found", id) } return nil } // FindOrgBranchRuleForBranch finds the first matching org rule for a given branch name func FindOrgBranchRuleForBranch(ctx context.Context, orgID int64, branchName string) (*OrgProtectedBranch, error) { rules, err := FindOrgProtectedBranchRules(ctx, orgID) if err != nil { return nil, err } for _, rule := range rules { if rule.Match(branchName) { return rule, nil } } return nil, nil //nolint:nilnil }