Compare commits

..

1 Commits

Author SHA1 Message Date
jmiller 56e65268d7 feat: org branch protection user allowlists + actions-bot toggles (#734)
Org-level branch protection rules previously supported team-only
allowlists. This adds per-user allowlists (resolved by username OR
email) and "allow the actions bot" toggles for the push, merge and
force-push allowlists, plus a per-user approvals allowlist -- mirroring
the repo-level ProtectedBranch fields exactly. These carry through
ToProtectedBranch() into the standard enforcement path.

Model: add WhitelistUserIDs/MergeWhitelistUserIDs/
ForcePushAllowlistUserIDs/ApprovalsWhitelistUserIDs ([]int64 JSON) and
WhitelistActionsUser/MergeWhitelistActionsUser/
ForcePushAllowlistActionsUser (bool) to OrgProtectedBranch, and copy
them in ToProtectedBranch().

Migration 362 (v368-style file v363.go): additive columns via x.Sync.

API: new resolveUserIDs helper (username-then-email lookup, dedupe,
422 on unknown); Create/Edit set the new fields gated on the matching
Enable* flags; GET resolves stored user IDs back to usernames and
returns the actions-user bools.

DTOs: add *Usernames []string + *ActionsUser bool to the Create/Edit/
response structs, using the same json names as repo_branch.go.

Swagger: register OrgBranchProtection(/List) responses and the
Create/Edit option bodies; regenerate specs for the org endpoints.

Delete allowlists are intentionally omitted -- org branch protection
has no delete-allowlist concept (no CanDelete/EnableDeleteAllowlist/
DeleteAllowlistTeamIDs), so there is no team allowlist to extend.

Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
2026-07-05 17:17:20 -05:00
44 changed files with 1694 additions and 2064 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: Feature Request
about: Suggest a new feature or enhancement
title: '(feat) '
title: '[FEATURE] '
labels: 'enhancement'
assignees: ''
-7
View File
@@ -3,12 +3,6 @@
## [Unreleased]
### Added
- Org branch protection: repositories now show the inherited organization rules read-only in their Branch Protection settings, with an expandable detail (direct push, force-push, branch deletion, merge restrictions, required approvals, status checks, protected files, and whitelisted teams) — like GitHub surfaces org rulesets in a repo (#727)
- Org branch protection: org-level rules can now also protect against branch deletion (`enable_delete` + delete allowlist teams), mirroring the per-repo delete allowlist (#727)
- Org-level tag protection: protect tag patterns org-wide (e.g. `v*`) with a team allowlist, layered on top of each repo's own protected tags — a tag is controllable only if allowed at both levels (fail-closed). API at `/orgs/{org}/tag_protections`; enforced at the git push/delete hook and the release create/delete paths; shown read-only in the repo Tag settings (#727)
- Org-level push policy: one policy per org, enforced in the pre-receive hook across all its repositories — branch/tag name conventions (glob), a mandatory secret-scanning block-on-push that repos cannot disable, a max pushed-file size, and blocked file-path patterns. API at `/orgs/{org}/push_policy`. Naming is fail-closed; the content checks (blocked paths, max size) fail open on error so a policy bug can never block every push (#727)
- Org-level repository defaults: an org can force new/transferred repositories private and set default pull-request settings (allowed merge styles, default merge style, auto-delete branch after merge), applied via a notifier when a repo is created in or transferred into the org (best-effort — never blocks repo creation). API at `/orgs/{org}/repo_defaults` (#727)
- Org-level email domain policy: restrict which email domains an organization's members may have — a user can only be added to the org (via any team) if their primary email matches one of the allowed domain globs. Enforced at the single membership-add choke point (`AddTeamMember`); API at `/orgs/{org}/email_domain_policy` (#727)
- Code security scanner: pattern-based detection of SQL injection, XSS, command injection, path traversal, insecure deserialization, hardcoded credentials, and weak cryptography across Go/PHP/Python/JS/TS (#552)
- Cascade merge: auto-create PRs to downstream branches after merge with configurable rules per repo (#460)
- Issue status presets: 4 built-in templates (default, software-development, support-tickets, bug-tracking) with API + web UI (#507)
@@ -63,7 +57,6 @@
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
### Fixed
- Org-level branch protection now **layers** with per-repo rules instead of being ignored whenever a repo rule exists. When both an org rule and a repo rule match a branch, the effective rule is the most-restrictive (fail-closed) combination — the org rule is a mandatory floor a repo cannot weaken: allow flags AND'd, gate/require/block flags OR'd, required approvals max'd, status checks and protected-file patterns unioned, whitelists intersected. Previously a repo rule shadowed the org rule entirely at the enforcement choke point (`GetFirstMatchProtectedBranchRule`), letting a repo opt out of org protection (#727)
- Org Teams page: list now renders — the handler wrote `ctx.Data["OrgListTeams"]` but the template reads `.Teams`, so the page showed header/nav but no teams (#720)
- Issue type: now editable after creation for users with issue write permission — the sidebar gated editing on a `FieldEditFlags` data key that was never populated (always read-only); now uses `HasIssuesOrPullsWritePermission` like the priority field (#721)
- Admin config form: radio inputs (e.g. instance landing page Mode) no longer throw "Unsupported config form value mapping", which had aborted all JS init on the admin settings page
-134
View File
@@ -1,134 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
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"
)
// OrgEmailDomainPolicy restricts which email domains an organization's members may
// have. When configured, a user can only be added to the org if their primary email
// matches one of the allowed domain globs. At most one row per org. See issue #727.
type OrgEmailDomainPolicy struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
AllowedDomains string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(OrgEmailDomainPolicy))
}
// ErrEmailDomainNotAllowed is returned when a user's email domain is not permitted
// by the organization's email domain policy.
type ErrEmailDomainNotAllowed struct {
Email string
OrgID int64
}
func (e ErrEmailDomainNotAllowed) Error() string {
return fmt.Sprintf("email %q is not in an allowed domain for organization %d", e.Email, e.OrgID)
}
// IsErrEmailDomainNotAllowed reports whether err is an ErrEmailDomainNotAllowed.
func IsErrEmailDomainNotAllowed(err error) bool {
_, ok := err.(ErrEmailDomainNotAllowed)
return ok
}
func (p *OrgEmailDomainPolicy) domainGlobs() []glob.Glob {
var out []glob.Glob
for _, d := range strings.Split(p.AllowedDomains, ";") {
d = strings.TrimSpace(strings.ToLower(d))
if d == "" {
continue
}
if g, err := glob.Compile(d); err == nil {
out = append(out, g)
} else {
log.Warn("Invalid org email domain glob %q: %v", d, err)
}
}
return out
}
// EmailAllowed reports whether email's domain satisfies the policy. An empty policy
// (no configured domains) allows any email.
func (p *OrgEmailDomainPolicy) EmailAllowed(email string) bool {
globs := p.domainGlobs()
if len(globs) == 0 {
return true
}
at := strings.LastIndexByte(email, '@')
if at < 0 {
return false
}
domain := strings.ToLower(email[at+1:])
for _, g := range globs {
if g.Match(domain) {
return true
}
}
return false
}
// GetOrgEmailDomainPolicy returns the org's email domain policy, or nil if none.
func GetOrgEmailDomainPolicy(ctx context.Context, orgID int64) (*OrgEmailDomainPolicy, error) {
policy, exist, err := db.Get[OrgEmailDomainPolicy](ctx, builder.Eq{"org_id": orgID})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return policy, nil
}
// OrgEmailDomainAllowed reports whether email is permitted for the org. It returns
// true when the org has no policy configured.
func OrgEmailDomainAllowed(ctx context.Context, orgID int64, email string) (bool, error) {
policy, err := GetOrgEmailDomainPolicy(ctx, orgID)
if err != nil {
return false, err
}
if policy == nil {
return true, nil
}
return policy.EmailAllowed(email), nil
}
// UpsertOrgEmailDomainPolicy creates or updates the single policy row for an org.
func UpsertOrgEmailDomainPolicy(ctx context.Context, policy *OrgEmailDomainPolicy) error {
existing, err := GetOrgEmailDomainPolicy(ctx, policy.OrgID)
if err != nil {
return err
}
if existing == nil {
if _, err := db.GetEngine(ctx).Insert(policy); err != nil {
return fmt.Errorf("Insert OrgEmailDomainPolicy: %v", err)
}
return nil
}
policy.ID = existing.ID
if _, err := db.GetEngine(ctx).ID(existing.ID).AllCols().Update(policy); err != nil {
return fmt.Errorf("Update OrgEmailDomainPolicy: %v", err)
}
return nil
}
// DeleteOrgEmailDomainPolicy removes an org's email domain policy.
func DeleteOrgEmailDomainPolicy(ctx context.Context, orgID int64) error {
_, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Delete(new(OrgEmailDomainPolicy))
return err
}
+18 -8
View File
@@ -28,19 +28,23 @@ type OrgProtectedBranch struct {
CanPush bool `xorm:"NOT NULL DEFAULT false"`
EnableWhitelist bool `xorm:"NOT NULL DEFAULT false"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
CanForcePush bool `xorm:"NOT NULL DEFAULT false"`
EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"`
ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
CanDelete bool `xorm:"NOT NULL DEFAULT false"`
EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"`
DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
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"`
ApprovalsWhitelistUserIDs []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"`
@@ -84,8 +88,10 @@ func (o *OrgProtectedBranch) Match(branchName string) bool {
}
// 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.
// in the standard enforcement path. Both team-scoped and user-scoped allowlists
// (plus the "allow the actions bot" toggles) are carried over so org rules can
// grant push/merge/force-push/approval access to individual users and the actions
// bot, in addition to teams.
func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
return &ProtectedBranch{
ID: o.ID,
@@ -94,19 +100,23 @@ func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
CanPush: o.CanPush,
EnableWhitelist: o.EnableWhitelist,
WhitelistTeamIDs: o.WhitelistTeamIDs,
WhitelistUserIDs: o.WhitelistUserIDs,
WhitelistActionsUser: o.WhitelistActionsUser,
EnableMergeWhitelist: o.EnableMergeWhitelist,
MergeWhitelistTeamIDs: o.MergeWhitelistTeamIDs,
MergeWhitelistUserIDs: o.MergeWhitelistUserIDs,
MergeWhitelistActionsUser: o.MergeWhitelistActionsUser,
CanForcePush: o.CanForcePush,
EnableForcePushAllowlist: o.EnableForcePushAllowlist,
ForcePushAllowlistTeamIDs: o.ForcePushAllowlistTeamIDs,
CanDelete: o.CanDelete,
EnableDeleteAllowlist: o.EnableDeleteAllowlist,
DeleteAllowlistTeamIDs: o.DeleteAllowlistTeamIDs,
ForcePushAllowlistUserIDs: o.ForcePushAllowlistUserIDs,
ForcePushAllowlistActionsUser: o.ForcePushAllowlistActionsUser,
EnableStatusCheck: o.EnableStatusCheck,
StatusCheckContexts: o.StatusCheckContexts,
RequiredApprovals: o.RequiredApprovals,
EnableApprovalsWhitelist: o.EnableApprovalsWhitelist,
ApprovalsWhitelistTeamIDs: o.ApprovalsWhitelistTeamIDs,
ApprovalsWhitelistUserIDs: o.ApprovalsWhitelistUserIDs,
BlockOnRejectedReviews: o.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: o.BlockOnOfficialReviewRequests,
BlockOnOutdatedBranch: o.BlockOnOutdatedBranch,
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestOrgProtectedBranchToProtectedBranch verifies that the org-level rule carries
// its team allowlists, user allowlists and actions-user toggles across to the
// standard ProtectedBranch used by the enforcement path.
func TestOrgProtectedBranchToProtectedBranch(t *testing.T) {
org := &OrgProtectedBranch{
ID: 7,
RuleName: "main",
Priority: 3,
CanPush: true,
EnableWhitelist: true,
WhitelistTeamIDs: []int64{1, 2},
WhitelistUserIDs: []int64{10, 11},
WhitelistActionsUser: true,
EnableMergeWhitelist: true,
MergeWhitelistTeamIDs: []int64{3},
MergeWhitelistUserIDs: []int64{12},
MergeWhitelistActionsUser: true,
CanForcePush: true,
EnableForcePushAllowlist: true,
ForcePushAllowlistTeamIDs: []int64{4},
ForcePushAllowlistUserIDs: []int64{13, 14},
ForcePushAllowlistActionsUser: true,
RequiredApprovals: 2,
EnableApprovalsWhitelist: true,
ApprovalsWhitelistTeamIDs: []int64{5},
ApprovalsWhitelistUserIDs: []int64{15},
}
pb := org.ToProtectedBranch()
// Push allowlist
assert.Equal(t, []int64{1, 2}, pb.WhitelistTeamIDs)
assert.Equal(t, []int64{10, 11}, pb.WhitelistUserIDs)
assert.True(t, pb.WhitelistActionsUser)
// Merge allowlist
assert.Equal(t, []int64{3}, pb.MergeWhitelistTeamIDs)
assert.Equal(t, []int64{12}, pb.MergeWhitelistUserIDs)
assert.True(t, pb.MergeWhitelistActionsUser)
// Force-push allowlist
assert.Equal(t, []int64{4}, pb.ForcePushAllowlistTeamIDs)
assert.Equal(t, []int64{13, 14}, pb.ForcePushAllowlistUserIDs)
assert.True(t, pb.ForcePushAllowlistActionsUser)
// Approvals allowlist (no actions-user at the approval stage, matching repo-level)
assert.Equal(t, []int64{5}, pb.ApprovalsWhitelistTeamIDs)
assert.Equal(t, []int64{15}, pb.ApprovalsWhitelistUserIDs)
// Sanity: identity fields propagate
assert.Equal(t, int64(7), pb.ID)
assert.Equal(t, "main", pb.RuleName)
assert.Equal(t, int64(3), pb.Priority)
assert.EqualValues(t, 2, pb.RequiredApprovals)
}
// TestOrgProtectedBranchToProtectedBranchEmpty verifies that when the org rule does
// not grant actions-user access, the toggles remain false (opt-in semantics).
func TestOrgProtectedBranchToProtectedBranchEmpty(t *testing.T) {
pb := (&OrgProtectedBranch{RuleName: "release/*"}).ToProtectedBranch()
assert.False(t, pb.WhitelistActionsUser)
assert.False(t, pb.MergeWhitelistActionsUser)
assert.False(t, pb.ForcePushAllowlistActionsUser)
assert.Nil(t, pb.WhitelistUserIDs)
assert.Nil(t, pb.MergeWhitelistUserIDs)
assert.Nil(t, pb.ForcePushAllowlistUserIDs)
assert.Nil(t, pb.ApprovalsWhitelistUserIDs)
}
-133
View File
@@ -1,133 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import (
"context"
"fmt"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/builder"
)
// OrgProtectedTag represents an org-level tag protection rule. It cascades to all
// repositories in the organization and layers on top of each repo's own protected
// tags (a tag is controllable only if allowed at both levels). Org rules reference
// teams only (like OrgProtectedBranch). See issue #727.
type OrgProtectedTag struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE(s) index"`
NamePattern string `xorm:"UNIQUE(s)"`
AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(OrgProtectedTag))
}
// ToProtectedTag converts an org-level tag rule into a repo-scoped ProtectedTag so
// the standard name-matching and allowlist logic can be reused. Org rules are
// team-only, so the user allowlist is left empty.
func (o *OrgProtectedTag) ToProtectedTag() *ProtectedTag {
return &ProtectedTag{
NamePattern: o.NamePattern,
AllowlistTeamIDs: o.AllowlistTeamIDs,
}
}
// GetOrgProtectedTagByID retrieves a single org tag rule by org ID and rule ID.
func GetOrgProtectedTagByID(ctx context.Context, orgID, id int64) (*OrgProtectedTag, error) {
rule, exist, err := db.Get[OrgProtectedTag](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
}
// GetOrgProtectedTagByNamePattern retrieves a single org tag rule by its pattern.
func GetOrgProtectedTagByNamePattern(ctx context.Context, orgID int64, pattern string) (*OrgProtectedTag, error) {
rule, exist, err := db.Get[OrgProtectedTag](ctx, builder.Eq{"org_id": orgID, "name_pattern": pattern})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return rule, nil
}
// FindOrgProtectedTags loads all org-level tag protection rules for an organization.
func FindOrgProtectedTags(ctx context.Context, orgID int64) ([]*OrgProtectedTag, error) {
var rules []*OrgProtectedTag
err := db.GetEngine(ctx).Where("org_id = ?", orgID).Asc("created_unix").Find(&rules)
return rules, err
}
// CreateOrgProtectedTag creates a new org-level tag protection rule.
func CreateOrgProtectedTag(ctx context.Context, rule *OrgProtectedTag) error {
if _, err := db.GetEngine(ctx).Insert(rule); err != nil {
return fmt.Errorf("Insert OrgProtectedTag: %v", err)
}
return nil
}
// UpdateOrgProtectedTag updates an existing org-level tag protection rule.
func UpdateOrgProtectedTag(ctx context.Context, rule *OrgProtectedTag) error {
if _, err := db.GetEngine(ctx).ID(rule.ID).AllCols().Update(rule); err != nil {
return fmt.Errorf("Update OrgProtectedTag: %v", err)
}
return nil
}
// DeleteOrgProtectedTag deletes an org-level tag protection rule.
func DeleteOrgProtectedTag(ctx context.Context, orgID, id int64) error {
affected, err := db.GetEngine(ctx).Where("org_id = ? AND id = ?", orgID, id).Delete(new(OrgProtectedTag))
if err != nil {
return err
}
if affected == 0 {
return fmt.Errorf("org tag protection rule ID(%d) not found", id)
}
return nil
}
// IsUserAllowedToControlTagInRepo layers org-level tag rules on top of a repo's own
// protected tags: the user must be allowed by BOTH levels (most-restrictive). The
// repo decision is evaluated first (using the already-loaded repoTags); if it
// allows and the owner is an organization, the org-level rules must also allow.
func IsUserAllowedToControlTagInRepo(ctx context.Context, repoTags []*ProtectedTag, repo *repo_model.Repository, tagName string, userID int64) (bool, error) {
allowed, err := IsUserAllowedToControlTag(ctx, repoTags, tagName, userID)
if err != nil || !allowed {
return allowed, err
}
owner, err := user_model.GetUserByID(ctx, repo.OwnerID)
if err != nil {
return false, err
}
if !owner.IsOrganization() {
return true, nil
}
orgRules, err := FindOrgProtectedTags(ctx, owner.ID)
if err != nil {
return false, err
}
if len(orgRules) == 0 {
return true, nil
}
orgTags := make([]*ProtectedTag, len(orgRules))
for i, r := range orgRules {
orgTags[i] = r.ToProtectedTag()
}
return IsUserAllowedToControlTag(ctx, orgTags, tagName, userID)
}
-130
View File
@@ -1,130 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import (
"context"
"fmt"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/glob"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/builder"
)
// OrgPushPolicy is a single org-wide policy enforced in the pre-receive hook on
// every repository of the organization. Unlike the branch/tag rulesets there is at
// most one policy per org. Empty pattern / zero fields mean "no constraint". See #727.
type OrgPushPolicy struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
BranchNamePattern string `xorm:"TEXT"`
TagNamePattern string `xorm:"TEXT"`
RequireSecretBlock bool `xorm:"NOT NULL DEFAULT false"`
MaxFileSize int64 `xorm:"NOT NULL DEFAULT 0"`
BlockedFilePatterns string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(OrgPushPolicy))
}
// nameMatchesPattern reports whether name satisfies a glob pattern. An empty pattern
// imposes no constraint; an invalid pattern fails open (no constraint) so a
// misconfigured policy never blocks all pushes.
func nameMatchesPattern(pattern, name string) bool {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
return true
}
g, err := glob.Compile(pattern, '/')
if err != nil {
log.Warn("Invalid org push policy name pattern %q: %v", pattern, err)
return true
}
return g.Match(name)
}
// BranchNameAllowed reports whether a branch name satisfies the naming policy.
func (p *OrgPushPolicy) BranchNameAllowed(name string) bool {
return nameMatchesPattern(p.BranchNamePattern, name)
}
// TagNameAllowed reports whether a tag name satisfies the naming policy.
func (p *OrgPushPolicy) TagNameAllowed(name string) bool {
return nameMatchesPattern(p.TagNamePattern, name)
}
// BlockedFileGlobs parses the ';'-separated blocked file pattern list.
func (p *OrgPushPolicy) BlockedFileGlobs() []glob.Glob {
var out []glob.Glob
for _, expr := range strings.Split(p.BlockedFilePatterns, ";") {
expr = strings.TrimSpace(strings.ToLower(expr))
if expr == "" {
continue
}
if g, err := glob.Compile(expr, '.', '/'); err == nil {
out = append(out, g)
} else {
log.Warn("Invalid org push policy blocked file pattern %q: %v", expr, err)
}
}
return out
}
// GetOrgPushPolicy returns the org's push policy, or nil if none is configured.
func GetOrgPushPolicy(ctx context.Context, orgID int64) (*OrgPushPolicy, error) {
policy, exist, err := db.Get[OrgPushPolicy](ctx, builder.Eq{"org_id": orgID})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return policy, nil
}
// GetOrgPushPolicyForRepo returns the push policy of the repo's owning organization,
// or nil if the owner is not an organization or has no policy.
func GetOrgPushPolicyForRepo(ctx context.Context, repo *repo_model.Repository) (*OrgPushPolicy, error) {
owner, err := user_model.GetUserByID(ctx, repo.OwnerID)
if err != nil {
return nil, err
}
if !owner.IsOrganization() {
return nil, nil //nolint:nilnil
}
return GetOrgPushPolicy(ctx, owner.ID)
}
// UpsertOrgPushPolicy creates or updates the single push policy for an org.
func UpsertOrgPushPolicy(ctx context.Context, policy *OrgPushPolicy) error {
existing, err := GetOrgPushPolicy(ctx, policy.OrgID)
if err != nil {
return err
}
if existing == nil {
if _, err := db.GetEngine(ctx).Insert(policy); err != nil {
return fmt.Errorf("Insert OrgPushPolicy: %v", err)
}
return nil
}
policy.ID = existing.ID
if _, err := db.GetEngine(ctx).ID(existing.ID).AllCols().Update(policy); err != nil {
return fmt.Errorf("Update OrgPushPolicy: %v", err)
}
return nil
}
// DeleteOrgPushPolicy removes an org's push policy.
func DeleteOrgPushPolicy(ctx context.Context, orgID int64) error {
_, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Delete(new(OrgPushPolicy))
return err
}
-88
View File
@@ -1,88 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import (
"context"
"fmt"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/builder"
)
// OrgRepoDefaults holds an organization's default repository settings, applied to a
// repository when it is created in or transferred into the org (via a notifier).
// There is at most one row per org. See issue #727.
type OrgRepoDefaults struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
ForcePrivate bool `xorm:"NOT NULL DEFAULT false"`
ApplyPRDefaults bool `xorm:"NOT NULL DEFAULT false"`
AllowMerge bool `xorm:"NOT NULL DEFAULT true"`
AllowRebase bool `xorm:"NOT NULL DEFAULT true"`
AllowRebaseMerge bool `xorm:"NOT NULL DEFAULT true"`
AllowSquash bool `xorm:"NOT NULL DEFAULT true"`
AllowFastForwardOnly bool `xorm:"NOT NULL DEFAULT true"`
DefaultMergeStyle string `xorm:"TEXT"`
DeleteBranchAfterMerge bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(OrgRepoDefaults))
}
// GetOrgRepoDefaults returns the org's repo defaults, or nil if none are configured.
func GetOrgRepoDefaults(ctx context.Context, orgID int64) (*OrgRepoDefaults, error) {
defaults, exist, err := db.Get[OrgRepoDefaults](ctx, builder.Eq{"org_id": orgID})
if err != nil {
return nil, err
} else if !exist {
return nil, nil //nolint:nilnil
}
return defaults, nil
}
// GetOrgRepoDefaultsForRepo returns the repo-defaults of the repo's owning org, or
// nil if the owner is not an organization or has none configured.
func GetOrgRepoDefaultsForRepo(ctx context.Context, repo *repo_model.Repository) (*OrgRepoDefaults, error) {
owner, err := user_model.GetUserByID(ctx, repo.OwnerID)
if err != nil {
return nil, err
}
if !owner.IsOrganization() {
return nil, nil //nolint:nilnil
}
return GetOrgRepoDefaults(ctx, owner.ID)
}
// UpsertOrgRepoDefaults creates or updates the single repo-defaults row for an org.
func UpsertOrgRepoDefaults(ctx context.Context, defaults *OrgRepoDefaults) error {
existing, err := GetOrgRepoDefaults(ctx, defaults.OrgID)
if err != nil {
return err
}
if existing == nil {
if _, err := db.GetEngine(ctx).Insert(defaults); err != nil {
return fmt.Errorf("Insert OrgRepoDefaults: %v", err)
}
return nil
}
defaults.ID = existing.ID
if _, err := db.GetEngine(ctx).ID(existing.ID).AllCols().Update(defaults); err != nil {
return fmt.Errorf("Update OrgRepoDefaults: %v", err)
}
return nil
}
// DeleteOrgRepoDefaults removes an org's repo defaults.
func DeleteOrgRepoDefaults(ctx context.Context, orgID int64) error {
_, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Delete(new(OrgRepoDefaults))
return err
}
+7 -28
View File
@@ -85,40 +85,19 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string)
return results, nil
}
// GetFirstMatchProtectedBranchRule returns the effective protected-branch rule for a
// branch. It combines the matching repo-level rule with the matching org-level rule
// (when the repo belongs to an organization): if both match they are layered with
// mergeMostRestrictive so the org rule acts as a floor the repo cannot weaken; if
// only one matches that one is returned; if neither matches, nil.
// 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
}
repoRule := rules.GetFirstMatched(branchName)
orgRule, err := getFirstMatchOrgProtectedBranchRule(ctx, repoID, branchName)
if err != nil {
return nil, err
if matched := rules.GetFirstMatched(branchName); matched != nil {
return matched, nil
}
switch {
case repoRule == nil && orgRule == nil:
return nil, nil
case orgRule == nil:
return repoRule, nil
case repoRule == nil:
return orgRule, nil
default:
return mergeMostRestrictive(repoRule, orgRule), nil
}
}
// getFirstMatchOrgProtectedBranchRule returns the matching org-level rule for a
// branch expressed as a repo-scoped ProtectedBranch (RepoID set so downstream
// permission checks work), or nil if the repo's owner is not an organization or no
// org rule matches.
func getFirstMatchOrgProtectedBranchRule(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) {
// Fall back to org-level rules
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return nil, err
@@ -140,7 +119,7 @@ func getFirstMatchOrgProtectedBranchRule(ctx context.Context, repoID int64, bran
return nil, nil
}
// Convert org rule to a ProtectedBranch with RepoID set so callers work correctly.
// Convert org rule to a ProtectedBranch with RepoID set so callers work correctly
pb := orgRule.ToProtectedBranch()
pb.RepoID = repoID
return pb, nil
-178
View File
@@ -1,178 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package git
import "strings"
// mergeMostRestrictive combines a repo-level and an org-level protected-branch rule
// that both match the same branch into a single effective rule, always applying the
// STRICTER constraint from each side (fail-closed). This makes an org-level rule a
// mandatory floor that a repo rule can only tighten, never weaken. See issue #727.
//
// Combination directions:
// - "Can*" / allow booleans -> AND (an action is allowed only if both allow it)
// - "Enable*/Block*/Require*" -> OR (a gate is on if either side turns it on)
// - RequiredApprovals -> max
// - required-set lists -> union (status contexts, protected files)
// - allow-set lists -> intersection (whitelists, unprotected files)
//
// Identity (ID, RepoID, RuleName, Priority) is taken from the repo rule so that
// downstream permission checks (which LoadRepo via RepoID) keep working.
func mergeMostRestrictive(repoRule, orgRule *ProtectedBranch) *ProtectedBranch {
eff := *repoRule
// Direct push.
eff.CanPush = repoRule.CanPush && orgRule.CanPush
eff.EnableWhitelist, eff.WhitelistUserIDs = mergeAllowlist(repoRule.EnableWhitelist, repoRule.WhitelistUserIDs, orgRule.EnableWhitelist, orgRule.WhitelistUserIDs)
_, eff.WhitelistTeamIDs = mergeAllowlist(repoRule.EnableWhitelist, repoRule.WhitelistTeamIDs, orgRule.EnableWhitelist, orgRule.WhitelistTeamIDs)
eff.WhitelistDeployKeys = repoRule.WhitelistDeployKeys && orgRule.WhitelistDeployKeys
eff.WhitelistActionsUser = repoRule.WhitelistActionsUser && orgRule.WhitelistActionsUser
// Force push.
eff.CanForcePush = repoRule.CanForcePush && orgRule.CanForcePush
eff.EnableForcePushAllowlist, eff.ForcePushAllowlistUserIDs = mergeAllowlist(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistUserIDs, orgRule.EnableForcePushAllowlist, orgRule.ForcePushAllowlistUserIDs)
_, eff.ForcePushAllowlistTeamIDs = mergeAllowlist(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistTeamIDs, orgRule.EnableForcePushAllowlist, orgRule.ForcePushAllowlistTeamIDs)
eff.ForcePushAllowlistDeployKeys = repoRule.ForcePushAllowlistDeployKeys && orgRule.ForcePushAllowlistDeployKeys
eff.ForcePushAllowlistActionsUser = repoRule.ForcePushAllowlistActionsUser && orgRule.ForcePushAllowlistActionsUser
// Delete.
eff.CanDelete = repoRule.CanDelete && orgRule.CanDelete
eff.EnableDeleteAllowlist, eff.DeleteAllowlistUserIDs = mergeAllowlist(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistUserIDs, orgRule.EnableDeleteAllowlist, orgRule.DeleteAllowlistUserIDs)
_, eff.DeleteAllowlistTeamIDs = mergeAllowlist(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistTeamIDs, orgRule.EnableDeleteAllowlist, orgRule.DeleteAllowlistTeamIDs)
eff.DeleteAllowlistDeployKeys = repoRule.DeleteAllowlistDeployKeys && orgRule.DeleteAllowlistDeployKeys
eff.DeleteAllowlistActionsUser = repoRule.DeleteAllowlistActionsUser && orgRule.DeleteAllowlistActionsUser
// Merge whitelist.
eff.EnableMergeWhitelist, eff.MergeWhitelistUserIDs = mergeAllowlist(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistUserIDs, orgRule.EnableMergeWhitelist, orgRule.MergeWhitelistUserIDs)
_, eff.MergeWhitelistTeamIDs = mergeAllowlist(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistTeamIDs, orgRule.EnableMergeWhitelist, orgRule.MergeWhitelistTeamIDs)
eff.MergeWhitelistActionsUser = repoRule.MergeWhitelistActionsUser && orgRule.MergeWhitelistActionsUser
// Status checks.
eff.EnableStatusCheck = repoRule.EnableStatusCheck || orgRule.EnableStatusCheck
eff.StatusCheckContexts = unionStrings(repoRule.StatusCheckContexts, orgRule.StatusCheckContexts)
// Approvals and reviews.
eff.RequiredApprovals = maxInt64(repoRule.RequiredApprovals, orgRule.RequiredApprovals)
eff.EnableApprovalsWhitelist, eff.ApprovalsWhitelistUserIDs = mergeAllowlist(repoRule.EnableApprovalsWhitelist, repoRule.ApprovalsWhitelistUserIDs, orgRule.EnableApprovalsWhitelist, orgRule.ApprovalsWhitelistUserIDs)
_, eff.ApprovalsWhitelistTeamIDs = mergeAllowlist(repoRule.EnableApprovalsWhitelist, repoRule.ApprovalsWhitelistTeamIDs, orgRule.EnableApprovalsWhitelist, orgRule.ApprovalsWhitelistTeamIDs)
eff.BlockOnRejectedReviews = repoRule.BlockOnRejectedReviews || orgRule.BlockOnRejectedReviews
eff.BlockOnOfficialReviewRequests = repoRule.BlockOnOfficialReviewRequests || orgRule.BlockOnOfficialReviewRequests
eff.BlockOnOutdatedBranch = repoRule.BlockOnOutdatedBranch || orgRule.BlockOnOutdatedBranch
eff.DismissStaleApprovals = repoRule.DismissStaleApprovals || orgRule.DismissStaleApprovals
eff.IgnoreStaleApprovals = repoRule.IgnoreStaleApprovals || orgRule.IgnoreStaleApprovals
// Commits, files, admin override.
eff.RequireSignedCommits = repoRule.RequireSignedCommits || orgRule.RequireSignedCommits
eff.ProtectedFilePatterns = unionPatterns(repoRule.ProtectedFilePatterns, orgRule.ProtectedFilePatterns)
eff.UnprotectedFilePatterns = intersectPatterns(repoRule.UnprotectedFilePatterns, orgRule.UnprotectedFilePatterns)
eff.BlockAdminMergeOverride = repoRule.BlockAdminMergeOverride || orgRule.BlockAdminMergeOverride
return &eff
}
// mergeAllowlist combines two allow-lists under most-restrictive semantics. An
// allow-list only narrows access when its Enable flag is set; a disabled list means
// "everyone", so it imposes no constraint. Therefore: if both are enabled the result
// is the intersection (a principal must be allowed by both); if only one is enabled
// its list is used as-is; if neither is enabled the list is irrelevant.
func mergeAllowlist(aEnabled bool, aIDs []int64, bEnabled bool, bIDs []int64) (bool, []int64) {
switch {
case aEnabled && bEnabled:
return true, intersectInt64(aIDs, bIDs)
case aEnabled:
return true, aIDs
case bEnabled:
return true, bIDs
default:
return false, nil
}
}
func intersectInt64(a, b []int64) []int64 {
if len(a) == 0 || len(b) == 0 {
return nil
}
set := make(map[int64]struct{}, len(a))
for _, x := range a {
set[x] = struct{}{}
}
var out []int64
for _, x := range b {
if _, ok := set[x]; ok {
out = append(out, x)
}
}
return out
}
func unionStrings(a, b []string) []string {
if len(a) == 0 {
return b
}
if len(b) == 0 {
return a
}
seen := make(map[string]struct{}, len(a)+len(b))
out := make([]string, 0, len(a)+len(b))
for _, s := range a {
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
out = append(out, s)
}
}
for _, s := range b {
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
out = append(out, s)
}
}
return out
}
// unionPatterns unions two ';'-separated file-pattern lists (more patterns protected
// = more restrictive).
func unionPatterns(a, b string) string {
return strings.Join(unionStrings(splitPatterns(a), splitPatterns(b)), ";")
}
// intersectPatterns intersects two ';'-separated file-pattern lists. Unprotected
// patterns are carve-outs that REDUCE protection, so the restrictive combination
// keeps only the exemptions present in both.
func intersectPatterns(a, b string) string {
as, bs := splitPatterns(a), splitPatterns(b)
set := make(map[string]struct{}, len(as))
for _, s := range as {
set[s] = struct{}{}
}
seen := make(map[string]struct{}, len(bs))
var out []string
for _, s := range bs {
if _, ok := set[s]; !ok {
continue
}
if _, dup := seen[s]; dup {
continue
}
seen[s] = struct{}{}
out = append(out, s)
}
return strings.Join(out, ";")
}
func splitPatterns(s string) []string {
var out []string
for _, p := range strings.Split(s, ";") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
func maxInt64(a, b int64) int64 {
if a > b {
return a
}
return b
}
+1 -5
View File
@@ -439,11 +439,7 @@ func prepareMigrationTasks() []*migration {
newMigration(359, "Add deploy fields to repo manifest", v1_27.AddDeployFieldsToRepoManifest),
newMigration(360, "Add delete allowlist to protected branch", v1_27.AddDeleteAllowlistToProtectedBranch),
newMigration(361, "Add cascade merge rule table", v1_27.AddCascadeMergeRuleTable),
newMigration(362, "Add delete allowlist to org protected branch", v1_27.AddDeleteAllowlistToOrgProtectedBranch),
newMigration(363, "Add org protected tag table", v1_27.AddOrgProtectedTagTable),
newMigration(364, "Add org push policy table", v1_27.AddOrgPushPolicyTable),
newMigration(365, "Add org repo defaults table", v1_27.AddOrgRepoDefaultsTable),
newMigration(366, "Add org email domain policy table", v1_27.AddOrgEmailDomainPolicyTable),
newMigration(362, "Add user allowlists and actions-user toggles to org protected branch", v1_27.AddUserAllowlistsToOrgProtectedBranch),
}
return preparedMigrations
}
+14 -8
View File
@@ -3,16 +3,22 @@
package v1_27
import "xorm.io/xorm"
import (
"xorm.io/xorm"
)
// AddDeleteAllowlistToOrgProtectedBranch adds branch-deletion protection columns to
// org-level branch protection rules, mirroring the per-repo delete allowlist so an
// org rule can also protect branches from deletion. See issue #727.
func AddDeleteAllowlistToOrgProtectedBranch(x *xorm.Engine) error {
// AddUserAllowlistsToOrgProtectedBranch adds per-user allowlists (by user ID) and
// "allow the actions bot" toggles to org-level branch protection rules, alongside
// the existing team-only allowlists. Additive columns only.
func AddUserAllowlistsToOrgProtectedBranch(x *xorm.Engine) error {
type OrgProtectedBranch struct {
CanDelete bool `xorm:"NOT NULL DEFAULT false"`
EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"`
DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"`
ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"`
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
}
return x.Sync(new(OrgProtectedBranch))
}
-25
View File
@@ -1,25 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
// AddOrgProtectedTagTable creates the org-level tag protection table. Org tag rules
// cascade to all repositories in the organization and layer on top of each repo's
// own protected tags. See issue #727.
func AddOrgProtectedTagTable(x *xorm.Engine) error {
type OrgProtectedTag struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE(s) index"`
NamePattern string `xorm:"UNIQUE(s)"`
AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(OrgProtectedTag))
}
-27
View File
@@ -1,27 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
// AddOrgPushPolicyTable creates the org-level push policy table (one row per org),
// enforced in the pre-receive hook across all repositories of the org. See #727.
func AddOrgPushPolicyTable(x *xorm.Engine) error {
type OrgPushPolicy struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
BranchNamePattern string `xorm:"TEXT"`
TagNamePattern string `xorm:"TEXT"`
RequireSecretBlock bool `xorm:"NOT NULL DEFAULT false"`
MaxFileSize int64 `xorm:"NOT NULL DEFAULT 0"`
BlockedFilePatterns string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(OrgPushPolicy))
}
-31
View File
@@ -1,31 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
// AddOrgRepoDefaultsTable creates the org repository-defaults table (one row per
// org), applied to repositories created in or transferred into the org. See #727.
func AddOrgRepoDefaultsTable(x *xorm.Engine) error {
type OrgRepoDefaults struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
ForcePrivate bool `xorm:"NOT NULL DEFAULT false"`
ApplyPRDefaults bool `xorm:"NOT NULL DEFAULT false"`
AllowMerge bool `xorm:"NOT NULL DEFAULT true"`
AllowRebase bool `xorm:"NOT NULL DEFAULT true"`
AllowRebaseMerge bool `xorm:"NOT NULL DEFAULT true"`
AllowSquash bool `xorm:"NOT NULL DEFAULT true"`
AllowFastForwardOnly bool `xorm:"NOT NULL DEFAULT true"`
DefaultMergeStyle string `xorm:"TEXT"`
DeleteBranchAfterMerge bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(OrgRepoDefaults))
}
-23
View File
@@ -1,23 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
// AddOrgEmailDomainPolicyTable creates the org email-domain policy table (one row
// per org) restricting the email domains of members added to the org. See #727.
func AddOrgEmailDomainPolicyTable(x *xorm.Engine) error {
type OrgEmailDomainPolicy struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"UNIQUE NOT NULL"`
AllowedDomains string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(OrgEmailDomainPolicy))
}
+21 -9
View File
@@ -14,19 +14,23 @@ type OrgBranchProtection struct {
EnablePush bool `json:"enable_push"`
EnablePushWhitelist bool `json:"enable_push_whitelist"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"`
EnableForcePush bool `json:"enable_force_push"`
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
EnableDelete bool `json:"enable_delete"`
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"`
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"`
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"`
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
@@ -49,19 +53,23 @@ type CreateOrgBranchProtectionOption struct {
EnablePush bool `json:"enable_push"`
EnablePushWhitelist bool `json:"enable_push_whitelist"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"`
EnableForcePush bool `json:"enable_force_push"`
EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
EnableDelete bool `json:"enable_delete"`
EnableDeleteAllowlist bool `json:"enable_delete_allowlist"`
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"`
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"`
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"`
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"`
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"`
@@ -79,19 +87,23 @@ type EditOrgBranchProtectionOption struct {
EnablePush *bool `json:"enable_push"`
EnablePushWhitelist *bool `json:"enable_push_whitelist"`
PushWhitelistTeams []string `json:"push_whitelist_teams"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames"`
PushWhitelistActionsUser *bool `json:"push_whitelist_actions_user"`
EnableForcePush *bool `json:"enable_force_push"`
EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"`
ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"`
EnableDelete *bool `json:"enable_delete"`
EnableDeleteAllowlist *bool `json:"enable_delete_allowlist"`
DeleteAllowlistTeams []string `json:"delete_allowlist_teams"`
ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"`
ForcePushAllowlistActionsUser *bool `json:"force_push_allowlist_actions_user"`
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"`
MergeWhitelistActionsUser *bool `json:"merge_whitelist_actions_user"`
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"`
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
BlockOnRejectedReviews *bool `json:"block_on_rejected_reviews"`
BlockOnOfficialReviewRequests *bool `json:"block_on_official_review_requests"`
BlockOnOutdatedBranch *bool `json:"block_on_outdated_branch"`
-15
View File
@@ -1,15 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
// OrgEmailDomainPolicy represents an organization's email domain policy
type OrgEmailDomainPolicy struct {
OrgID int64 `json:"org_id"`
AllowedDomains string `json:"allowed_domains"`
}
// EditOrgEmailDomainPolicyOption options for editing an org's email domain policy
type EditOrgEmailDomainPolicyOption struct {
AllowedDomains *string `json:"allowed_domains"`
}
-30
View File
@@ -1,30 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
import "time"
// OrgPushPolicy represents an organization's push policy (one per org)
type OrgPushPolicy struct {
OrgID int64 `json:"org_id"`
BranchNamePattern string `json:"branch_name_pattern"`
TagNamePattern string `json:"tag_name_pattern"`
RequireSecretBlock bool `json:"require_secret_block"`
MaxFileSize int64 `json:"max_file_size"`
BlockedFilePatterns string `json:"blocked_file_patterns"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
}
// EditOrgPushPolicyOption options for editing an organization's push policy. Only
// fields that are set will be changed.
type EditOrgPushPolicyOption struct {
BranchNamePattern *string `json:"branch_name_pattern"`
TagNamePattern *string `json:"tag_name_pattern"`
RequireSecretBlock *bool `json:"require_secret_block"`
MaxFileSize *int64 `json:"max_file_size"`
BlockedFilePatterns *string `json:"blocked_file_patterns"`
}
-32
View File
@@ -1,32 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
// OrgRepoDefaults represents an organization's default repository settings
type OrgRepoDefaults struct {
OrgID int64 `json:"org_id"`
ForcePrivate bool `json:"force_private"`
ApplyPRDefaults bool `json:"apply_pr_defaults"`
AllowMerge bool `json:"allow_merge"`
AllowRebase bool `json:"allow_rebase"`
AllowRebaseMerge bool `json:"allow_rebase_merge"`
AllowSquash bool `json:"allow_squash"`
AllowFastForwardOnly bool `json:"allow_fast_forward_only"`
DefaultMergeStyle string `json:"default_merge_style"`
DeleteBranchAfterMerge bool `json:"delete_branch_after_merge"`
}
// EditOrgRepoDefaultsOption options for editing an org's repo defaults. Only fields
// that are set will be changed.
type EditOrgRepoDefaultsOption struct {
ForcePrivate *bool `json:"force_private"`
ApplyPRDefaults *bool `json:"apply_pr_defaults"`
AllowMerge *bool `json:"allow_merge"`
AllowRebase *bool `json:"allow_rebase"`
AllowRebaseMerge *bool `json:"allow_rebase_merge"`
AllowSquash *bool `json:"allow_squash"`
AllowFastForwardOnly *bool `json:"allow_fast_forward_only"`
DefaultMergeStyle *string `json:"default_merge_style"`
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge"`
}
-30
View File
@@ -1,30 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
import "time"
// OrgTagProtection represents an org-level tag protection rule
type OrgTagProtection struct {
ID int64 `json:"id"`
OrgID int64 `json:"org_id"`
NamePattern string `json:"name_pattern"`
WhitelistTeams []string `json:"whitelist_teams"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
}
// CreateOrgTagProtectionOption options for creating an org-level tag protection
type CreateOrgTagProtectionOption struct {
NamePattern string `json:"name_pattern" binding:"Required"`
WhitelistTeams []string `json:"whitelist_teams"`
}
// EditOrgTagProtectionOption options for editing an org-level tag protection
type EditOrgTagProtectionOption struct {
NamePattern *string `json:"name_pattern"`
WhitelistTeams []string `json:"whitelist_teams"`
}
-50
View File
@@ -2411,56 +2411,6 @@
"repo.settings.protected_branch": "Branch Protection",
"repo.settings.protected_branch.save_rule": "Save Rule",
"repo.settings.protected_branch.delete_rule": "Delete Rule",
"repo.settings.org_protected_branch": "Organization Branch Protection",
"repo.settings.org_protected_branch_desc": "These rules are defined by the organization and are enforced on top of this repository's own rules — the stricter of the two applies. They cannot be edited here.",
"repo.settings.org_protected_branch.inherited": "Organization",
"repo.settings.org_protected_branch.read_only": "Read-only",
"repo.settings.org_protected_branch.approvals": "Required approvals",
"repo.settings.org_protected_branch.signed": "Signed commits",
"repo.settings.org_protected_branch.status_check": "Required status checks",
"repo.settings.org_protected_branch.direct_push": "Direct push",
"repo.settings.org_protected_branch.force_push": "Force push",
"repo.settings.org_protected_branch.deletion": "Branch deletion",
"repo.settings.org_protected_branch.merge": "Merge restricted to",
"repo.settings.org_protected_branch.protected_files": "Protected files",
"repo.settings.org_protected_branch.also": "Also enforces",
"repo.settings.org_protected_branch.blocked": "Blocked",
"repo.settings.org_protected_branch.allowed": "Allowed",
"repo.settings.org_protected_branch.restricted": "Restricted to specific teams",
"repo.settings.org_protected_branch.write_access": "Anyone with write access",
"repo.settings.org_protected_branch.teams": "Teams: %s",
"repo.settings.org_protected_branch.any": "Any configured checks",
"repo.settings.org_protected_branch.block_outdated": "Block on outdated branch",
"repo.settings.org_protected_branch.block_rejected": "Block on rejected reviews",
"repo.settings.org_protected_branch.block_admin": "Block admin merge override",
"repo.settings.org_protected_tag": "Organization Tag Protection",
"repo.settings.org_protected_tag_desc": "These tag protection rules are defined by the organization and are enforced on top of this repository's own rules. They cannot be edited here.",
"repo.settings.org_protected_tag.read_only": "Read-only",
"repo.settings.org_push_policy": "Organization Push Policy",
"repo.settings.org_push_policy_desc": "This push policy is defined by the organization and enforced on every push to this repository. It cannot be edited here.",
"repo.settings.org_push_policy.inherited": "Organization",
"repo.settings.org_push_policy.read_only": "Read-only",
"repo.settings.org_push_policy.branch_pattern": "Allowed branch names",
"repo.settings.org_push_policy.tag_pattern": "Allowed tag names",
"repo.settings.org_push_policy.require_secret_block": "Block pushes containing secrets",
"repo.settings.org_push_policy.max_file_size": "Maximum file size",
"repo.settings.org_push_policy.blocked_files": "Blocked file paths",
"repo.settings.org_push_policy.enabled": "Enabled",
"repo.settings.org_repo_defaults": "Organization Repository Defaults",
"repo.settings.org_repo_defaults_desc": "These defaults are defined by the organization and were applied to this repository when it was created or transferred into the organization. They are shown for reference and cannot be edited here.",
"repo.settings.org_repo_defaults.inherited": "Organization",
"repo.settings.org_repo_defaults.read_only": "Read-only",
"repo.settings.org_repo_defaults.force_private": "Force private repositories",
"repo.settings.org_repo_defaults.merge_styles": "Allowed merge styles",
"repo.settings.org_repo_defaults.default_merge_style": "Default merge style",
"repo.settings.org_repo_defaults.delete_branch_after_merge": "Delete branch after merge",
"repo.settings.org_repo_defaults.style_merge": "Merge commits",
"repo.settings.org_repo_defaults.style_rebase": "Rebase",
"repo.settings.org_repo_defaults.style_rebase_merge": "Rebase with merge commit",
"repo.settings.org_repo_defaults.style_squash": "Squash",
"repo.settings.org_repo_defaults.style_fast_forward_only": "Fast-forward only",
"repo.settings.org_repo_defaults.enabled": "Enabled",
"repo.settings.org_repo_defaults.disabled": "Disabled",
"repo.settings.protected_branch_can_push": "Allow push?",
"repo.settings.protected_branch_can_push_yes": "You can push",
"repo.settings.protected_branch_can_push_no": "You cannot push",
-28
View File
@@ -1823,34 +1823,6 @@ func Routes() *web.Router {
})
}, reqToken(), reqOrgOwnership())
m.Group("/tag_protections", func() {
m.Combo("").Get(org.ListOrgTagProtections).
Post(bind(api.CreateOrgTagProtectionOption{}), org.CreateOrgTagProtection)
m.Group("/{id}", func() {
m.Get("", org.GetOrgTagProtection)
m.Patch("", bind(api.EditOrgTagProtectionOption{}), org.EditOrgTagProtection)
m.Delete("", org.DeleteOrgTagProtection)
})
}, reqToken(), reqOrgOwnership())
m.Group("/push_policy", func() {
m.Combo("").Get(org.GetOrgPushPolicy).
Patch(bind(api.EditOrgPushPolicyOption{}), org.EditOrgPushPolicy).
Delete(org.DeleteOrgPushPolicy)
}, reqToken(), reqOrgOwnership())
m.Group("/repo_defaults", func() {
m.Combo("").Get(org.GetOrgRepoDefaults).
Patch(bind(api.EditOrgRepoDefaultsOption{}), org.EditOrgRepoDefaults).
Delete(org.DeleteOrgRepoDefaults)
}, reqToken(), reqOrgOwnership())
m.Group("/email_domain_policy", func() {
m.Combo("").Get(org.GetOrgEmailDomainPolicy).
Patch(bind(api.EditOrgEmailDomainPolicyOption{}), org.EditOrgEmailDomainPolicy).
Delete(org.DeleteOrgEmailDomainPolicy)
}, reqToken(), reqOrgOwnership())
m.Group("/blocks", func() {
m.Get("", org.ListBlocks)
m.Group("/{username}", func() {
+118 -18
View File
@@ -8,6 +8,7 @@ import (
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
@@ -36,6 +37,18 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec
return names
}
resolveUserNames := func(ids []int64) []string {
names := make([]string, 0, len(ids))
for _, id := range ids {
u, err := user_model.GetUserByID(ctx, id)
if err != nil {
continue
}
names = append(names, u.Name)
}
return names
}
return &api.OrgBranchProtection{
ID: rule.ID,
OrgID: rule.OrgID,
@@ -44,19 +57,23 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec
EnablePush: rule.CanPush,
EnablePushWhitelist: rule.EnableWhitelist,
PushWhitelistTeams: resolveTeamNames(rule.WhitelistTeamIDs),
PushWhitelistUsernames: resolveUserNames(rule.WhitelistUserIDs),
PushWhitelistActionsUser: rule.WhitelistActionsUser,
EnableForcePush: rule.CanForcePush,
EnableForcePushAllowlist: rule.EnableForcePushAllowlist,
ForcePushAllowlistTeams: resolveTeamNames(rule.ForcePushAllowlistTeamIDs),
EnableDelete: rule.CanDelete,
EnableDeleteAllowlist: rule.EnableDeleteAllowlist,
DeleteAllowlistTeams: resolveTeamNames(rule.DeleteAllowlistTeamIDs),
ForcePushAllowlistUsernames: resolveUserNames(rule.ForcePushAllowlistUserIDs),
ForcePushAllowlistActionsUser: rule.ForcePushAllowlistActionsUser,
EnableMergeWhitelist: rule.EnableMergeWhitelist,
MergeWhitelistTeams: resolveTeamNames(rule.MergeWhitelistTeamIDs),
MergeWhitelistUsernames: resolveUserNames(rule.MergeWhitelistUserIDs),
MergeWhitelistActionsUser: rule.MergeWhitelistActionsUser,
EnableStatusCheck: rule.EnableStatusCheck,
StatusCheckContexts: rule.StatusCheckContexts,
RequiredApprovals: rule.RequiredApprovals,
EnableApprovalsWhitelist: rule.EnableApprovalsWhitelist,
ApprovalsWhitelistTeams: resolveTeamNames(rule.ApprovalsWhitelistTeamIDs),
ApprovalsWhitelistUsernames: resolveUserNames(rule.ApprovalsWhitelistUserIDs),
BlockOnRejectedReviews: rule.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: rule.BlockOnOfficialReviewRequests,
BlockOnOutdatedBranch: rule.BlockOnOutdatedBranch,
@@ -88,6 +105,45 @@ func resolveTeamIDs(ctx *context.APIContext, orgID int64, names []string) ([]int
return ids, true
}
// resolveUserIDs converts a list of usernames or emails to user IDs, returning 422
// on any entry that matches no user. Each entry is first looked up by username and,
// if that user does not exist, by email address. Empty entries are ignored and the
// resulting IDs are deduplicated.
func resolveUserIDs(ctx *context.APIContext, names []string) ([]int64, bool) {
if len(names) == 0 {
return nil, true
}
seen := make(map[int64]struct{}, len(names))
ids := make([]int64, 0, len(names))
for _, name := range names {
if name == "" {
continue
}
u, err := user_model.GetUserByName(ctx, name)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
ctx.APIErrorInternal(err)
return nil, false
}
u, err = user_model.GetUserByEmail(ctx, name)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, "user does not exist: "+name)
} else {
ctx.APIErrorInternal(err)
}
return nil, false
}
}
if _, ok := seen[u.ID]; ok {
continue
}
seen[u.ID] = struct{}{}
ids = append(ids, u.ID)
}
return ids, true
}
// ListOrgBranchProtections list org-level branch protection rules
func ListOrgBranchProtections(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/branch_protections organization orgListBranchProtections
@@ -214,31 +270,51 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
if !ok {
return
}
deleteTeams, ok := resolveTeamIDs(ctx, orgID, form.DeleteAllowlistTeams)
pushUsers, ok := resolveUserIDs(ctx, form.PushWhitelistUsernames)
if !ok {
return
}
forcePushUsers, ok := resolveUserIDs(ctx, form.ForcePushAllowlistUsernames)
if !ok {
return
}
mergeUsers, ok := resolveUserIDs(ctx, form.MergeWhitelistUsernames)
if !ok {
return
}
approvalsUsers, ok := resolveUserIDs(ctx, form.ApprovalsWhitelistUsernames)
if !ok {
return
}
enablePushWhitelist := form.EnablePush && form.EnablePushWhitelist
enableForcePushAllowlist := form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist
rule := &git_model.OrgProtectedBranch{
OrgID: orgID,
RuleName: form.RuleName,
Priority: form.Priority,
CanPush: form.EnablePush,
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
EnableWhitelist: enablePushWhitelist,
WhitelistTeamIDs: pushTeams,
WhitelistUserIDs: pushUsers,
WhitelistActionsUser: enablePushWhitelist && form.PushWhitelistActionsUser,
CanForcePush: form.EnablePush && form.EnableForcePush,
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
EnableForcePushAllowlist: enableForcePushAllowlist,
ForcePushAllowlistTeamIDs: forcePushTeams,
CanDelete: form.EnableDelete,
EnableDeleteAllowlist: form.EnableDelete && form.EnableDeleteAllowlist,
DeleteAllowlistTeamIDs: deleteTeams,
ForcePushAllowlistUserIDs: forcePushUsers,
ForcePushAllowlistActionsUser: enableForcePushAllowlist && form.ForcePushAllowlistActionsUser,
EnableMergeWhitelist: form.EnableMergeWhitelist,
MergeWhitelistTeamIDs: mergeTeams,
MergeWhitelistUserIDs: mergeUsers,
MergeWhitelistActionsUser: form.EnableMergeWhitelist && form.MergeWhitelistActionsUser,
EnableStatusCheck: form.EnableStatusCheck,
StatusCheckContexts: form.StatusCheckContexts,
RequiredApprovals: form.RequiredApprovals,
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
ApprovalsWhitelistTeamIDs: approvalsTeams,
ApprovalsWhitelistUserIDs: approvalsUsers,
BlockOnRejectedReviews: form.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests,
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
@@ -320,6 +396,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
}
rule.WhitelistTeamIDs = ids
}
if form.PushWhitelistUsernames != nil {
ids, ok := resolveUserIDs(ctx, form.PushWhitelistUsernames)
if !ok {
return
}
rule.WhitelistUserIDs = ids
}
if form.PushWhitelistActionsUser != nil {
rule.WhitelistActionsUser = *form.PushWhitelistActionsUser
}
if form.EnableForcePush != nil {
rule.CanForcePush = *form.EnableForcePush
}
@@ -333,18 +419,15 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
}
rule.ForcePushAllowlistTeamIDs = ids
}
if form.EnableDelete != nil {
rule.CanDelete = *form.EnableDelete
}
if form.EnableDeleteAllowlist != nil {
rule.EnableDeleteAllowlist = *form.EnableDeleteAllowlist
}
if form.DeleteAllowlistTeams != nil {
ids, ok := resolveTeamIDs(ctx, orgID, form.DeleteAllowlistTeams)
if form.ForcePushAllowlistUsernames != nil {
ids, ok := resolveUserIDs(ctx, form.ForcePushAllowlistUsernames)
if !ok {
return
}
rule.DeleteAllowlistTeamIDs = ids
rule.ForcePushAllowlistUserIDs = ids
}
if form.ForcePushAllowlistActionsUser != nil {
rule.ForcePushAllowlistActionsUser = *form.ForcePushAllowlistActionsUser
}
if form.EnableMergeWhitelist != nil {
rule.EnableMergeWhitelist = *form.EnableMergeWhitelist
@@ -356,6 +439,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
}
rule.MergeWhitelistTeamIDs = ids
}
if form.MergeWhitelistUsernames != nil {
ids, ok := resolveUserIDs(ctx, form.MergeWhitelistUsernames)
if !ok {
return
}
rule.MergeWhitelistUserIDs = ids
}
if form.MergeWhitelistActionsUser != nil {
rule.MergeWhitelistActionsUser = *form.MergeWhitelistActionsUser
}
if form.EnableStatusCheck != nil {
rule.EnableStatusCheck = *form.EnableStatusCheck
}
@@ -375,6 +468,13 @@ func EditOrgBranchProtection(ctx *context.APIContext) {
}
rule.ApprovalsWhitelistTeamIDs = ids
}
if form.ApprovalsWhitelistUsernames != nil {
ids, ok := resolveUserIDs(ctx, form.ApprovalsWhitelistUsernames)
if !ok {
return
}
rule.ApprovalsWhitelistUserIDs = ids
}
if form.BlockOnRejectedReviews != nil {
rule.BlockOnRejectedReviews = *form.BlockOnRejectedReviews
}
-67
View File
@@ -1,67 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
func toAPIOrgEmailDomainPolicy(policy *git_model.OrgEmailDomainPolicy, orgID int64) *api.OrgEmailDomainPolicy {
if policy == nil {
return &api.OrgEmailDomainPolicy{OrgID: orgID}
}
return &api.OrgEmailDomainPolicy{
OrgID: policy.OrgID,
AllowedDomains: policy.AllowedDomains,
}
}
// GetOrgEmailDomainPolicy get the organization's email domain policy
func GetOrgEmailDomainPolicy(ctx *context.APIContext) {
orgID := ctx.Org.Organization.ID
policy, err := git_model.GetOrgEmailDomainPolicy(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgEmailDomainPolicy(policy, orgID))
}
// EditOrgEmailDomainPolicy create or update the organization's email domain policy
func EditOrgEmailDomainPolicy(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditOrgEmailDomainPolicyOption)
orgID := ctx.Org.Organization.ID
policy, err := git_model.GetOrgEmailDomainPolicy(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if policy == nil {
policy = &git_model.OrgEmailDomainPolicy{OrgID: orgID}
}
if form.AllowedDomains != nil {
policy.AllowedDomains = *form.AllowedDomains
}
if err := git_model.UpsertOrgEmailDomainPolicy(ctx, policy); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgEmailDomainPolicy(policy, orgID))
}
// DeleteOrgEmailDomainPolicy remove the organization's email domain policy
func DeleteOrgEmailDomainPolicy(ctx *context.APIContext) {
if err := git_model.DeleteOrgEmailDomainPolicy(ctx, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
-88
View File
@@ -1,88 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// toAPIOrgPushPolicy converts the model to its API representation. A nil policy is
// rendered as an all-empty policy so clients always get a consistent shape.
func toAPIOrgPushPolicy(policy *git_model.OrgPushPolicy, orgID int64) *api.OrgPushPolicy {
if policy == nil {
return &api.OrgPushPolicy{OrgID: orgID}
}
return &api.OrgPushPolicy{
OrgID: policy.OrgID,
BranchNamePattern: policy.BranchNamePattern,
TagNamePattern: policy.TagNamePattern,
RequireSecretBlock: policy.RequireSecretBlock,
MaxFileSize: policy.MaxFileSize,
BlockedFilePatterns: policy.BlockedFilePatterns,
Created: policy.CreatedUnix.AsTime(),
Updated: policy.UpdatedUnix.AsTime(),
}
}
// GetOrgPushPolicy get the organization's push policy
func GetOrgPushPolicy(ctx *context.APIContext) {
orgID := ctx.Org.Organization.ID
policy, err := git_model.GetOrgPushPolicy(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgPushPolicy(policy, orgID))
}
// EditOrgPushPolicy create or update the organization's push policy
func EditOrgPushPolicy(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditOrgPushPolicyOption)
orgID := ctx.Org.Organization.ID
policy, err := git_model.GetOrgPushPolicy(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if policy == nil {
policy = &git_model.OrgPushPolicy{OrgID: orgID}
}
if form.BranchNamePattern != nil {
policy.BranchNamePattern = *form.BranchNamePattern
}
if form.TagNamePattern != nil {
policy.TagNamePattern = *form.TagNamePattern
}
if form.RequireSecretBlock != nil {
policy.RequireSecretBlock = *form.RequireSecretBlock
}
if form.MaxFileSize != nil {
policy.MaxFileSize = *form.MaxFileSize
}
if form.BlockedFilePatterns != nil {
policy.BlockedFilePatterns = *form.BlockedFilePatterns
}
if err := git_model.UpsertOrgPushPolicy(ctx, policy); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgPushPolicy(policy, orgID))
}
// DeleteOrgPushPolicy remove the organization's push policy
func DeleteOrgPushPolicy(ctx *context.APIContext) {
if err := git_model.DeleteOrgPushPolicy(ctx, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
-117
View File
@@ -1,117 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// toAPIOrgRepoDefaults converts the model to its API representation. A nil value is
// rendered as the effective defaults (all merge styles allowed) so clients always
// get a consistent shape.
func toAPIOrgRepoDefaults(d *git_model.OrgRepoDefaults, orgID int64) *api.OrgRepoDefaults {
if d == nil {
return &api.OrgRepoDefaults{
OrgID: orgID,
AllowMerge: true,
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
AllowFastForwardOnly: true,
}
}
return &api.OrgRepoDefaults{
OrgID: d.OrgID,
ForcePrivate: d.ForcePrivate,
ApplyPRDefaults: d.ApplyPRDefaults,
AllowMerge: d.AllowMerge,
AllowRebase: d.AllowRebase,
AllowRebaseMerge: d.AllowRebaseMerge,
AllowSquash: d.AllowSquash,
AllowFastForwardOnly: d.AllowFastForwardOnly,
DefaultMergeStyle: d.DefaultMergeStyle,
DeleteBranchAfterMerge: d.DeleteBranchAfterMerge,
}
}
// GetOrgRepoDefaults get the organization's default repository settings
func GetOrgRepoDefaults(ctx *context.APIContext) {
orgID := ctx.Org.Organization.ID
defaults, err := git_model.GetOrgRepoDefaults(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgRepoDefaults(defaults, orgID))
}
// EditOrgRepoDefaults create or update the organization's default repository settings
func EditOrgRepoDefaults(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditOrgRepoDefaultsOption)
orgID := ctx.Org.Organization.ID
defaults, err := git_model.GetOrgRepoDefaults(ctx, orgID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if defaults == nil {
defaults = &git_model.OrgRepoDefaults{
OrgID: orgID,
AllowMerge: true,
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
AllowFastForwardOnly: true,
}
}
if form.ForcePrivate != nil {
defaults.ForcePrivate = *form.ForcePrivate
}
if form.ApplyPRDefaults != nil {
defaults.ApplyPRDefaults = *form.ApplyPRDefaults
}
if form.AllowMerge != nil {
defaults.AllowMerge = *form.AllowMerge
}
if form.AllowRebase != nil {
defaults.AllowRebase = *form.AllowRebase
}
if form.AllowRebaseMerge != nil {
defaults.AllowRebaseMerge = *form.AllowRebaseMerge
}
if form.AllowSquash != nil {
defaults.AllowSquash = *form.AllowSquash
}
if form.AllowFastForwardOnly != nil {
defaults.AllowFastForwardOnly = *form.AllowFastForwardOnly
}
if form.DefaultMergeStyle != nil {
defaults.DefaultMergeStyle = *form.DefaultMergeStyle
}
if form.DeleteBranchAfterMerge != nil {
defaults.DeleteBranchAfterMerge = *form.DeleteBranchAfterMerge
}
if err := git_model.UpsertOrgRepoDefaults(ctx, defaults); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgRepoDefaults(defaults, orgID))
}
// DeleteOrgRepoDefaults remove the organization's default repository settings
func DeleteOrgRepoDefaults(ctx *context.APIContext) {
if err := git_model.DeleteOrgRepoDefaults(ctx, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
-152
View File
@@ -1,152 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// toAPIOrgTagProtection converts an org tag rule to its API representation.
func toAPIOrgTagProtection(ctx *context.APIContext, rule *git_model.OrgProtectedTag) *api.OrgTagProtection {
teams, err := organization.FindOrgTeams(ctx, rule.OrgID)
if err != nil {
teams = nil
}
teamNamesByID := make(map[int64]string, len(teams))
for _, t := range teams {
teamNamesByID[t.ID] = t.Name
}
names := make([]string, 0, len(rule.AllowlistTeamIDs))
for _, id := range rule.AllowlistTeamIDs {
if name, ok := teamNamesByID[id]; ok {
names = append(names, name)
}
}
return &api.OrgTagProtection{
ID: rule.ID,
OrgID: rule.OrgID,
NamePattern: rule.NamePattern,
WhitelistTeams: names,
Created: rule.CreatedUnix.AsTime(),
Updated: rule.UpdatedUnix.AsTime(),
}
}
// ListOrgTagProtections list org-level tag protection rules
func ListOrgTagProtections(ctx *context.APIContext) {
rules, err := git_model.FindOrgProtectedTags(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiRules := make([]*api.OrgTagProtection, len(rules))
for i, rule := range rules {
apiRules[i] = toAPIOrgTagProtection(ctx, rule)
}
ctx.JSON(http.StatusOK, apiRules)
}
// GetOrgTagProtection get a specific org-level tag protection rule
func GetOrgTagProtection(ctx *context.APIContext) {
rule, err := git_model.GetOrgProtectedTagByID(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, toAPIOrgTagProtection(ctx, rule))
}
// CreateOrgTagProtection create an org-level tag protection rule
func CreateOrgTagProtection(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateOrgTagProtectionOption)
orgID := ctx.Org.Organization.ID
existing, err := git_model.GetOrgProtectedTagByNamePattern(ctx, orgID, form.NamePattern)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if existing != nil {
ctx.APIError(http.StatusForbidden, "org tag protection rule already exists for this pattern")
return
}
teams, ok := resolveTeamIDs(ctx, orgID, form.WhitelistTeams)
if !ok {
return
}
rule := &git_model.OrgProtectedTag{
OrgID: orgID,
NamePattern: form.NamePattern,
AllowlistTeamIDs: teams,
}
if err := git_model.CreateOrgProtectedTag(ctx, rule); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, toAPIOrgTagProtection(ctx, rule))
}
// EditOrgTagProtection edit an org-level tag protection rule
func EditOrgTagProtection(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditOrgTagProtectionOption)
orgID := ctx.Org.Organization.ID
rule, err := git_model.GetOrgProtectedTagByID(ctx, orgID, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil {
ctx.APIErrorNotFound()
return
}
if form.NamePattern != nil {
rule.NamePattern = *form.NamePattern
}
if form.WhitelistTeams != nil {
ids, ok := resolveTeamIDs(ctx, orgID, form.WhitelistTeams)
if !ok {
return
}
rule.AllowlistTeamIDs = ids
}
if err := git_model.UpdateOrgProtectedTag(ctx, rule); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPIOrgTagProtection(ctx, rule))
}
// DeleteOrgTagProtection delete an org-level tag protection rule
func DeleteOrgTagProtection(ctx *context.APIContext) {
orgID := ctx.Org.Organization.ID
rule, err := git_model.GetOrgProtectedTagByID(ctx, orgID, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil {
ctx.APIErrorNotFound()
return
}
if err := git_model.DeleteOrgProtectedTag(ctx, orgID, rule.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
-3
View File
@@ -9,7 +9,6 @@ import (
"net/http"
activities_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/activities"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
access_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm/access"
@@ -492,8 +491,6 @@ func AddTeamMember(ctx *context.APIContext) {
if err := org_service.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
} else if git_model.IsErrEmailDomainNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
+6
View File
@@ -156,6 +156,12 @@ type swaggerParameterBodies struct {
// in:body
EditBranchProtectionOption api.EditBranchProtectionOption
// in:body
CreateOrgBranchProtectionOption api.CreateOrgBranchProtectionOption
// in:body
EditOrgBranchProtectionOption api.EditOrgBranchProtectionOption
// in:body
UpdateBranchProtectionPriories api.UpdateBranchProtectionPriories
+14
View File
@@ -41,3 +41,17 @@ type swaggerResponseOrganizationPermissions struct {
// in:body
Body api.OrganizationPermissions `json:"body"`
}
// OrgBranchProtection
// swagger:response OrgBranchProtection
type swaggerResponseOrgBranchProtection struct {
// in:body
Body api.OrgBranchProtection `json:"body"`
}
// OrgBranchProtectionList
// swagger:response OrgBranchProtectionList
type swaggerResponseOrgBranchProtectionList struct {
// in:body
Body []api.OrgBranchProtection `json:"body"`
}
-2
View File
@@ -48,7 +48,6 @@ import (
repo_migrations "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/migrations"
mirror_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/mirror"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/oauth2_provider"
org_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/org"
packages_spec "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/packages/pkgspec"
pull_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/pull"
release_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/release"
@@ -156,7 +155,6 @@ func InitWebInstalled(ctx context.Context) {
mustInit(pull_service.Init)
mustInit(automerge.Init)
cascade.Init()
org_service.Init()
mustInit(task.Init)
mustInit(repo_migrations.Init)
eventsource.GetManager().Init()
+1 -110
View File
@@ -8,8 +8,6 @@ import (
"fmt"
"net/http"
"os"
"strconv"
"strings"
asymkey_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/asymkey"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
@@ -162,10 +160,6 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
gitRepo := ctx.Repo.GitRepo
objectFormat := ctx.Repo.GetObjectFormat()
if ctx.checkOrgPushPolicyBranch(oldCommitID, newCommitID, branchName) {
return
}
if newCommitID != objectFormat.EmptyObjectID().String() {
newCommit, err := gitRepo.GetCommit(newCommitID)
if err != nil {
@@ -461,10 +455,6 @@ func preReceiveTag(ctx *preReceiveContext, refFullName git.RefName) {
tagName := refFullName.TagName()
if ctx.checkOrgPushPolicyTag(tagName) {
return
}
if !ctx.gotProtectedTags {
var err error
ctx.protectedTags, err = git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID)
@@ -478,7 +468,7 @@ func preReceiveTag(ctx *preReceiveContext, refFullName git.RefName) {
ctx.gotProtectedTags = true
}
isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, ctx.protectedTags, ctx.Repo.Repository, tagName, ctx.opts.UserID)
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, ctx.protectedTags, tagName, ctx.opts.UserID)
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
@@ -606,102 +596,3 @@ func (ctx *preReceiveContext) loadPusherAndPermission() bool {
ctx.loadedPusher = true
return true
}
// checkOrgPushPolicyBranch enforces the owning organization's push policy on a
// branch push. It writes a 403 response and returns true when the push is rejected.
// Content checks (blocked paths, max file size) fail open on unexpected errors so a
// policy or parsing bug can never block every push in the organization.
func (ctx *preReceiveContext) checkOrgPushPolicyBranch(oldCommitID, newCommitID, branchName string) bool {
policy, err := git_model.GetOrgPushPolicyForRepo(ctx, ctx.Repo.Repository)
if err != nil {
log.Error("GetOrgPushPolicyForRepo for %-v: %v", ctx.Repo.Repository, err)
return false
}
if policy == nil {
return false
}
if !policy.BranchNameAllowed(branchName) {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Branch name %q is not allowed by the organization push policy (pattern: %s)", branchName, policy.BranchNamePattern),
})
return true
}
// Deletions have no content to inspect.
if newCommitID == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
return false
}
if globs := policy.BlockedFileGlobs(); len(globs) > 0 {
if _, err := pull_service.CheckFileProtection(ctx.Repo.GitRepo, branchName, oldCommitID, newCommitID, globs, 10, ctx.env); err != nil {
if pull_service.IsErrFilePathProtected(err) {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: "Push rejected by the organization push policy: a changed file matches a blocked path pattern",
})
return true
}
log.Error("org push policy blocked-path check for %-v: %v", ctx.Repo.Repository, err) // fail open
}
}
if policy.MaxFileSize > 0 {
if path, size := ctx.largestBlobOverLimit(newCommitID, policy.MaxFileSize); path != "" {
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Push rejected by the organization push policy: %q is %d bytes, over the %d-byte limit", path, size, policy.MaxFileSize),
})
return true
}
}
return false
}
// checkOrgPushPolicyTag enforces the organization tag naming policy. Returns true
// (with a 403 written) when the tag name is rejected.
func (ctx *preReceiveContext) checkOrgPushPolicyTag(tagName string) bool {
policy, err := git_model.GetOrgPushPolicyForRepo(ctx, ctx.Repo.Repository)
if err != nil {
log.Error("GetOrgPushPolicyForRepo for %-v: %v", ctx.Repo.Repository, err)
return false
}
if policy == nil || policy.TagNameAllowed(tagName) {
return false
}
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Tag name %q is not allowed by the organization push policy (pattern: %s)", tagName, policy.TagNamePattern),
})
return true
}
// largestBlobOverLimit returns the first file (and its size) in the pushed tip tree
// that exceeds limit bytes, or ("", 0) if none — or on any error (fail open).
func (ctx *preReceiveContext) largestBlobOverLimit(commitID string, limit int64) (string, int64) {
output, _, err := gitrepo.RunCmdString(ctx,
ctx.Repo.Repository,
gitcmd.NewCommand("ls-tree", "-r", "--long").
AddDynamicArguments(commitID).
WithEnv(ctx.env),
)
if err != nil {
log.Error("org push policy ls-tree for %-v: %v", ctx.Repo.Repository, err)
return "", 0
}
for _, line := range strings.Split(output, "\n") {
tab := strings.IndexByte(line, '\t')
if tab < 0 {
continue
}
fields := strings.Fields(line[:tab]) // mode, type, hash, size
if len(fields) < 4 || fields[1] != "blob" {
continue
}
size, perr := strconv.ParseInt(fields[3], 10, 64)
if perr != nil {
continue
}
if size > limit {
return line[tab+1:], size
}
}
return "", 0
}
@@ -1,51 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
import (
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// prepareOrgPushPolicyView surfaces the owning organization's push policy (if any)
// as a read-only informational section on the repo's branch protection settings page.
// The policy is enforced org-wide in the pre-receive hook and cannot be edited from
// the repository. Template data is only populated when the repo is owned by an
// organization that has a push policy configured (nil policy renders nothing).
// See issue #738.
func prepareOrgPushPolicyView(ctx *context.Context) error {
if !ctx.Repo.Owner.IsOrganization() {
return nil
}
policy, err := git_model.GetOrgPushPolicyForRepo(ctx, ctx.Repo.Repository)
if err != nil {
return err
}
if policy == nil {
return nil
}
ctx.Data["OrgPushPolicy"] = policy
return nil
}
// prepareOrgRepoDefaultsView surfaces the owning organization's repository defaults
// (if any) as a read-only informational section. These defaults are applied when a
// repository is created in, or transferred into, the organization; they are shown
// for reference only and are not editable here. Template data is only populated when
// the repo is owned by an organization that has repo defaults configured (nil renders
// nothing). See issue #738.
func prepareOrgRepoDefaultsView(ctx *context.Context) error {
if !ctx.Repo.Owner.IsOrganization() {
return nil
}
defaults, err := git_model.GetOrgRepoDefaultsForRepo(ctx, ctx.Repo.Repository)
if err != nil {
return err
}
if defaults == nil {
return nil
}
ctx.Data["OrgRepoDefaults"] = defaults
return nil
}
@@ -34,64 +34,6 @@ const (
tplProtectedBranch templates.TplName = "repo/settings/protected_branch"
)
// orgBranchProtectionView is a read-only presentation of an org-level branch
// protection rule for the repo settings page, with team IDs resolved to names.
type orgBranchProtectionView struct {
Rule *git_model.OrgProtectedBranch
PushTeams string
ForcePushTeams string
DeleteTeams string
MergeTeams string
ApprovalTeams string
StatusContexts string
}
// prepareOrgProtectedBranches loads the owning organization's branch protection
// rules and exposes them (with team IDs resolved to names) as read-only view models
// under the "OrgProtectedBranches" template key.
func prepareOrgProtectedBranches(ctx *context.Context) error {
orgRules, err := git_model.FindOrgProtectedBranchRules(ctx, ctx.Repo.Owner.ID)
if err != nil {
return err
}
if len(orgRules) == 0 {
return nil
}
teams, err := organization.FindOrgTeams(ctx, ctx.Repo.Owner.ID)
if err != nil {
return err
}
teamNames := make(map[int64]string, len(teams))
for _, t := range teams {
teamNames[t.ID] = t.Name
}
join := func(ids []int64) string {
names := make([]string, 0, len(ids))
for _, id := range ids {
if n, ok := teamNames[id]; ok {
names = append(names, n)
}
}
return strings.Join(names, ", ")
}
views := make([]*orgBranchProtectionView, len(orgRules))
for i, r := range orgRules {
views[i] = &orgBranchProtectionView{
Rule: r,
PushTeams: join(r.WhitelistTeamIDs),
ForcePushTeams: join(r.ForcePushAllowlistTeamIDs),
DeleteTeams: join(r.DeleteAllowlistTeamIDs),
MergeTeams: join(r.MergeWhitelistTeamIDs),
ApprovalTeams: join(r.ApprovalsWhitelistTeamIDs),
StatusContexts: strings.Join(r.StatusCheckContexts, ", "),
}
}
ctx.Data["OrgProtectedBranches"] = views
return nil
}
// ProtectedBranchRules render the page to protect the repository
func ProtectedBranchRules(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.branches")
@@ -104,26 +46,6 @@ func ProtectedBranchRules(ctx *context.Context) {
}
ctx.Data["ProtectedBranches"] = rules
// Surface the organization-level rules that also apply to this repo (read-only),
// so admins can see the org "floor" that is layered on top of the repo's own
// rules at enforcement time. See issue #727.
if ctx.Repo.Owner.IsOrganization() {
if err := prepareOrgProtectedBranches(ctx); err != nil {
ctx.ServerError("prepareOrgProtectedBranches", err)
return
}
// Surface the org-wide push policy and the org repo defaults that apply to
// this repo (read-only, informational). See issue #738.
if err := prepareOrgPushPolicyView(ctx); err != nil {
ctx.ServerError("prepareOrgPushPolicyView", err)
return
}
if err := prepareOrgRepoDefaultsView(ctx); err != nil {
ctx.ServerError("prepareOrgRepoDefaultsView", err)
return
}
}
repo.PrepareBranchList(ctx)
if ctx.Written() {
return
-37
View File
@@ -138,13 +138,6 @@ func DeleteProtectedTagPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
}
// orgProtectedTagView is a read-only presentation of an org-level tag rule for the
// repo settings page, with allowlist team IDs resolved to names.
type orgProtectedTagView struct {
Rule *git_model.OrgProtectedTag
Teams string
}
func setTagsContext(ctx *context.Context) error {
ctx.Data["Title"] = ctx.Tr("repo.settings.tags")
ctx.Data["PageIsSettingsTags"] = true
@@ -170,36 +163,6 @@ func setTagsContext(ctx *context.Context) error {
return err
}
ctx.Data["Teams"] = teams
// Surface the organization's tag protection rules read-only, so admins can see
// the org "floor" layered on top of this repo's own protected tags (#727).
orgRules, err := git_model.FindOrgProtectedTags(ctx, ctx.Repo.Owner.ID)
if err != nil {
ctx.ServerError("FindOrgProtectedTags", err)
return err
}
if len(orgRules) > 0 {
allTeams, err := organization.FindOrgTeams(ctx, ctx.Repo.Owner.ID)
if err != nil {
ctx.ServerError("FindOrgTeams", err)
return err
}
teamNames := make(map[int64]string, len(allTeams))
for _, t := range allTeams {
teamNames[t.ID] = t.Name
}
views := make([]*orgProtectedTagView, len(orgRules))
for i, r := range orgRules {
names := make([]string, 0, len(r.AllowlistTeamIDs))
for _, id := range r.AllowlistTeamIDs {
if n, ok := teamNames[id]; ok {
names = append(names, n)
}
}
views[i] = &orgProtectedTagView{Rule: r, Teams: strings.Join(names, ", ")}
}
ctx.Data["OrgProtectedTags"] = views
}
}
return nil
-92
View File
@@ -1,92 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package org
import (
"context"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
notify_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/notify"
repo_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/repository"
)
// Init registers the org notifier so organization repository defaults are applied to
// repositories as they are created in or transferred into the org. The notifier
// pattern avoids the services/repository -> services/org import cycle.
func Init() {
notify_service.RegisterNotifier(&repoDefaultsNotifier{})
}
type repoDefaultsNotifier struct {
notify_service.NullNotifier
}
func (n *repoDefaultsNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
applyOrgRepoDefaults(ctx, u, repo)
}
func (n *repoDefaultsNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
applyOrgRepoDefaults(ctx, nil, repo)
}
// applyOrgRepoDefaults applies the owning organization's default repository settings
// to a repo that has just joined the org. Best-effort: errors are logged and never
// propagated, so a defaults bug can never break repository creation or transfer.
func applyOrgRepoDefaults(ctx context.Context, owner *user_model.User, repo *repo_model.Repository) {
if owner == nil {
if err := repo.LoadOwner(ctx); err != nil {
log.Error("org repo defaults: load owner of repo %d: %v", repo.ID, err)
return
}
owner = repo.Owner
}
if owner == nil || !owner.IsOrganization() {
return
}
defaults, err := git_model.GetOrgRepoDefaults(ctx, owner.ID)
if err != nil {
log.Error("org repo defaults: load for org %d: %v", owner.ID, err)
return
}
if defaults == nil {
return
}
if defaults.ForcePrivate && !repo.IsPrivate {
repo.IsPrivate = true
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
log.Error("org repo defaults: force private on repo %d: %v", repo.ID, err)
}
}
if defaults.ApplyPRDefaults {
prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
// The repository may not have pull requests enabled; nothing to apply.
return
}
cfg := prUnit.PullRequestsConfig()
cfg.AllowMerge = defaults.AllowMerge
cfg.AllowRebase = defaults.AllowRebase
cfg.AllowRebaseMerge = defaults.AllowRebaseMerge
cfg.AllowSquash = defaults.AllowSquash
cfg.AllowFastForwardOnly = defaults.AllowFastForwardOnly
cfg.DefaultDeleteBranchAfterMerge = defaults.DeleteBranchAfterMerge
if defaults.DefaultMergeStyle != "" {
cfg.DefaultMergeStyle = repo_model.MergeStyle(defaults.DefaultMergeStyle)
}
if err := repo_service.UpdateRepositoryUnits(ctx, repo, []repo_model.RepoUnit{{
RepoID: repo.ID,
Type: unit.TypePullRequests,
Config: cfg,
}}, nil); err != nil {
log.Error("org repo defaults: update PR unit on repo %d: %v", repo.ID, err)
}
}
}
-7
View File
@@ -220,13 +220,6 @@ func AddTeamMember(ctx context.Context, team *organization.Team, user *user_mode
return err
}
// Enforce the organization email domain policy for new members.
if allowed, err := git_model.OrgEmailDomainAllowed(ctx, team.OrgID, user.Email); err != nil {
return err
} else if !allowed {
return git_model.ErrEmailDomainNotAllowed{Email: user.Email, OrgID: team.OrgID}
}
if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil {
return err
}
+2 -2
View File
@@ -92,7 +92,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel
// Trim '--' prefix to prevent command line argument vulnerability.
rel.TagName = strings.TrimPrefix(rel.TagName, "--")
isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, protectedTags, rel.Repo, rel.TagName, rel.PublisherID)
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID)
if err != nil {
return false, err
}
@@ -439,7 +439,7 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
if err != nil {
return fmt.Errorf("GetProtectedTags: %w", err)
}
isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, protectedTags, repo, rel.TagName, doer.ID)
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, doer.ID)
if err != nil {
return err
}
+1 -22
View File
@@ -6,7 +6,6 @@ package security
import (
"context"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
@@ -22,11 +21,7 @@ func ScanPushForSecrets(ctx context.Context, repoID int64, commit *git.Commit) [
return nil
}
if !cfg.Enabled || !cfg.BlockOnPush || !cfg.SecretScanner {
// The owning organization may mandate secret blocking regardless of the
// repository's own scanner config (org push policy).
if !orgRequiresSecretBlock(ctx, repoID) {
return nil
}
return nil
}
scanner := NewSecretScanner()
@@ -38,22 +33,6 @@ func ScanPushForSecrets(ctx context.Context, repoID int64, commit *git.Commit) [
return findings
}
// orgRequiresSecretBlock reports whether the repo's owning organization mandates
// secret blocking on push via its org push policy.
func orgRequiresSecretBlock(ctx context.Context, repoID int64) bool {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
log.Error("orgRequiresSecretBlock: GetRepositoryByID: %v", err)
return false
}
policy, err := git_model.GetOrgPushPolicyForRepo(ctx, repo)
if err != nil {
log.Error("orgRequiresSecretBlock: GetOrgPushPolicyForRepo: %v", err)
return false
}
return policy != nil && policy.RequireSecretBlock
}
// ScanOnPush runs enabled scanners against a commit pushed to the default branch.
// Called from services/repository/push.go on default branch pushes.
func ScanOnPush(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
-174
View File
@@ -62,180 +62,6 @@
{{end}}
</div>
</div>
{{if .OrgProtectedBranches}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.org_protected_branch"}}
</h4>
<div class="ui attached segment">
<p class="tw-mb-3">{{ctx.Locale.Tr "repo.settings.org_protected_branch_desc"}}</p>
<div class="flex-divided-list items-with-main">
{{range .OrgProtectedBranches}}
<div class="item">
<div class="item-main tw-w-full">
<details class="tw-w-full">
<summary class="tw-flex tw-items-center tw-gap-2 tw-flex-wrap tw-cursor-pointer">
<div class="ui basic label">{{.Rule.RuleName}}</div>
<span class="ui tiny label">{{svg "octicon-organization" 12}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.inherited"}}</span>
<span class="text grey tw-text-sm">{{svg "octicon-lock" 12}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.read_only"}}</span>
</summary>
<table class="ui very basic compact table tw-mt-2 tw-w-auto tw-ml-4">
<tbody>
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.direct_push"}}</td>
<td>{{if not .Rule.CanPush}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.blocked"}}{{else if .Rule.EnableWhitelist}}{{if .PushTeams}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.teams" .PushTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.write_access"}}{{end}}</td>
</tr>
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.force_push"}}</td>
<td>{{if not .Rule.CanForcePush}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.blocked"}}{{else if .Rule.EnableForcePushAllowlist}}{{if .ForcePushTeams}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.teams" .ForcePushTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.allowed"}}{{end}}</td>
</tr>
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.deletion"}}</td>
<td>{{if not .Rule.CanDelete}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.blocked"}}{{else if .Rule.EnableDeleteAllowlist}}{{if .DeleteTeams}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.teams" .DeleteTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.allowed"}}{{end}}</td>
</tr>
{{if gt .Rule.RequiredApprovals 0}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.approvals"}}</td>
<td>{{.Rule.RequiredApprovals}}{{if .ApprovalTeams}} ({{.ApprovalTeams}}){{end}}</td>
</tr>
{{end}}
{{if .Rule.EnableMergeWhitelist}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.merge"}}</td>
<td>{{if .MergeTeams}}{{.MergeTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}</td>
</tr>
{{end}}
{{if .Rule.EnableStatusCheck}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.status_check"}}</td>
<td>{{if .StatusContexts}}{{.StatusContexts}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.any"}}{{end}}</td>
</tr>
{{end}}
{{if .Rule.RequireSignedCommits}}
<tr>
<td class="tw-font-semibold">{{ctx.Locale.Tr "repo.settings.org_protected_branch.signed"}}</td>
<td>{{svg "octicon-check" 14}}</td>
</tr>
{{end}}
{{if .Rule.ProtectedFilePatterns}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.protected_files"}}</td>
<td><code>{{.Rule.ProtectedFilePatterns}}</code></td>
</tr>
{{end}}
{{if or .Rule.BlockOnOutdatedBranch .Rule.BlockOnRejectedReviews .Rule.BlockAdminMergeOverride}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_protected_branch.also"}}</td>
<td>
<div class="tw-flex tw-gap-1 tw-flex-wrap">
{{if .Rule.BlockOnOutdatedBranch}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_outdated"}}</span>{{end}}
{{if .Rule.BlockOnRejectedReviews}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_rejected"}}</span>{{end}}
{{if .Rule.BlockAdminMergeOverride}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_admin"}}</span>{{end}}
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</details>
</div>
</div>
{{end}}
</div>
</div>
{{end}}
{{if .OrgPushPolicy}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.org_push_policy"}}
</h4>
<div class="ui attached segment">
<p class="tw-mb-3">
{{svg "octicon-organization" 12}} {{ctx.Locale.Tr "repo.settings.org_push_policy.inherited"}}
<span class="text grey tw-text-sm tw-ml-2">{{svg "octicon-lock" 12}} {{ctx.Locale.Tr "repo.settings.org_push_policy.read_only"}}</span>
</p>
<p class="tw-mb-3">{{ctx.Locale.Tr "repo.settings.org_push_policy_desc"}}</p>
<table class="ui very basic compact table tw-w-auto">
<tbody>
{{if .OrgPushPolicy.BranchNamePattern}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_push_policy.branch_pattern"}}</td>
<td><code>{{.OrgPushPolicy.BranchNamePattern}}</code></td>
</tr>
{{end}}
{{if .OrgPushPolicy.TagNamePattern}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_push_policy.tag_pattern"}}</td>
<td><code>{{.OrgPushPolicy.TagNamePattern}}</code></td>
</tr>
{{end}}
{{if .OrgPushPolicy.RequireSecretBlock}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_push_policy.require_secret_block"}}</td>
<td>{{svg "octicon-check" 14}} {{ctx.Locale.Tr "repo.settings.org_push_policy.enabled"}}</td>
</tr>
{{end}}
{{if gt .OrgPushPolicy.MaxFileSize 0}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_push_policy.max_file_size"}}</td>
<td>{{FileSize .OrgPushPolicy.MaxFileSize}}</td>
</tr>
{{end}}
{{if .OrgPushPolicy.BlockedFilePatterns}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_push_policy.blocked_files"}}</td>
<td><code>{{.OrgPushPolicy.BlockedFilePatterns}}</code></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{if .OrgRepoDefaults}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "repo.settings.org_repo_defaults"}}
</h4>
<div class="ui attached segment">
<p class="tw-mb-3">
{{svg "octicon-organization" 12}} {{ctx.Locale.Tr "repo.settings.org_repo_defaults.inherited"}}
<span class="text grey tw-text-sm tw-ml-2">{{svg "octicon-lock" 12}} {{ctx.Locale.Tr "repo.settings.org_repo_defaults.read_only"}}</span>
</p>
<p class="tw-mb-3">{{ctx.Locale.Tr "repo.settings.org_repo_defaults_desc"}}</p>
<table class="ui very basic compact table tw-w-auto">
<tbody>
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.force_private"}}</td>
<td>{{if .OrgRepoDefaults.ForcePrivate}}{{ctx.Locale.Tr "repo.settings.org_repo_defaults.enabled"}}{{else}}{{ctx.Locale.Tr "repo.settings.org_repo_defaults.disabled"}}{{end}}</td>
</tr>
{{if .OrgRepoDefaults.ApplyPRDefaults}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.merge_styles"}}</td>
<td>
<div class="tw-flex tw-gap-1 tw-flex-wrap">
{{if .OrgRepoDefaults.AllowMerge}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.style_merge"}}</span>{{end}}
{{if .OrgRepoDefaults.AllowRebase}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.style_rebase"}}</span>{{end}}
{{if .OrgRepoDefaults.AllowRebaseMerge}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.style_rebase_merge"}}</span>{{end}}
{{if .OrgRepoDefaults.AllowSquash}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.style_squash"}}</span>{{end}}
{{if .OrgRepoDefaults.AllowFastForwardOnly}}<span class="ui tiny basic label">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.style_fast_forward_only"}}</span>{{end}}
</div>
</td>
</tr>
{{if .OrgRepoDefaults.DefaultMergeStyle}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.default_merge_style"}}</td>
<td><code>{{.OrgRepoDefaults.DefaultMergeStyle}}</code></td>
</tr>
{{end}}
<tr>
<td class="tw-font-semibold tw-align-top">{{ctx.Locale.Tr "repo.settings.org_repo_defaults.delete_branch_after_merge"}}</td>
<td>{{if .OrgRepoDefaults.DeleteBranchAfterMerge}}{{ctx.Locale.Tr "repo.settings.org_repo_defaults.enabled"}}{{else}}{{ctx.Locale.Tr "repo.settings.org_repo_defaults.disabled"}}{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{end}}
</div>
-24
View File
@@ -116,30 +116,6 @@
{{end}}
</tbody>
</table>
{{if .OrgProtectedTags}}
<h5 class="ui top attached header tw-mt-4">
{{svg "octicon-organization" 14}} {{ctx.Locale.Tr "repo.settings.org_protected_tag"}}
</h5>
<div class="ui attached segment">
<p class="tw-mb-3">{{ctx.Locale.Tr "repo.settings.org_protected_tag_desc"}}</p>
<table class="ui single line table">
<thead>
<th>{{ctx.Locale.Tr "repo.settings.tags.protection.pattern"}}</th>
<th>{{ctx.Locale.Tr "repo.settings.tags.protection.allowed"}}</th>
<th class="tw-text-right">{{ctx.Locale.Tr "repo.settings.org_protected_tag.read_only"}}</th>
</thead>
<tbody>
{{range .OrgProtectedTags}}
<tr>
<td><pre>{{.Rule.NamePattern}}</pre></td>
<td>{{if .Teams}}{{.Teams}}{{else}}{{ctx.Locale.Tr "repo.settings.tags.protection.allowed.noone"}}{{end}}</td>
<td class="tw-text-right"><span class="text grey">{{svg "octicon-lock" 14}}</span></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
</div>
</div>
+697
View File
@@ -21518,6 +21518,192 @@
}
}
}
},
"/orgs/{org}/branch_protections": {
"get": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "List an organization's branch protection rules",
"operationId": "orgListBranchProtections",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/OrgBranchProtectionList"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Create an org-level branch protection rule",
"operationId": "orgCreateBranchProtection",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"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"
}
}
}
},
"/orgs/{org}/branch_protections/{name}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Get a specific org-level branch protection rule",
"operationId": "orgGetBranchProtection",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the branch protection rule",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/OrgBranchProtection"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"delete": {
"tags": [
"organization"
],
"summary": "Delete an org-level branch protection rule",
"operationId": "orgDeleteBranchProtection",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the branch protection rule",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"patch": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Edit an org-level branch protection rule. Only fields that are set will be changed",
"operationId": "orgEditBranchProtection",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the branch protection rule",
"name": "name",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/EditOrgBranchProtectionOption"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/OrgBranchProtection"
},
"404": {
"$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/validationError"
}
}
}
}
},
"definitions": {
@@ -30266,6 +30452,502 @@
}
},
"x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
},
"CreateOrgBranchProtectionOption": {
"description": "CreateOrgBranchProtectionOption options for creating an org-level branch protection",
"type": "object",
"properties": {
"approvals_whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "ApprovalsWhitelistTeams"
},
"approvals_whitelist_username": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "ApprovalsWhitelistUsernames"
},
"block_admin_merge_override": {
"type": "boolean",
"x-go-name": "BlockAdminMergeOverride"
},
"block_on_official_review_requests": {
"type": "boolean",
"x-go-name": "BlockOnOfficialReviewRequests"
},
"block_on_outdated_branch": {
"type": "boolean",
"x-go-name": "BlockOnOutdatedBranch"
},
"block_on_rejected_reviews": {
"type": "boolean",
"x-go-name": "BlockOnRejectedReviews"
},
"dismiss_stale_approvals": {
"type": "boolean",
"x-go-name": "DismissStaleApprovals"
},
"enable_approvals_whitelist": {
"type": "boolean",
"x-go-name": "EnableApprovalsWhitelist"
},
"enable_force_push": {
"type": "boolean",
"x-go-name": "EnableForcePush"
},
"enable_force_push_allowlist": {
"type": "boolean",
"x-go-name": "EnableForcePushAllowlist"
},
"enable_merge_whitelist": {
"type": "boolean",
"x-go-name": "EnableMergeWhitelist"
},
"enable_push": {
"type": "boolean",
"x-go-name": "EnablePush"
},
"enable_push_whitelist": {
"type": "boolean",
"x-go-name": "EnablePushWhitelist"
},
"enable_status_check": {
"type": "boolean",
"x-go-name": "EnableStatusCheck"
},
"force_push_allowlist_actions_user": {
"type": "boolean",
"x-go-name": "ForcePushAllowlistActionsUser"
},
"force_push_allowlist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "ForcePushAllowlistTeams"
},
"force_push_allowlist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "ForcePushAllowlistUsernames"
},
"ignore_stale_approvals": {
"type": "boolean",
"x-go-name": "IgnoreStaleApprovals"
},
"merge_whitelist_actions_user": {
"type": "boolean",
"x-go-name": "MergeWhitelistActionsUser"
},
"merge_whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "MergeWhitelistTeams"
},
"merge_whitelist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "MergeWhitelistUsernames"
},
"priority": {
"type": "integer",
"format": "int64",
"x-go-name": "Priority"
},
"protected_file_patterns": {
"type": "string",
"x-go-name": "ProtectedFilePatterns"
},
"push_whitelist_actions_user": {
"type": "boolean",
"x-go-name": "PushWhitelistActionsUser"
},
"push_whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "PushWhitelistTeams"
},
"push_whitelist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "PushWhitelistUsernames"
},
"require_signed_commits": {
"type": "boolean",
"x-go-name": "RequireSignedCommits"
},
"required_approvals": {
"type": "integer",
"format": "int64",
"x-go-name": "RequiredApprovals"
},
"rule_name": {
"type": "string",
"x-go-name": "RuleName"
},
"status_check_contexts": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "StatusCheckContexts"
},
"unprotected_file_patterns": {
"type": "string",
"x-go-name": "UnprotectedFilePatterns"
}
},
"x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
},
"EditOrgBranchProtectionOption": {
"description": "EditOrgBranchProtectionOption options for editing an org-level branch protection",
"type": "object",
"properties": {
"approvals_whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "ApprovalsWhitelistTeams"
},
"approvals_whitelist_username": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "ApprovalsWhitelistUsernames"
},
"block_admin_merge_override": {
"type": "boolean",
"x-go-name": "BlockAdminMergeOverride"
},
"block_on_official_review_requests": {
"type": "boolean",
"x-go-name": "BlockOnOfficialReviewRequests"
},
"block_on_outdated_branch": {
"type": "boolean",
"x-go-name": "BlockOnOutdatedBranch"
},
"block_on_rejected_reviews": {
"type": "boolean",
"x-go-name": "BlockOnRejectedReviews"
},
"dismiss_stale_approvals": {
"type": "boolean",
"x-go-name": "DismissStaleApprovals"
},
"enable_approvals_whitelist": {
"type": "boolean",
"x-go-name": "EnableApprovalsWhitelist"
},
"enable_force_push": {
"type": "boolean",
"x-go-name": "EnableForcePush"
},
"enable_force_push_allowlist": {
"type": "boolean",
"x-go-name": "EnableForcePushAllowlist"
},
"enable_merge_whitelist": {
"type": "boolean",
"x-go-name": "EnableMergeWhitelist"
},
"enable_push": {
"type": "boolean",
"x-go-name": "EnablePush"
},
"enable_push_whitelist": {
"type": "boolean",
"x-go-name": "EnablePushWhitelist"
},
"enable_status_check": {
"type": "boolean",
"x-go-name": "EnableStatusCheck"
},
"force_push_allowlist_actions_user": {
"type": "boolean",
"x-go-name": "ForcePushAllowlistActionsUser"
},
"force_push_allowlist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "ForcePushAllowlistTeams"
},
"force_push_allowlist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "ForcePushAllowlistUsernames"
},
"ignore_stale_approvals": {
"type": "boolean",
"x-go-name": "IgnoreStaleApprovals"
},
"merge_whitelist_actions_user": {
"type": "boolean",
"x-go-name": "MergeWhitelistActionsUser"
},
"merge_whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "MergeWhitelistTeams"
},
"merge_whitelist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "MergeWhitelistUsernames"
},
"priority": {
"type": "integer",
"format": "int64",
"x-go-name": "Priority"
},
"protected_file_patterns": {
"type": "string",
"x-go-name": "ProtectedFilePatterns"
},
"push_whitelist_actions_user": {
"type": "boolean",
"x-go-name": "PushWhitelistActionsUser"
},
"push_whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "PushWhitelistTeams"
},
"push_whitelist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "PushWhitelistUsernames"
},
"require_signed_commits": {
"type": "boolean",
"x-go-name": "RequireSignedCommits"
},
"required_approvals": {
"type": "integer",
"format": "int64",
"x-go-name": "RequiredApprovals"
},
"status_check_contexts": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "StatusCheckContexts"
},
"unprotected_file_patterns": {
"type": "string",
"x-go-name": "UnprotectedFilePatterns"
}
},
"x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
},
"OrgBranchProtection": {
"description": "OrgBranchProtection represents an org-level branch protection ruleset",
"type": "object",
"properties": {
"approvals_whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "ApprovalsWhitelistTeams"
},
"approvals_whitelist_username": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "ApprovalsWhitelistUsernames"
},
"block_admin_merge_override": {
"type": "boolean",
"x-go-name": "BlockAdminMergeOverride"
},
"block_on_official_review_requests": {
"type": "boolean",
"x-go-name": "BlockOnOfficialReviewRequests"
},
"block_on_outdated_branch": {
"type": "boolean",
"x-go-name": "BlockOnOutdatedBranch"
},
"block_on_rejected_reviews": {
"type": "boolean",
"x-go-name": "BlockOnRejectedReviews"
},
"created_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Created"
},
"dismiss_stale_approvals": {
"type": "boolean",
"x-go-name": "DismissStaleApprovals"
},
"enable_approvals_whitelist": {
"type": "boolean",
"x-go-name": "EnableApprovalsWhitelist"
},
"enable_force_push": {
"type": "boolean",
"x-go-name": "EnableForcePush"
},
"enable_force_push_allowlist": {
"type": "boolean",
"x-go-name": "EnableForcePushAllowlist"
},
"enable_merge_whitelist": {
"type": "boolean",
"x-go-name": "EnableMergeWhitelist"
},
"enable_push": {
"type": "boolean",
"x-go-name": "EnablePush"
},
"enable_push_whitelist": {
"type": "boolean",
"x-go-name": "EnablePushWhitelist"
},
"enable_status_check": {
"type": "boolean",
"x-go-name": "EnableStatusCheck"
},
"force_push_allowlist_actions_user": {
"type": "boolean",
"x-go-name": "ForcePushAllowlistActionsUser"
},
"force_push_allowlist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "ForcePushAllowlistTeams"
},
"force_push_allowlist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "ForcePushAllowlistUsernames"
},
"id": {
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"ignore_stale_approvals": {
"type": "boolean",
"x-go-name": "IgnoreStaleApprovals"
},
"merge_whitelist_actions_user": {
"type": "boolean",
"x-go-name": "MergeWhitelistActionsUser"
},
"merge_whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "MergeWhitelistTeams"
},
"merge_whitelist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "MergeWhitelistUsernames"
},
"org_id": {
"type": "integer",
"format": "int64",
"x-go-name": "OrgID"
},
"priority": {
"type": "integer",
"format": "int64",
"x-go-name": "Priority"
},
"protected_file_patterns": {
"type": "string",
"x-go-name": "ProtectedFilePatterns"
},
"push_whitelist_actions_user": {
"type": "boolean",
"x-go-name": "PushWhitelistActionsUser"
},
"push_whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "PushWhitelistTeams"
},
"push_whitelist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "PushWhitelistUsernames"
},
"require_signed_commits": {
"type": "boolean",
"x-go-name": "RequireSignedCommits"
},
"required_approvals": {
"type": "integer",
"format": "int64",
"x-go-name": "RequiredApprovals"
},
"rule_name": {
"type": "string",
"x-go-name": "RuleName"
},
"status_check_contexts": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "StatusCheckContexts"
},
"unprotected_file_patterns": {
"type": "string",
"x-go-name": "UnprotectedFilePatterns"
},
"updated_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Updated"
}
},
"x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
}
},
"responses": {
@@ -31404,6 +32086,21 @@
"type": "string"
}
}
},
"OrgBranchProtection": {
"description": "OrgBranchProtection",
"schema": {
"$ref": "#/definitions/OrgBranchProtection"
}
},
"OrgBranchProtectionList": {
"description": "OrgBranchProtectionList",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/OrgBranchProtection"
}
}
}
},
"securityDefinitions": {
+709
View File
@@ -911,6 +911,29 @@
},
"description": "OAuth2ApplicationList represents a list of OAuth2 applications."
},
"OrgBranchProtection": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrgBranchProtection"
}
}
},
"description": "OrgBranchProtection"
},
"OrgBranchProtectionList": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/OrgBranchProtection"
},
"type": "array"
}
}
},
"description": "OrgBranchProtectionList"
},
"Organization": {
"content": {
"application/json": {
@@ -4264,6 +4287,166 @@
"type": "object",
"x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
},
"CreateOrgBranchProtectionOption": {
"description": "CreateOrgBranchProtectionOption options for creating an org-level branch protection",
"properties": {
"approvals_whitelist_teams": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "ApprovalsWhitelistTeams"
},
"approvals_whitelist_username": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "ApprovalsWhitelistUsernames"
},
"block_admin_merge_override": {
"type": "boolean",
"x-go-name": "BlockAdminMergeOverride"
},
"block_on_official_review_requests": {
"type": "boolean",
"x-go-name": "BlockOnOfficialReviewRequests"
},
"block_on_outdated_branch": {
"type": "boolean",
"x-go-name": "BlockOnOutdatedBranch"
},
"block_on_rejected_reviews": {
"type": "boolean",
"x-go-name": "BlockOnRejectedReviews"
},
"dismiss_stale_approvals": {
"type": "boolean",
"x-go-name": "DismissStaleApprovals"
},
"enable_approvals_whitelist": {
"type": "boolean",
"x-go-name": "EnableApprovalsWhitelist"
},
"enable_force_push": {
"type": "boolean",
"x-go-name": "EnableForcePush"
},
"enable_force_push_allowlist": {
"type": "boolean",
"x-go-name": "EnableForcePushAllowlist"
},
"enable_merge_whitelist": {
"type": "boolean",
"x-go-name": "EnableMergeWhitelist"
},
"enable_push": {
"type": "boolean",
"x-go-name": "EnablePush"
},
"enable_push_whitelist": {
"type": "boolean",
"x-go-name": "EnablePushWhitelist"
},
"enable_status_check": {
"type": "boolean",
"x-go-name": "EnableStatusCheck"
},
"force_push_allowlist_actions_user": {
"type": "boolean",
"x-go-name": "ForcePushAllowlistActionsUser"
},
"force_push_allowlist_teams": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "ForcePushAllowlistTeams"
},
"force_push_allowlist_usernames": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "ForcePushAllowlistUsernames"
},
"ignore_stale_approvals": {
"type": "boolean",
"x-go-name": "IgnoreStaleApprovals"
},
"merge_whitelist_actions_user": {
"type": "boolean",
"x-go-name": "MergeWhitelistActionsUser"
},
"merge_whitelist_teams": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "MergeWhitelistTeams"
},
"merge_whitelist_usernames": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "MergeWhitelistUsernames"
},
"priority": {
"format": "int64",
"type": "integer",
"x-go-name": "Priority"
},
"protected_file_patterns": {
"type": "string",
"x-go-name": "ProtectedFilePatterns"
},
"push_whitelist_actions_user": {
"type": "boolean",
"x-go-name": "PushWhitelistActionsUser"
},
"push_whitelist_teams": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "PushWhitelistTeams"
},
"push_whitelist_usernames": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "PushWhitelistUsernames"
},
"require_signed_commits": {
"type": "boolean",
"x-go-name": "RequireSignedCommits"
},
"required_approvals": {
"format": "int64",
"type": "integer",
"x-go-name": "RequiredApprovals"
},
"rule_name": {
"type": "string",
"x-go-name": "RuleName"
},
"status_check_contexts": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "StatusCheckContexts"
},
"unprotected_file_patterns": {
"type": "string",
"x-go-name": "UnprotectedFilePatterns"
}
},
"type": "object",
"x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
},
"CreateOrgOption": {
"description": "CreateOrgOption options for creating an organization",
"properties": {
@@ -5431,6 +5614,162 @@
"type": "object",
"x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
},
"EditOrgBranchProtectionOption": {
"description": "EditOrgBranchProtectionOption options for editing an org-level branch protection",
"properties": {
"approvals_whitelist_teams": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "ApprovalsWhitelistTeams"
},
"approvals_whitelist_username": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "ApprovalsWhitelistUsernames"
},
"block_admin_merge_override": {
"type": "boolean",
"x-go-name": "BlockAdminMergeOverride"
},
"block_on_official_review_requests": {
"type": "boolean",
"x-go-name": "BlockOnOfficialReviewRequests"
},
"block_on_outdated_branch": {
"type": "boolean",
"x-go-name": "BlockOnOutdatedBranch"
},
"block_on_rejected_reviews": {
"type": "boolean",
"x-go-name": "BlockOnRejectedReviews"
},
"dismiss_stale_approvals": {
"type": "boolean",
"x-go-name": "DismissStaleApprovals"
},
"enable_approvals_whitelist": {
"type": "boolean",
"x-go-name": "EnableApprovalsWhitelist"
},
"enable_force_push": {
"type": "boolean",
"x-go-name": "EnableForcePush"
},
"enable_force_push_allowlist": {
"type": "boolean",
"x-go-name": "EnableForcePushAllowlist"
},
"enable_merge_whitelist": {
"type": "boolean",
"x-go-name": "EnableMergeWhitelist"
},
"enable_push": {
"type": "boolean",
"x-go-name": "EnablePush"
},
"enable_push_whitelist": {
"type": "boolean",
"x-go-name": "EnablePushWhitelist"
},
"enable_status_check": {
"type": "boolean",
"x-go-name": "EnableStatusCheck"
},
"force_push_allowlist_actions_user": {
"type": "boolean",
"x-go-name": "ForcePushAllowlistActionsUser"
},
"force_push_allowlist_teams": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "ForcePushAllowlistTeams"
},
"force_push_allowlist_usernames": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "ForcePushAllowlistUsernames"
},
"ignore_stale_approvals": {
"type": "boolean",
"x-go-name": "IgnoreStaleApprovals"
},
"merge_whitelist_actions_user": {
"type": "boolean",
"x-go-name": "MergeWhitelistActionsUser"
},
"merge_whitelist_teams": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "MergeWhitelistTeams"
},
"merge_whitelist_usernames": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "MergeWhitelistUsernames"
},
"priority": {
"format": "int64",
"type": "integer",
"x-go-name": "Priority"
},
"protected_file_patterns": {
"type": "string",
"x-go-name": "ProtectedFilePatterns"
},
"push_whitelist_actions_user": {
"type": "boolean",
"x-go-name": "PushWhitelistActionsUser"
},
"push_whitelist_teams": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "PushWhitelistTeams"
},
"push_whitelist_usernames": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "PushWhitelistUsernames"
},
"require_signed_commits": {
"type": "boolean",
"x-go-name": "RequireSignedCommits"
},
"required_approvals": {
"format": "int64",
"type": "integer",
"x-go-name": "RequiredApprovals"
},
"status_check_contexts": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "StatusCheckContexts"
},
"unprotected_file_patterns": {
"type": "string",
"x-go-name": "UnprotectedFilePatterns"
}
},
"type": "object",
"x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
},
"EditOrgOption": {
"description": "EditOrgOption options for editing an organization",
"properties": {
@@ -7900,6 +8239,186 @@
],
"type": "string"
},
"OrgBranchProtection": {
"description": "OrgBranchProtection represents an org-level branch protection ruleset",
"properties": {
"approvals_whitelist_teams": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "ApprovalsWhitelistTeams"
},
"approvals_whitelist_username": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "ApprovalsWhitelistUsernames"
},
"block_admin_merge_override": {
"type": "boolean",
"x-go-name": "BlockAdminMergeOverride"
},
"block_on_official_review_requests": {
"type": "boolean",
"x-go-name": "BlockOnOfficialReviewRequests"
},
"block_on_outdated_branch": {
"type": "boolean",
"x-go-name": "BlockOnOutdatedBranch"
},
"block_on_rejected_reviews": {
"type": "boolean",
"x-go-name": "BlockOnRejectedReviews"
},
"created_at": {
"format": "date-time",
"type": "string",
"x-go-name": "Created"
},
"dismiss_stale_approvals": {
"type": "boolean",
"x-go-name": "DismissStaleApprovals"
},
"enable_approvals_whitelist": {
"type": "boolean",
"x-go-name": "EnableApprovalsWhitelist"
},
"enable_force_push": {
"type": "boolean",
"x-go-name": "EnableForcePush"
},
"enable_force_push_allowlist": {
"type": "boolean",
"x-go-name": "EnableForcePushAllowlist"
},
"enable_merge_whitelist": {
"type": "boolean",
"x-go-name": "EnableMergeWhitelist"
},
"enable_push": {
"type": "boolean",
"x-go-name": "EnablePush"
},
"enable_push_whitelist": {
"type": "boolean",
"x-go-name": "EnablePushWhitelist"
},
"enable_status_check": {
"type": "boolean",
"x-go-name": "EnableStatusCheck"
},
"force_push_allowlist_actions_user": {
"type": "boolean",
"x-go-name": "ForcePushAllowlistActionsUser"
},
"force_push_allowlist_teams": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "ForcePushAllowlistTeams"
},
"force_push_allowlist_usernames": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "ForcePushAllowlistUsernames"
},
"id": {
"format": "int64",
"type": "integer",
"x-go-name": "ID"
},
"ignore_stale_approvals": {
"type": "boolean",
"x-go-name": "IgnoreStaleApprovals"
},
"merge_whitelist_actions_user": {
"type": "boolean",
"x-go-name": "MergeWhitelistActionsUser"
},
"merge_whitelist_teams": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "MergeWhitelistTeams"
},
"merge_whitelist_usernames": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "MergeWhitelistUsernames"
},
"org_id": {
"format": "int64",
"type": "integer",
"x-go-name": "OrgID"
},
"priority": {
"format": "int64",
"type": "integer",
"x-go-name": "Priority"
},
"protected_file_patterns": {
"type": "string",
"x-go-name": "ProtectedFilePatterns"
},
"push_whitelist_actions_user": {
"type": "boolean",
"x-go-name": "PushWhitelistActionsUser"
},
"push_whitelist_teams": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "PushWhitelistTeams"
},
"push_whitelist_usernames": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "PushWhitelistUsernames"
},
"require_signed_commits": {
"type": "boolean",
"x-go-name": "RequireSignedCommits"
},
"required_approvals": {
"format": "int64",
"type": "integer",
"x-go-name": "RequiredApprovals"
},
"rule_name": {
"type": "string",
"x-go-name": "RuleName"
},
"status_check_contexts": {
"items": {
"type": "string"
},
"type": "array",
"x-go-name": "StatusCheckContexts"
},
"unprotected_file_patterns": {
"type": "string",
"x-go-name": "UnprotectedFilePatterns"
},
"updated_at": {
"format": "date-time",
"type": "string",
"x-go-name": "Updated"
}
},
"type": "object",
"x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
},
"Organization": {
"description": "Organization represents an organization",
"properties": {
@@ -13484,6 +14003,196 @@
]
}
},
"/orgs/{org}/branch_protections": {
"get": {
"operationId": "orgListBranchProtections",
"parameters": [
{
"description": "name of the organization",
"in": "path",
"name": "org",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/OrgBranchProtectionList"
},
"404": {
"$ref": "#/components/responses/notFound"
}
},
"summary": "List an organization's branch protection rules",
"tags": [
"organization"
]
},
"post": {
"operationId": "orgCreateBranchProtection",
"parameters": [
{
"description": "name of the organization",
"in": "path",
"name": "org",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateOrgBranchProtectionOption"
}
}
},
"x-originalParamName": "body"
},
"responses": {
"201": {
"$ref": "#/components/responses/OrgBranchProtection"
},
"403": {
"$ref": "#/components/responses/forbidden"
},
"404": {
"$ref": "#/components/responses/notFound"
},
"422": {
"$ref": "#/components/responses/validationError"
}
},
"summary": "Create an org-level branch protection rule",
"tags": [
"organization"
]
}
},
"/orgs/{org}/branch_protections/{name}": {
"delete": {
"operationId": "orgDeleteBranchProtection",
"parameters": [
{
"description": "name of the organization",
"in": "path",
"name": "org",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "name of the branch protection rule",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"$ref": "#/components/responses/empty"
},
"404": {
"$ref": "#/components/responses/notFound"
}
},
"summary": "Delete an org-level branch protection rule",
"tags": [
"organization"
]
},
"get": {
"operationId": "orgGetBranchProtection",
"parameters": [
{
"description": "name of the organization",
"in": "path",
"name": "org",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "name of the branch protection rule",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/OrgBranchProtection"
},
"404": {
"$ref": "#/components/responses/notFound"
}
},
"summary": "Get a specific org-level branch protection rule",
"tags": [
"organization"
]
},
"patch": {
"operationId": "orgEditBranchProtection",
"parameters": [
{
"description": "name of the organization",
"in": "path",
"name": "org",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "name of the branch protection rule",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EditOrgBranchProtectionOption"
}
}
},
"x-originalParamName": "body"
},
"responses": {
"200": {
"$ref": "#/components/responses/OrgBranchProtection"
},
"404": {
"$ref": "#/components/responses/notFound"
},
"422": {
"$ref": "#/components/responses/validationError"
}
},
"summary": "Edit an org-level branch protection rule. Only fields that are set will be changed",
"tags": [
"organization"
]
}
},
"/orgs/{org}/hooks": {
"get": {
"operationId": "orgListHooks",