feat(org): add org-level branch protection rulesets & configurable help URLs #72

Merged
jmiller merged 2 commits from feature/org-branch-protection into dev 2026-05-12 20:24:15 +00:00
15 changed files with 852 additions and 4 deletions
+197
View File
@@ -0,0 +1,197 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/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
}
+36 -2
View File
@@ -8,7 +8,10 @@ import (
"sort"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
)
@@ -82,13 +85,44 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string)
return results, nil
}
// GetFirstMatchProtectedBranchRule returns the first matched rules
// GetFirstMatchProtectedBranchRule returns the first matched rule.
// It checks repo-level rules first; if none match, it falls back to org-level rules
// (if the repo belongs to an organization).
func GetFirstMatchProtectedBranchRule(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) {
rules, err := FindRepoProtectedBranchRules(ctx, repoID)
if err != nil {
return nil, err
}
return rules.GetFirstMatched(branchName), nil
if matched := rules.GetFirstMatched(branchName); matched != nil {
return matched, nil
}
// Fall back to org-level rules
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return nil, err
}
owner, err := user_model.GetUserByID(ctx, repo.OwnerID)
if err != nil {
return nil, err
}
if !owner.IsOrganization() {
return nil, nil
}
orgRule, err := FindOrgBranchRuleForBranch(ctx, owner.ID, branchName)
if err != nil {
log.Error("FindOrgBranchRuleForBranch: %v", err)
return nil, nil
}
if orgRule == nil {
return nil, nil
}
// Convert org rule to a ProtectedBranch with RepoID set so callers work correctly
pb := orgRule.ToProtectedBranch()
pb.RepoID = repoID
return pb, nil
}
// IsBranchProtected checks if branch is protected
+1
View File
@@ -409,6 +409,7 @@ func prepareMigrationTasks() []*migration {
// Gitea 1.26.0 ends at migration ID number 330 (database version 331)
newMigration(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel),
newMigration(332, "Add org-level branch protection rulesets", v1_27.AddOrgProtectedBranchTable),
}
return preparedMigrations
}
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)
type orgProtectedBranch struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE(s) index"`
RuleName string `xorm:"UNIQUE(s)"`
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
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 (orgProtectedBranch) TableName() string {
return "org_protected_branch"
}
// AddOrgProtectedBranchTable creates the org_protected_branch table.
func AddOrgProtectedBranchTable(x *xorm.Engine) error {
return x.Sync(new(orgProtectedBranch))
}
+5
View File
@@ -15,4 +15,9 @@ var (
// AppName is the Application name, used in the page title. ini: "APP_NAME"
AppName string
// HelpURL is the URL shown in the help menu. ini: "HELP_URL"
HelpURL string
// SupportURL is the URL for support links. ini: "SUPPORT_URL"
SupportURL string
)
+2
View File
@@ -116,6 +116,8 @@ var (
func loadServerFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("server")
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea")
HelpURL = rootCfg.Section("").Key("HELP_URL").MustString("https://docs.gitea.com")
SupportURL = rootCfg.Section("").Key("SUPPORT_URL").MustString("")
Domain = sec.Key("DOMAIN").MustString("localhost")
HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
import "time"
// OrgBranchProtection represents an org-level branch protection ruleset
type OrgBranchProtection struct {
ID int64 `json:"id"`
OrgID int64 `json:"org_id"`
RuleName string `json:"rule_name"`
Priority int64 `json:"priority"`
EnablePush bool `json:"enable_push"`
EnablePushWhitelist bool `json:"enable_push_whitelist"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
EnableForcePush bool `json:"enable_force_push"`
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
EnableStatusCheck bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
RequiredApprovals int64 `json:"required_approvals"`
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"`
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
DismissStaleApprovals bool `json:"dismiss_stale_approvals"`
IgnoreStaleApprovals bool `json:"ignore_stale_approvals"`
RequireSignedCommits bool `json:"require_signed_commits"`
ProtectedFilePatterns string `json:"protected_file_patterns"`
UnprotectedFilePatterns string `json:"unprotected_file_patterns"`
BlockAdminMergeOverride bool `json:"block_admin_merge_override"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
}
// CreateOrgBranchProtectionOption options for creating an org-level branch protection
type CreateOrgBranchProtectionOption struct {
RuleName string `json:"rule_name" binding:"Required"`
Priority int64 `json:"priority"`
EnablePush bool `json:"enable_push"`
EnablePushWhitelist bool `json:"enable_push_whitelist"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
EnableForcePush bool `json:"enable_force_push"`
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
EnableStatusCheck bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
RequiredApprovals int64 `json:"required_approvals"`
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"`
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"`
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
DismissStaleApprovals bool `json:"dismiss_stale_approvals"`
IgnoreStaleApprovals bool `json:"ignore_stale_approvals"`
RequireSignedCommits bool `json:"require_signed_commits"`
ProtectedFilePatterns string `json:"protected_file_patterns"`
UnprotectedFilePatterns string `json:"unprotected_file_patterns"`
BlockAdminMergeOverride bool `json:"block_admin_merge_override"`
}
// EditOrgBranchProtectionOption options for editing an org-level branch protection
type EditOrgBranchProtectionOption struct {
Priority *int64 `json:"priority"`
EnablePush *bool `json:"enable_push"`
EnablePushWhitelist *bool `json:"enable_push_whitelist"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
EnableForcePush *bool `json:"enable_force_push"`
EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
EnableStatusCheck *bool `json:"enable_status_check"`
StatusCheckContexts []string `json:"status_check_contexts"`
RequiredApprovals *int64 `json:"required_approvals"`
EnableApprovalsWhitelist *bool `json:"enable_approvals_whitelist"`
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"`
BlockOnRejectedReviews *bool `json:"block_on_rejected_reviews"`
BlockOnOfficialReviewRequests *bool `json:"block_on_official_review_requests"`
BlockOnOutdatedBranch *bool `json:"block_on_outdated_branch"`
DismissStaleApprovals *bool `json:"dismiss_stale_approvals"`
IgnoreStaleApprovals *bool `json:"ignore_stale_approvals"`
RequireSignedCommits *bool `json:"require_signed_commits"`
ProtectedFilePatterns *string `json:"protected_file_patterns"`
UnprotectedFilePatterns *string `json:"unprotected_file_patterns"`
BlockAdminMergeOverride *bool `json:"block_admin_merge_override"`
}
+2
View File
@@ -65,6 +65,8 @@ type BranchProtection struct {
ProtectedFilePatterns string `json:"protected_file_patterns"`
UnprotectedFilePatterns string `json:"unprotected_file_patterns"`
BlockAdminMergeOverride bool `json:"block_admin_merge_override"`
// InheritedFrom indicates where this rule originates ("org" if inherited from org-level rules, empty if repo-level)
InheritedFrom string `json:"inherited_from,omitempty"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
+6
View File
@@ -83,6 +83,12 @@ func newFuncMapWebPage() template.FuncMap {
"AppVer": func() string {
return setting.AppVer
},
"HelpURL": func() string {
return setting.HelpURL
},
"SupportURL": func() string {
return setting.SupportURL
},
"AppDomain": func() string { // TODO: helm registry still uses it, need to use current request host in the future
return setting.Domain
},
+3
View File
@@ -3243,6 +3243,9 @@
"admin.config.lfs_root_path": "LFS Root Path",
"admin.config.log_file_root_path": "Log Path",
"admin.config.script_type": "Script Type",
"admin.config.help_url": "Help URL",
"admin.config.support_url": "Support URL",
"admin.config.not_set": "Not set",
"admin.config.reverse_auth_user": "Reverse Authentication User",
"admin.config.ssh_config": "SSH Configuration",
"admin.config.ssh_enabled": "Enabled",
+9
View File
@@ -1696,6 +1696,15 @@ func Routes() *web.Router {
m.Delete("", org.DeleteAvatar)
}, reqToken(), reqOrgOwnership())
m.Get("/activities/feeds", org.ListOrgActivityFeeds)
m.Group("/branch_protections", func() {
m.Combo("").Get(org.ListOrgBranchProtections).
Post(bind(api.CreateOrgBranchProtectionOption{}), org.CreateOrgBranchProtection)
m.Group("/*", func() {
m.Get("", org.GetOrgBranchProtection)
m.Patch("", bind(api.EditOrgBranchProtectionOption{}), org.EditOrgBranchProtection)
m.Delete("", org.DeleteOrgBranchProtection)
})
}, reqToken(), reqOrgOwnership())
m.Group("/blocks", func() {
m.Get("", org.ListBlocks)
+432
View File
@@ -0,0 +1,432 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
)
// toAPIOrgBranchProtection converts model to API response
func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtectedBranch) *api.OrgBranchProtection {
orgID := rule.OrgID
teams, err := organization.FindOrgTeams(ctx, orgID)
if err != nil {
teams = nil
}
teamNamesByID := make(map[int64]string, len(teams))
for _, t := range teams {
teamNamesByID[t.ID] = t.Name
}
resolveTeamNames := func(ids []int64) []string {
names := make([]string, 0, len(ids))
for _, id := range ids {
if name, ok := teamNamesByID[id]; ok {
names = append(names, name)
}
}
return names
}
return &api.OrgBranchProtection{
ID: rule.ID,
OrgID: rule.OrgID,
RuleName: rule.RuleName,
Priority: rule.Priority,
EnablePush: rule.CanPush,
EnablePushWhitelist: rule.EnableWhitelist,
PushWhitelistTeams: resolveTeamNames(rule.WhitelistTeamIDs),
EnableForcePush: rule.CanForcePush,
EnableForcePushAllowlist: rule.EnableForcePushAllowlist,
ForcePushAllowlistTeams: resolveTeamNames(rule.ForcePushAllowlistTeamIDs),
EnableMergeWhitelist: rule.EnableMergeWhitelist,
MergeWhitelistTeams: resolveTeamNames(rule.MergeWhitelistTeamIDs),
EnableStatusCheck: rule.EnableStatusCheck,
StatusCheckContexts: rule.StatusCheckContexts,
RequiredApprovals: rule.RequiredApprovals,
EnableApprovalsWhitelist: rule.EnableApprovalsWhitelist,
ApprovalsWhitelistTeams: resolveTeamNames(rule.ApprovalsWhitelistTeamIDs),
BlockOnRejectedReviews: rule.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: rule.BlockOnOfficialReviewRequests,
BlockOnOutdatedBranch: rule.BlockOnOutdatedBranch,
DismissStaleApprovals: rule.DismissStaleApprovals,
IgnoreStaleApprovals: rule.IgnoreStaleApprovals,
RequireSignedCommits: rule.RequireSignedCommits,
ProtectedFilePatterns: rule.ProtectedFilePatterns,
UnprotectedFilePatterns: rule.UnprotectedFilePatterns,
BlockAdminMergeOverride: rule.BlockAdminMergeOverride,
Created: rule.CreatedUnix.AsTime(),
Updated: rule.UpdatedUnix.AsTime(),
}
}
// resolveTeamIDs converts team names to IDs for the given org, returning 422 on unknown team
func resolveTeamIDs(ctx *context.APIContext, orgID int64, names []string) ([]int64, bool) {
if len(names) == 0 {
return nil, true
}
ids, err := organization.GetTeamIDsByNames(ctx, orgID, names, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return nil, false
}
return ids, true
}
// ListOrgBranchProtections list org-level branch protection rules
func ListOrgBranchProtections(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/branch_protections organization orgListBranchProtections
// ---
// summary: List an organization's branch protection rules
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgBranchProtectionList"
// "404":
// "$ref": "#/responses/notFound"
rules, err := git_model.FindOrgProtectedBranchRules(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiRules := make([]*api.OrgBranchProtection, len(rules))
for i, rule := range rules {
apiRules[i] = toAPIOrgBranchProtection(ctx, rule)
}
ctx.JSON(http.StatusOK, apiRules)
}
// GetOrgBranchProtection get a specific org-level branch protection rule
func GetOrgBranchProtection(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/branch_protections/{name} organization orgGetBranchProtection
// ---
// summary: Get a specific org-level branch protection rule
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: name
// in: path
// description: name of the branch protection rule
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgBranchProtection"
// "404":
// "$ref": "#/responses/notFound"
ruleName := ctx.PathParam("*")
rule, err := git_model.GetOrgProtectedBranchByName(ctx, ctx.Org.Organization.ID, ruleName)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, toAPIOrgBranchProtection(ctx, rule))
}
// CreateOrgBranchProtection create an org-level branch protection rule
func CreateOrgBranchProtection(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/branch_protections organization orgCreateBranchProtection
// ---
// summary: Create an org-level branch protection rule
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateOrgBranchProtectionOption"
// responses:
// "201":
// "$ref": "#/responses/OrgBranchProtection"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateOrgBranchProtectionOption)
orgID := ctx.Org.Organization.ID
existing, err := git_model.GetOrgProtectedBranchByName(ctx, orgID, form.RuleName)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if existing != nil {
ctx.APIError(http.StatusForbidden, "org branch protection rule already exists for this pattern")
return
}
pushTeams, ok := resolveTeamIDs(ctx, orgID, form.PushWhitelistTeams)
if !ok {
return
}
forcePushTeams, ok := resolveTeamIDs(ctx, orgID, form.ForcePushAllowlistTeams)
if !ok {
return
}
mergeTeams, ok := resolveTeamIDs(ctx, orgID, form.MergeWhitelistTeams)
if !ok {
return
}
approvalsTeams, ok := resolveTeamIDs(ctx, orgID, form.ApprovalsWhitelistTeams)
if !ok {
return
}
rule := &git_model.OrgProtectedBranch{
OrgID: orgID,
RuleName: form.RuleName,
Priority: form.Priority,
CanPush: form.EnablePush,
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
WhitelistTeamIDs: pushTeams,
CanForcePush: form.EnablePush && form.EnableForcePush,
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
ForcePushAllowlistTeamIDs: forcePushTeams,
EnableMergeWhitelist: form.EnableMergeWhitelist,
MergeWhitelistTeamIDs: mergeTeams,
EnableStatusCheck: form.EnableStatusCheck,
StatusCheckContexts: form.StatusCheckContexts,
RequiredApprovals: form.RequiredApprovals,
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
ApprovalsWhitelistTeamIDs: approvalsTeams,
BlockOnRejectedReviews: form.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests,
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
DismissStaleApprovals: form.DismissStaleApprovals,
IgnoreStaleApprovals: form.IgnoreStaleApprovals,
RequireSignedCommits: form.RequireSignedCommits,
ProtectedFilePatterns: form.ProtectedFilePatterns,
UnprotectedFilePatterns: form.UnprotectedFilePatterns,
BlockAdminMergeOverride: form.BlockAdminMergeOverride,
}
if err := git_model.CreateOrgProtectedBranch(ctx, rule); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, toAPIOrgBranchProtection(ctx, rule))
}
// EditOrgBranchProtection edit an org-level branch protection rule
func EditOrgBranchProtection(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org}/branch_protections/{name} organization orgEditBranchProtection
// ---
// summary: Edit an org-level branch protection rule. Only fields that are set will be changed
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: name
// in: path
// description: name of the branch protection rule
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditOrgBranchProtectionOption"
// responses:
// "200":
// "$ref": "#/responses/OrgBranchProtection"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditOrgBranchProtectionOption)
orgID := ctx.Org.Organization.ID
ruleName := ctx.PathParam("*")
rule, err := git_model.GetOrgProtectedBranchByName(ctx, orgID, ruleName)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil {
ctx.APIErrorNotFound()
return
}
if form.Priority != nil {
rule.Priority = *form.Priority
}
if form.EnablePush != nil {
rule.CanPush = *form.EnablePush
}
if form.EnablePushWhitelist != nil {
rule.EnableWhitelist = *form.EnablePushWhitelist
}
if form.PushWhitelistTeams != nil {
ids, ok := resolveTeamIDs(ctx, orgID, form.PushWhitelistTeams)
if !ok {
return
}
rule.WhitelistTeamIDs = ids
}
if form.EnableForcePush != nil {
rule.CanForcePush = *form.EnableForcePush
}
if form.EnableForcePushAllowlist != nil {
rule.EnableForcePushAllowlist = *form.EnableForcePushAllowlist
}
if form.ForcePushAllowlistTeams != nil {
ids, ok := resolveTeamIDs(ctx, orgID, form.ForcePushAllowlistTeams)
if !ok {
return
}
rule.ForcePushAllowlistTeamIDs = ids
}
if form.EnableMergeWhitelist != nil {
rule.EnableMergeWhitelist = *form.EnableMergeWhitelist
}
if form.MergeWhitelistTeams != nil {
ids, ok := resolveTeamIDs(ctx, orgID, form.MergeWhitelistTeams)
if !ok {
return
}
rule.MergeWhitelistTeamIDs = ids
}
if form.EnableStatusCheck != nil {
rule.EnableStatusCheck = *form.EnableStatusCheck
}
if form.StatusCheckContexts != nil {
rule.StatusCheckContexts = form.StatusCheckContexts
}
if form.RequiredApprovals != nil {
rule.RequiredApprovals = *form.RequiredApprovals
}
if form.EnableApprovalsWhitelist != nil {
rule.EnableApprovalsWhitelist = *form.EnableApprovalsWhitelist
}
if form.ApprovalsWhitelistTeams != nil {
ids, ok := resolveTeamIDs(ctx, orgID, form.ApprovalsWhitelistTeams)
if !ok {
return
}
rule.ApprovalsWhitelistTeamIDs = ids
}
if form.BlockOnRejectedReviews != nil {
rule.BlockOnRejectedReviews = *form.BlockOnRejectedReviews
}
if form.BlockOnOfficialReviewRequests != nil {
rule.BlockOnOfficialReviewRequests = *form.BlockOnOfficialReviewRequests
}
if form.BlockOnOutdatedBranch != nil {
rule.BlockOnOutdatedBranch = *form.BlockOnOutdatedBranch
}
if form.DismissStaleApprovals != nil {
rule.DismissStaleApprovals = *form.DismissStaleApprovals
}
if form.IgnoreStaleApprovals != nil {
rule.IgnoreStaleApprovals = *form.IgnoreStaleApprovals
}
if form.RequireSignedCommits != nil {
rule.RequireSignedCommits = *form.RequireSignedCommits
}
if form.ProtectedFilePatterns != nil {
rule.ProtectedFilePatterns = *form.ProtectedFilePatterns
}
if form.UnprotectedFilePatterns != nil {
rule.UnprotectedFilePatterns = *form.UnprotectedFilePatterns
}
if form.BlockAdminMergeOverride != nil {
rule.BlockAdminMergeOverride = *form.BlockAdminMergeOverride
}
if err := git_model.UpdateOrgProtectedBranch(ctx, rule); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgBranchProtection(ctx, rule))
}
// DeleteOrgBranchProtection delete an org-level branch protection rule
func DeleteOrgBranchProtection(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/branch_protections/{name} organization orgDeleteBranchProtection
// ---
// summary: Delete an org-level branch protection rule
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: name
// in: path
// description: name of the branch protection rule
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
orgID := ctx.Org.Organization.ID
ruleName := ctx.PathParam("*")
rule, err := git_model.GetOrgProtectedBranchByName(ctx, orgID, ruleName)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil {
ctx.APIErrorNotFound()
return
}
if err := git_model.DeleteOrgProtectedBranch(ctx, orgID, rule.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+2
View File
@@ -114,6 +114,8 @@ func Config(ctx *context.Context) {
ctx.Data["Git"] = setting.Git
ctx.Data["AccessLogTemplate"] = setting.Log.AccessLogTemplate
ctx.Data["LogSQL"] = setting.Database.LogSQL
ctx.Data["HelpURL"] = setting.HelpURL
ctx.Data["SupportURL"] = setting.SupportURL
ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
config.GetDynGetter().InvalidateCache()
+10
View File
@@ -38,6 +38,16 @@
<dd>{{.LogRootPath}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.script_type"}}</dt>
<dd>{{.ScriptType}}</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.config.help_url"}}</dt>
<dd>{{if .HelpURL}}{{.HelpURL}}{{else}}<i>{{ctx.Locale.Tr "admin.config.not_set"}}</i>{{end}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.support_url"}}</dt>
<dd>{{if .SupportURL}}{{.SupportURL}}{{else}}<i>{{ctx.Locale.Tr "admin.config.not_set"}}</i>{{end}}</dd>
<div class="divider"></div>
<dt>{{ctx.Locale.Tr "admin.config.reverse_auth_user"}}</dt>
<dd>{{.ReverseProxyAuthUser}}</dd>
</dl>
+2 -2
View File
@@ -36,7 +36,7 @@
{{template "custom/extra_links" .}}
{{if not .IsSigned}}
<a class="item" target="_blank" href="https://docs.gitea.com">{{ctx.Locale.Tr "help"}}</a>
<a class="item" target="_blank" href="{{HelpURL}}">{{ctx.Locale.Tr "help"}}</a>
{{end}}
</div>
@@ -119,7 +119,7 @@
{{svg "octicon-tools"}}
{{ctx.Locale.Tr "your_settings"}}
</a>
<a class="item" target="_blank" href="https://docs.gitea.com">
<a class="item" target="_blank" href="{{HelpURL}}">
{{svg "octicon-question"}}
{{ctx.Locale.Tr "help"}}
</a>