release: promote dev to main — org branch protection & help URLs #73
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user