Compare commits

..

2 Commits

51 changed files with 91 additions and 7591 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: ''
+1 -1
View File
@@ -50,7 +50,7 @@ jobs:
for BRANCH in $BRANCHES; do
# Skip protected branches
case "$BRANCH" in
main|master|develop|release/*|hotfix/*) continue ;;
main|master|dev|develop|rc|beta|alpha|release|release/*|production|stable|staging|hotfix/*|version/*) continue ;;
esac
# Check if branch is merged into main
+13 -23
View File
@@ -52,61 +52,51 @@ jobs:
REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
TAG: ${{ steps.config.outputs.tag }}
run: |
# Inject runner-side values (TAG, REGISTRY_TOKEN) into the remote shell's
# environment via a command prefix, then use a *quoted* heredoc so every
# $var below expands in exactly one place: the remote dev host. This avoids
# the local-vs-remote expansion trap that previously left TAG empty.
HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}'
ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \
-o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
"TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF'
${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash -s <<DEPLOY_EOF
set -e
echo 'SSH connected to dev environment'
if [ -z "$TAG" ]; then
echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2
exit 1
fi
HEALTH_FMT='{{.State.Health.Status}}'
echo 'Cleaning Docker build cache...'
docker builder prune -af 2>/dev/null || true
docker image prune -af 2>/dev/null || true
echo 'Pulling source...'
SOURCE_DIR=/opt/gitea-dev/source
if [ ! -d "$SOURCE_DIR/.git" ]; then
git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git "$SOURCE_DIR"
if [ ! -d \$SOURCE_DIR/.git ]; then
git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git \$SOURCE_DIR
fi
cd "$SOURCE_DIR"
cd \$SOURCE_DIR
git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git 2>/dev/null || true
git fetch origin dev
git reset --hard origin/dev
echo "Building Docker image: ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG"
echo 'Building Docker image...'
docker build --no-cache --build-arg GOFLAGS='-p 1' \
--tag "${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG" \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG \
-f Dockerfile .
echo 'Pushing to registry...'
echo "$REGISTRY_TOKEN" | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
docker push "${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG"
echo '\$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:\$TAG
echo 'Restarting dev container...'
cd /opt/gitea-dev
sed -i "s|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:$TAG|" docker-compose.yml
sed -i "s|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:\$TAG|" docker-compose.yml
docker compose up -d mokogitea-dev
echo 'Health check...'
for i in 1 2 3 4 5 6 7 8; do
sleep 15
if docker inspect --format="$HEALTH_FMT" mokogitea-dev 2>/dev/null | grep -q healthy; then
if docker inspect --format='\$HEALTH_FMT' mokogitea-dev 2>/dev/null | grep -q healthy; then
echo 'Dev container healthy!'
exit 0
fi
echo "Waiting... (attempt $i/8)"
echo "Waiting... (attempt \$i/8)"
done
echo 'Health check failed'
docker logs mokogitea-dev --tail 20
+3 -3
View File
@@ -15,9 +15,9 @@ name: "Universal: Notifications"
on:
workflow_run:
workflows:
- "Joomla Build & Release"
- "Joomla Extension CI"
- "Deploy"
- "Universal: Build & Release"
- "Joomla: Extension CI"
- "Generic: Project CI"
types:
- completed
-11
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,11 +57,6 @@
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
### Fixed
- Fork server binary now compiles: `routers/api/v1/api.go` called `organization.HasOrgOrUserVisible`, which had been renamed to `IsOwnerVisibleToDoer`; the one missed call site broke `go build` of the entire `routers/api/v1` package (CI's Lint & Validate does not run a full build, so it went unnoticed) (#735)
- Dev deploy workflow: the build/deploy step referenced runner-side values as `\$TAG` / `\$REGISTRY_TOKEN` inside an unquoted SSH heredoc, deferring expansion to the remote shell where those names are unset — the Docker tag collapsed to an empty `mokogitea:` and every dev deploy failed with `invalid reference format`. Runner values are now injected via an ssh env-prefix and the heredoc is quoted so each `$var` expands in exactly one place (#737)
- Repaired unit-test compile and `go vet` failures: `CryptoRandomInt/String/Bytes` now return two values (updated `modules/util/util_test.go`), removed a redundant `&&` condition in `issue_comment.go`, and cleaned up isolated integration-test compile errors (#736)
- Removed a stray `package-lock.json` (13.9k lines) that a `git add -A` had accidentally swept into the org-push-policy branch (#734)
- 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
+1 -2
View File
@@ -1,6 +1,6 @@
# MokoGitea
Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cascade merge, security scanning, org-level governance, org metadata, CI standardization, and project board API.
Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cascade merge, security scanning, org metadata, CI standardization, and project board API.
![Language](https://img.shields.io/badge/Go-00ADD8?style=flat-square&logo=go&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square)
@@ -17,7 +17,6 @@ Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cas
- **Default Org Teams** -- auto-create Developers, Reviewers, and CI/CD teams on org creation
- **Org Metadata** -- per-repo metadata API (public GET, admin PUT), platform detection for versioning
- **Branch Protection** -- delete allowlist for protected branches (per-user/team/deploy-key)
- **Org Governance** -- organization-wide rules that layer onto every repository: branch protection as a most-restrictive floor a repo cannot weaken, tag protection (team allowlist), push policy (branch/tag naming, mandatory secret-block, max file size, blocked paths), repository defaults (force-private, PR merge settings), and member email-domain allowlists
- **Project Board API** -- REST endpoints for project columns and cards
- **CI Infrastructure** -- reusable workflows, centralized ci-issue-reporter, standardized MOKOGITEA_TOKEN naming
- **Dev Deploy Gate** -- builds deploy to dev environment first, production checks dev health
-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
}
-6
View File
@@ -33,9 +33,6 @@ type OrgProtectedBranch struct {
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"`
EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"`
StatusCheckContexts []string `xorm:"JSON TEXT"`
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
@@ -99,9 +96,6 @@ func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
CanForcePush: o.CanForcePush,
EnableForcePushAllowlist: o.EnableForcePushAllowlist,
ForcePushAllowlistTeamIDs: o.ForcePushAllowlistTeamIDs,
CanDelete: o.CanDelete,
EnableDeleteAllowlist: o.EnableDeleteAllowlist,
DeleteAllowlistTeamIDs: o.DeleteAllowlistTeamIDs,
EnableStatusCheck: o.EnableStatusCheck,
StatusCheckContexts: o.StatusCheckContexts,
RequiredApprovals: o.RequiredApprovals,
-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
}
-5
View File
@@ -439,11 +439,6 @@ 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),
}
return preparedMigrations
}
-18
View File
@@ -1,18 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
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 {
type OrgProtectedBranch struct {
CanDelete bool `xorm:"NOT NULL DEFAULT false"`
EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"`
DeleteAllowlistTeamIDs []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))
}
-9
View File
@@ -17,9 +17,6 @@ type OrgBranchProtection struct {
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"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
EnableStatusCheck bool `json:"enable_status_check"`
@@ -52,9 +49,6 @@ type CreateOrgBranchProtectionOption struct {
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"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
EnableStatusCheck bool `json:"enable_status_check"`
@@ -82,9 +76,6 @@ type EditOrgBranchProtectionOption struct {
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"`
EnableMergeWhitelist *bool `json:"enable_merge_whitelist"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams"`
EnableStatusCheck *bool `json:"enable_status_check"`
-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"`
}
+10 -18
View File
@@ -86,35 +86,31 @@ func Test_NormalizeEOL(t *testing.T) {
}
func Test_RandomInt(t *testing.T) {
randInt, err := CryptoRandomInt(255)
assert.NoError(t, err)
randInt := CryptoRandomInt(255)
assert.GreaterOrEqual(t, randInt, int64(0))
assert.LessOrEqual(t, randInt, int64(255))
}
func Test_RandomString(t *testing.T) {
str1, err := CryptoRandomString(32)
assert.NoError(t, err)
str1 := CryptoRandomString(32)
var err error
matches, err := regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
assert.NoError(t, err)
assert.True(t, matches)
str2, err := CryptoRandomString(32)
assert.NoError(t, err)
str2 := CryptoRandomString(32)
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
assert.NoError(t, err)
assert.True(t, matches)
assert.NotEqual(t, str1, str2)
str3, err := CryptoRandomString(256)
assert.NoError(t, err)
str3 := CryptoRandomString(256)
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str3)
assert.NoError(t, err)
assert.True(t, matches)
str4, err := CryptoRandomString(256)
assert.NoError(t, err)
str4 := CryptoRandomString(256)
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str4)
assert.NoError(t, err)
assert.True(t, matches)
@@ -123,19 +119,15 @@ func Test_RandomString(t *testing.T) {
}
func Test_RandomBytes(t *testing.T) {
bytes1, err := CryptoRandomBytes(32)
assert.NoError(t, err)
bytes1 := CryptoRandomBytes(32)
bytes2, err := CryptoRandomBytes(32)
assert.NoError(t, err)
bytes2 := CryptoRandomBytes(32)
assert.NotEqual(t, bytes1, bytes2)
bytes3, err := CryptoRandomBytes(256)
assert.NoError(t, err)
bytes3 := CryptoRandomBytes(256)
bytes4, err := CryptoRandomBytes(256)
assert.NoError(t, err)
bytes4 := CryptoRandomBytes(256)
assert.NotEqual(t, bytes3, bytes4)
}
-25
View File
@@ -2411,31 +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.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",
+24 -52
View File
@@ -516,7 +516,7 @@ func reqOrgVisible() func(ctx *context.APIContext) {
ctx.APIErrorInternal(errors.New("reqOrgVisible: unprepared context"))
return
}
if !organization.IsOwnerVisibleToDoer(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound()
return
}
@@ -1698,29 +1698,29 @@ func Routes() *web.Router {
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
})
// m.Group("/projects", func() {
// m.Combo("").Get(repo.ListProjects).
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectOption{}), repo.CreateProject)
// m.Group("/{id}", func() {
// m.Combo("").Get(repo.GetProject).
// Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
// Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
// m.Post("/{action}", reqToken(), reqRepoWriter(unit.TypeProjects), repo.ChangeProjectStatus)
// m.Group("/columns", func() {
// m.Combo("").Get(repo.ListProjectColumns).
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
// m.Group("/{columnId}", func() {
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProjectColumn)
// m.Combo("/issues").Get(repo.ListProjectColumnIssues).
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.AddProjectColumnIssueOption{}), repo.AddIssueToColumn)
// })
// })
// m.Group("/issues/{issueId}", func() {
// m.Patch("/move", reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.MoveProjectColumnIssueOption{}), repo.MoveIssueOnColumn)
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.RemoveIssueFromProject)
// })
// })
// })
// m.Group("/projects", func() {
// m.Combo("").Get(repo.ListProjects).
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectOption{}), repo.CreateProject)
// m.Group("/{id}", func() {
// m.Combo("").Get(repo.GetProject).
// Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
// Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
// m.Post("/{action}", reqToken(), reqRepoWriter(unit.TypeProjects), repo.ChangeProjectStatus)
// m.Group("/columns", func() {
// m.Combo("").Get(repo.ListProjectColumns).
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
// m.Group("/{columnId}", func() {
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProjectColumn)
// m.Combo("/issues").Get(repo.ListProjectColumnIssues).
// Post(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.AddProjectColumnIssueOption{}), repo.AddIssueToColumn)
// })
// })
// m.Group("/issues/{issueId}", func() {
// m.Patch("/move", reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.MoveProjectColumnIssueOption{}), repo.MoveIssueOnColumn)
// m.Delete("", reqToken(), reqRepoWriter(unit.TypeProjects), repo.RemoveIssueFromProject)
// })
// })
// })
// Repo custom fields (repo-scoped key-value metadata)
m.Group("/custom-fields", func() {
m.Get("", repo.GetRepoCustomFields)
@@ -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() {
-23
View File
@@ -47,9 +47,6 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec
EnableForcePush: rule.CanForcePush,
EnableForcePushAllowlist: rule.EnableForcePushAllowlist,
ForcePushAllowlistTeams: resolveTeamNames(rule.ForcePushAllowlistTeamIDs),
EnableDelete: rule.CanDelete,
EnableDeleteAllowlist: rule.EnableDeleteAllowlist,
DeleteAllowlistTeams: resolveTeamNames(rule.DeleteAllowlistTeamIDs),
EnableMergeWhitelist: rule.EnableMergeWhitelist,
MergeWhitelistTeams: resolveTeamNames(rule.MergeWhitelistTeamIDs),
EnableStatusCheck: rule.EnableStatusCheck,
@@ -214,10 +211,6 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
if !ok {
return
}
deleteTeams, ok := resolveTeamIDs(ctx, orgID, form.DeleteAllowlistTeams)
if !ok {
return
}
rule := &git_model.OrgProtectedBranch{
OrgID: orgID,
@@ -229,9 +222,6 @@ func CreateOrgBranchProtection(ctx *context.APIContext) {
CanForcePush: form.EnablePush && form.EnableForcePush,
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
ForcePushAllowlistTeamIDs: forcePushTeams,
CanDelete: form.EnableDelete,
EnableDeleteAllowlist: form.EnableDelete && form.EnableDeleteAllowlist,
DeleteAllowlistTeamIDs: deleteTeams,
EnableMergeWhitelist: form.EnableMergeWhitelist,
MergeWhitelistTeamIDs: mergeTeams,
EnableStatusCheck: form.EnableStatusCheck,
@@ -333,19 +323,6 @@ 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 !ok {
return
}
rule.DeleteAllowlistTeamIDs = ids
}
if form.EnableMergeWhitelist != nil {
rule.EnableMergeWhitelist = *form.EnableMergeWhitelist
}
-124
View File
@@ -1,124 +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) {
// swagger:operation GET /orgs/{org}/email_domain_policy organization orgGetEmailDomainPolicy
// ---
// summary: Get the organization's email domain policy
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgEmailDomainPolicy"
// "404":
// "$ref": "#/responses/notFound"
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) {
// swagger:operation PATCH /orgs/{org}/email_domain_policy organization orgEditEmailDomainPolicy
// ---
// summary: Create or update the organization's email domain policy. Only fields that are set will be changed
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditOrgEmailDomainPolicyOption"
// responses:
// "200":
// "$ref": "#/responses/OrgEmailDomainPolicy"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
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) {
// swagger:operation DELETE /orgs/{org}/email_domain_policy organization orgDeleteEmailDomainPolicy
// ---
// summary: Remove the organization's email domain policy
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
if err := git_model.DeleteOrgEmailDomainPolicy(ctx, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
-145
View File
@@ -1,145 +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) {
// swagger:operation GET /orgs/{org}/push_policy organization orgGetPushPolicy
// ---
// summary: Get the organization's push policy
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgPushPolicy"
// "404":
// "$ref": "#/responses/notFound"
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) {
// swagger:operation PATCH /orgs/{org}/push_policy organization orgEditPushPolicy
// ---
// summary: Create or update the organization's push policy. Only fields that are set will be changed
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditOrgPushPolicyOption"
// responses:
// "200":
// "$ref": "#/responses/OrgPushPolicy"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
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) {
// swagger:operation DELETE /orgs/{org}/push_policy organization orgDeletePushPolicy
// ---
// summary: Remove the organization's push policy
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
if err := git_model.DeleteOrgPushPolicy(ctx, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
-174
View File
@@ -1,174 +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) {
// swagger:operation GET /orgs/{org}/repo_defaults organization orgGetRepoDefaults
// ---
// summary: Get the organization's default repository settings
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgRepoDefaults"
// "404":
// "$ref": "#/responses/notFound"
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) {
// swagger:operation PATCH /orgs/{org}/repo_defaults organization orgEditRepoDefaults
// ---
// summary: Create or update the organization's default repository settings. Only fields that are set will be changed
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditOrgRepoDefaultsOption"
// responses:
// "200":
// "$ref": "#/responses/OrgRepoDefaults"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
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) {
// swagger:operation DELETE /orgs/{org}/repo_defaults organization orgDeleteRepoDefaults
// ---
// summary: Remove the organization's default repository settings
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
if err := git_model.DeleteOrgRepoDefaults(ctx, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
-271
View File
@@ -1,271 +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) {
// swagger:operation GET /orgs/{org}/tag_protections organization orgListTagProtections
// ---
// summary: List an organization's tag protection rules
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgTagProtectionList"
// "404":
// "$ref": "#/responses/notFound"
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) {
// swagger:operation GET /orgs/{org}/tag_protections/{id} organization orgGetTagProtection
// ---
// summary: Get a specific org-level tag protection rule
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the tag protection rule
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgTagProtection"
// "404":
// "$ref": "#/responses/notFound"
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) {
// swagger:operation POST /orgs/{org}/tag_protections organization orgCreateTagProtection
// ---
// summary: Create an org-level tag protection rule
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateOrgTagProtectionOption"
// responses:
// "201":
// "$ref": "#/responses/OrgTagProtection"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
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) {
// swagger:operation PATCH /orgs/{org}/tag_protections/{id} organization orgEditTagProtection
// ---
// summary: Edit an org-level tag protection rule. Only fields that are set will be changed
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the tag protection rule
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditOrgTagProtectionOption"
// responses:
// "200":
// "$ref": "#/responses/OrgTagProtection"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
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) {
// swagger:operation DELETE /orgs/{org}/tag_protections/{id} organization orgDeleteTagProtection
// ---
// summary: Delete an org-level tag protection rule
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the tag protection rule
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
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)
}
+9 -39
View File
@@ -30,7 +30,7 @@ type apiMetadata struct {
TargetVersion string `json:"target_version"`
PHPMinimum string `json:"php_minimum"`
Language string `json:"language"`
ExtensionType string `json:"extension_type"`
ExtensionType string `json:"extension_type"`
EntryPoint string `json:"entry_point"`
// deploy
@@ -44,13 +44,6 @@ type apiMetadata struct {
HealthURL string `json:"health_url,omitempty"`
}
// Manifest
// swagger:response Manifest
type swaggerResponseManifest struct {
// in:body
Body apiMetadata `json:"body"`
}
// GetRepoMetadata returns the manifest settings for a repository.
func GetRepoMetadata(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/manifest repository repoGetManifest
@@ -58,17 +51,6 @@ func GetRepoMetadata(ctx *context.APIContext) {
// summary: Get repo manifest settings
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Manifest"
@@ -89,9 +71,9 @@ func GetRepoMetadata(ctx *context.APIContext) {
return
}
ctx.JSON(http.StatusOK, &apiMetadata{
Name: m.Name,
Org: m.Org,
Description: m.Description,
Name: m.Name,
Org: m.Org,
Description: m.Description,
LicenseSPDX: m.LicenseSPDX,
LicenseName: m.LicenseName,
@@ -107,7 +89,7 @@ func GetRepoMetadata(ctx *context.APIContext) {
TargetVersion: m.TargetVersion,
PHPMinimum: m.PHPMinimum,
Language: m.Language,
ExtensionType: m.ExtensionType,
ExtensionType: m.ExtensionType,
EntryPoint: m.EntryPoint,
DeployHost: m.DeployHost,
DeployPort: m.DeployPort,
@@ -129,21 +111,9 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Manifest"
// Decode into a map to detect which fields were actually sent.
var raw map[string]any
if err := json.NewDecoder(ctx.Req.Body).Decode(&raw); err != nil {
@@ -203,9 +173,9 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
}
ctx.JSON(http.StatusOK, &apiMetadata{
Name: m.Name,
Org: m.Org,
Description: m.Description,
Name: m.Name,
Org: m.Org,
Description: m.Description,
LicenseSPDX: m.LicenseSPDX,
LicenseName: m.LicenseName,
@@ -221,7 +191,7 @@ func UpdateRepoMetadata(ctx *context.APIContext) {
TargetVersion: m.TargetVersion,
PHPMinimum: m.PHPMinimum,
Language: m.Language,
ExtensionType: m.ExtensionType,
ExtensionType: m.ExtensionType,
EntryPoint: m.EntryPoint,
DeployHost: m.DeployHost,
DeployPort: m.DeployPort,
-38
View File
@@ -159,44 +159,6 @@ type swaggerParameterBodies struct {
// in:body
UpdateBranchProtectionPriories api.UpdateBranchProtectionPriories
// in:body
CreateOrgBranchProtectionOption api.CreateOrgBranchProtectionOption
// in:body
EditOrgBranchProtectionOption api.EditOrgBranchProtectionOption
// in:body
CreateOrgTagProtectionOption api.CreateOrgTagProtectionOption
// in:body
EditOrgTagProtectionOption api.EditOrgTagProtectionOption
// in:body
EditOrgPushPolicyOption api.EditOrgPushPolicyOption
// in:body
EditOrgRepoDefaultsOption api.EditOrgRepoDefaultsOption
// in:body
EditOrgEmailDomainPolicyOption api.EditOrgEmailDomainPolicyOption
// in:body
EditAccessTokenOption api.EditAccessTokenOption
// in:body
IssueBulkAssigneesOption api.IssueBulkAssigneesOption
// in:body
IssueBulkLabelsOption api.IssueBulkLabelsOption
// in:body
IssueBulkMilestoneOption api.IssueBulkMilestoneOption
// in:body
IssueBulkStateOption api.IssueBulkStateOption
// in:body
IssuePriorityDef api.IssuePriorityDef
// in:body
IssueStatusDef api.IssueStatusDef
// in:body
IssueTypeDef api.IssueTypeDef
// in:body
CreateOAuth2ApplicationOptions api.CreateOAuth2ApplicationOptions
-49
View File
@@ -41,52 +41,3 @@ 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"`
}
// OrgTagProtection
// swagger:response OrgTagProtection
type swaggerResponseOrgTagProtection struct {
// in:body
Body api.OrgTagProtection `json:"body"`
}
// OrgTagProtectionList
// swagger:response OrgTagProtectionList
type swaggerResponseOrgTagProtectionList struct {
// in:body
Body []*api.OrgTagProtection `json:"body"`
}
// OrgPushPolicy
// swagger:response OrgPushPolicy
type swaggerResponseOrgPushPolicy struct {
// in:body
Body api.OrgPushPolicy `json:"body"`
}
// OrgRepoDefaults
// swagger:response OrgRepoDefaults
type swaggerResponseOrgRepoDefaults struct {
// in:body
Body api.OrgRepoDefaults `json:"body"`
}
// OrgEmailDomainPolicy
// swagger:response OrgEmailDomainPolicy
type swaggerResponseOrgEmailDomainPolicy struct {
// in:body
Body api.OrgEmailDomainPolicy `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 -1
View File
@@ -185,7 +185,7 @@ func NewComment(ctx *context.Context) {
} // end if: handle close or reopen
// Handle custom status from the status dropdown (replaces close button for issues with org statuses).
if statusIDStr := ctx.Req.FormValue("status_id"); statusIDStr != "" {
if statusIDStr := ctx.Req.FormValue("status_id"); statusIDStr != "" && statusIDStr != "" {
if statusIDStr == "reopen" {
// Reopen via dropdown
if issue.IsClosed {
@@ -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,16 +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
}
}
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
}
+3 -3
View File
@@ -11,8 +11,8 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
@@ -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) {
-82
View File
@@ -62,88 +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}}
{{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>
+2 -2556
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10 -11
View File
@@ -6,7 +6,6 @@ package integration
import (
"fmt"
"net/http"
"strings"
"testing"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
@@ -37,7 +36,7 @@ func TestAPILicensePackages(t *testing.T) {
t.Run("CreatePackage", func(t *testing.T) {
body := `{"name":"Test Pro Annual","description":"Annual pro subscription","duration_days":365,"max_sites":5}`
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusCreated)
@@ -52,7 +51,7 @@ func TestAPILicensePackages(t *testing.T) {
t.Run("CreatePackageNoName", func(t *testing.T) {
body := `{"description":"Missing name"}`
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
MakeRequest(t, req, http.StatusUnprocessableEntity)
@@ -69,7 +68,7 @@ func TestAPILicenseKeys(t *testing.T) {
// Create a package first.
body := `{"name":"Test Package","duration_days":30}`
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusCreated)
@@ -81,7 +80,7 @@ func TestAPILicenseKeys(t *testing.T) {
t.Run("CreateKey", func(t *testing.T) {
body := fmt.Sprintf(`{"package_id":%d,"licensee_name":"John Doe","licensee_email":"john@example.com"}`, pkg.ID)
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys", strings.NewReader(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys", []byte(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusCreated)
@@ -105,7 +104,7 @@ func TestAPILicenseKeys(t *testing.T) {
t.Run("EditKey", func(t *testing.T) {
body := `{"licensee_name":"Jane Doe","domain_restriction":"example.com,test.com"}`
req := NewRequestWithBody(t, "PATCH", fmt.Sprintf("%s/license-keys/%d", urlPrefix, createdKeyID), strings.NewReader(body)).
req := NewRequestWithBody(t, "PATCH", fmt.Sprintf("%s/license-keys/%d", urlPrefix, createdKeyID), []byte(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusOK)
@@ -125,7 +124,7 @@ func TestAPILicenseKeys(t *testing.T) {
t.Run("ValidateKey", func(t *testing.T) {
body := fmt.Sprintf(`{"key":"%s","domain":"example.com"}`, rawKey)
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", strings.NewReader(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", []byte(body)).
SetHeader("Content-Type", "application/json")
// Note: no token — this is a public endpoint.
resp := MakeRequest(t, req, http.StatusOK)
@@ -137,7 +136,7 @@ func TestAPILicenseKeys(t *testing.T) {
t.Run("ValidateInvalidKey", func(t *testing.T) {
body := `{"key":"MOKO-XXXX-XXXX-XXXX-XXXX","domain":"example.com"}`
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", strings.NewReader(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", []byte(body)).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusOK)
var result api.ValidateLicenseKeyResponse
@@ -162,7 +161,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
// Create a package.
body := `{"name":"Purchase Test","duration_days":90}`
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusCreated)
@@ -171,7 +170,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
t.Run("PurchaseNewKey", func(t *testing.T) {
body := fmt.Sprintf(`{"package_id":%d,"licensee_name":"Buyer","licensee_email":"buyer@shop.com","domain":"shop.com","payment_ref":"stripe_pi_test123"}`, pkg.ID)
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", strings.NewReader(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", []byte(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusCreated)
@@ -184,7 +183,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) {
t.Run("PurchaseIdempotent", func(t *testing.T) {
// Same payment_ref should return existing key without raw_key.
body := fmt.Sprintf(`{"package_id":%d,"licensee_name":"Buyer","payment_ref":"stripe_pi_test123"}`, pkg.ID)
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", strings.NewReader(body)).
req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", []byte(body)).
AddTokenAuth(token).
SetHeader("Content-Type", "application/json")
resp := MakeRequest(t, req, http.StatusOK)
+2 -2
View File
@@ -253,7 +253,7 @@ func TestOAuth2CallbackReactivationGating(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
defer test.MockVariableValue(&setting.OAuth2Client.Username, setting.OAuth2UsernameUserid)()
srv := newFakeOIDCServerWithConfig(t, FakeOIDCConfig{Sub: "test-sub", Email: "test@example.com", Name: "Test User"})
srv := newFakeOIDCServer(t, FakeOIDCConfig{Sub: "test-sub", Email: "test@example.com", Name: "Test User"})
addOAuth2Source(t, "test-oauth-source", oauth2.Source{
Provider: "openidConnect",
ClientID: "test-client-id",
@@ -308,7 +308,7 @@ type FakeOIDCConfig struct {
}
// newFakeOIDCServer starts a httptest.Server that implements the minimum OIDC endpoints needed to complete a sign-in flow
func newFakeOIDCServerWithConfig(t *testing.T, cfg FakeOIDCConfig) *httptest.Server {
func newFakeOIDCServer(t *testing.T, cfg FakeOIDCConfig) *httptest.Server {
t.Helper()
var srv *httptest.Server