diff --git a/.mokogitea/ISSUE_TEMPLATE/feature_request.md b/.mokogitea/ISSUE_TEMPLATE/feature_request.md index 8036b08168..58097e3975 100644 --- a/.mokogitea/ISSUE_TEMPLATE/feature_request.md +++ b/.mokogitea/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature Request about: Suggest a new feature or enhancement -title: '[FEATURE] ' +title: '(feat) ' labels: 'enhancement' assignees: '' diff --git a/.mokogitea/workflows/ci-generic.yml b/.mokogitea/workflows/ci-generic.yml index 72650d2430..57ef131bd7 100644 --- a/.mokogitea/workflows/ci-generic.yml +++ b/.mokogitea/workflows/ci-generic.yml @@ -131,10 +131,11 @@ jobs: test: name: Tests runs-on: ubuntu-latest - needs: lint - # Run only when lint succeeded; always() forces evaluation so a skipped - # lint (e.g. template repos) skips this job cleanly instead of hanging. - if: ${{ always() && needs.lint.result == 'success' }} + # Independent job (no `needs: lint`): the Gitea Actions scheduler does not + # offer the dependent 2nd job of a needs-chain to runners, so it stalls in + # "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos + # directly (same condition lint uses) instead of gating on lint's result. + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} steps: - name: Checkout diff --git a/.mokogitea/workflows/custom/deploy-dev.yml b/.mokogitea/workflows/custom/deploy-dev.yml index 84159835f7..6e4361c608 100644 --- a/.mokogitea/workflows/custom/deploy-dev.yml +++ b/.mokogitea/workflows/custom/deploy-dev.yml @@ -52,51 +52,69 @@ jobs: REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} TAG: ${{ steps.config.outputs.tag }} run: | - HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}' - + # 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. 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 }} bash -s <&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...' + echo "Building Docker image: ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG" 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 - docker compose up -d mokogitea-dev + # The dev service in the SHARED compose file reads its image tag from + # ${MOKOGITEA_DEV_TAG}. Drive it from the freshly built tag instead of + # rewriting the file with sed: the old sed pattern matched the *prod* + # service line (container_name: mokogitea) and left the dev service pinned + # to a stale image, so every dev deploy recreated old code while silently + # corrupting the prod image pin. The env-var only affects the dev service. + # Remove any lingering fixed-name container first so the recreate can't hit + # a name conflict, pin the project name for determinism, then force-recreate. + docker rm -f mokogitea-dev 2>/dev/null || true + MOKOGITEA_DEV_TAG="$TAG" docker compose -p gitea-dev up -d --force-recreate 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 diff --git a/.mokogitea/workflows/deploy-rc.yml b/.mokogitea/workflows/deploy-rc.yml new file mode 100644 index 0000000000..42b35de3a8 --- /dev/null +++ b/.mokogitea/workflows/deploy-rc.yml @@ -0,0 +1,137 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# BRIEF: Build and deploy to the RC environment on push to the rc branch. +# Portable across Go server repos: ALL deployment config comes from repo +# Actions variables (vars.*) and secrets (secrets.*), nothing hardcoded. +# The rc branch is created by promote-rc when a PR to main opens. +# OWNER: Template-Go (canonical source; syncs to the root workflows dir). See Template-Go#3. +# +# Required repo VARIABLES (all tier-scoped so each environment is independent — +# a repo may host its rc/dev/prod tiers on different machines): +# RC_SSH_HOST, RC_SSH_PORT, RC_SSH_USERNAME - SSH deploy target for the rc tier +# RC_REGISTRY, RC_REGISTRY_USER, RC_IMAGE - container registry + login user + image +# RC_CONTAINER - compose service/container to recreate +# RC_COMPOSE_PROJECT - docker compose -p project name +# RC_COMPOSE_DIR - dir containing docker-compose.yml on host +# RC_SOURCE_DIR - build source checkout on host +# RC_TAG_ENV - compose env-var name that pins the image tag +# RC_HEALTH_URL - external URL to verify after deploy +# Required SECRETS (already configured org-wide; reused, not re-set): +# DEPLOY_SSH_KEY - deploy private key (repo/org secret) +# MOKOGITEA_TOKEN - registry/API token (org secret) + +name: Deploy (RC) + +on: + push: + branches: + - rc + # Manual trigger for isolated end-to-end tests without a full RC promotion. + # Runs on the ref it is dispatched from; that ref must carry current source + # (>= the RC database migration version) or the rebuilt image will refuse the + # newer DB. Dispatch from `rc` once `rc` is current. + workflow_dispatch: + +concurrency: + group: deploy-rc + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + deploy-rc: + name: "Build & Deploy to RC" + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine version + id: config + run: | + VERSION=$(git describe --tags --always 2>/dev/null || echo "rc-$(git rev-parse --short HEAD)") + echo "tag=${VERSION}-rc" >> $GITHUB_OUTPUT + echo "Version: ${VERSION}-rc" + + - name: Write deploy key + env: + DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + run: | + mkdir -p ~/.ssh + echo "$DEPLOY_KEY" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + + - name: Build and deploy to RC via SSH + env: + REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + TAG: ${{ steps.config.outputs.tag }} + run: | + # Runner-side values (TAG, REGISTRY_TOKEN) are injected into the remote shell + # via an env prefix; a *quoted* heredoc keeps every $var expanding once, on the + # remote. Repo variables (vars.*) are substituted inline by Actions before ssh. + ssh -i ~/.ssh/deploy_key -p ${{ vars.RC_SSH_PORT }} \ + -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \ + ${{ vars.RC_SSH_USERNAME }}@${{ vars.RC_SSH_HOST }} \ + "TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF' + set -e + echo 'SSH connected to RC 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='${{ vars.RC_SOURCE_DIR }}' + if [ ! -d "$SOURCE_DIR/.git" ]; then + git clone -b rc ${{ github.server_url }}/${{ github.repository }}.git "$SOURCE_DIR" + fi + cd "$SOURCE_DIR" + git remote set-url origin ${{ github.server_url }}/${{ github.repository }}.git 2>/dev/null || true + git fetch origin rc + git reset --hard origin/rc + + echo "Building image: ${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG" + docker build --no-cache --build-arg GOFLAGS='-p 1' \ + --tag "${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG" \ + -f Dockerfile . + + echo 'Pushing to registry...' + echo "$REGISTRY_TOKEN" | docker login ${{ vars.RC_REGISTRY }} -u ${{ vars.RC_REGISTRY_USER }} --password-stdin + docker push "${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG" + + echo 'Restarting RC container...' + cd '${{ vars.RC_COMPOSE_DIR }}' + # Drive the rc service image tag via its compose env-var (no sed on the shared + # file); remove any lingering fixed-name container first, then force-recreate. + docker rm -f '${{ vars.RC_CONTAINER }}' 2>/dev/null || true + ${{ vars.RC_TAG_ENV }}="$TAG" docker compose -p '${{ vars.RC_COMPOSE_PROJECT }}' up -d --force-recreate '${{ vars.RC_CONTAINER }}' + + echo 'Health check...' + for i in 1 2 3 4 5 6 7 8; do + sleep 15 + if docker inspect --format="$HEALTH_FMT" '${{ vars.RC_CONTAINER }}' 2>/dev/null | grep -q healthy; then + echo 'RC container healthy!' + exit 0 + fi + echo "Waiting... (attempt $i/8)" + done + echo 'Health check failed' + docker logs '${{ vars.RC_CONTAINER }}' --tail 20 + exit 1 + DEPLOY_EOF + + - name: Verify RC instance + run: | + sleep 5 + curl -sf "${{ vars.RC_HEALTH_URL }}" && echo " RC API healthy" diff --git a/CHANGELOG.md b/CHANGELOG.md index e45eaa9105..ccb77732ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [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) @@ -57,6 +63,11 @@ - 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 diff --git a/README.md b/README.md index 1aba6495e5..a33161e715 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoGitea -Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cascade merge, security scanning, org metadata, CI standardization, and project board API. +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. ![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,6 +17,7 @@ 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 diff --git a/models/git/org_email_domain.go b/models/git/org_email_domain.go new file mode 100644 index 0000000000..9984d5d60b --- /dev/null +++ b/models/git/org_email_domain.go @@ -0,0 +1,134 @@ +// Copyright 2026 Moko Consulting +// 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 +} diff --git a/models/git/org_protected_branch.go b/models/git/org_protected_branch.go index 99c09e6e03..da7b5039d0 100644 --- a/models/git/org_protected_branch.go +++ b/models/git/org_protected_branch.go @@ -28,16 +28,28 @@ type OrgProtectedBranch struct { CanPush bool `xorm:"NOT NULL DEFAULT false"` EnableWhitelist bool `xorm:"NOT NULL DEFAULT false"` WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + WhitelistUserIDs []int64 `xorm:"JSON TEXT"` + WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"` EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"` MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"` + MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"` CanForcePush bool `xorm:"NOT NULL DEFAULT false"` EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"` ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` + ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"` + ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + CanDelete bool `xorm:"NOT NULL DEFAULT false"` + EnableDeleteAllowlist bool `xorm:"NOT NULL DEFAULT false"` + DeleteAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` + DeleteAllowlistUserIDs []int64 `xorm:"JSON TEXT"` + DeleteAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"` EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"` StatusCheckContexts []string `xorm:"JSON TEXT"` RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"` EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"` ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"` BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"` BlockOnOfficialReviewRequests bool `xorm:"NOT NULL DEFAULT false"` BlockOnOutdatedBranch bool `xorm:"NOT NULL DEFAULT false"` @@ -81,8 +93,9 @@ func (o *OrgProtectedBranch) Match(branchName string) bool { } // ToProtectedBranch converts an org-level rule to a ProtectedBranch for use -// in the standard enforcement path. Fields that are user-scoped (WhitelistUserIDs etc.) -// are left empty because org rules only reference teams. +// in the standard enforcement path. Both team-scoped and user-scoped allowlists +// (plus the Actions-bot toggles) are carried across; deploy-key allowances remain +// repo-only because an org rule cannot express them. func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch { return &ProtectedBranch{ ID: o.ID, @@ -91,16 +104,28 @@ func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch { CanPush: o.CanPush, EnableWhitelist: o.EnableWhitelist, WhitelistTeamIDs: o.WhitelistTeamIDs, + WhitelistUserIDs: o.WhitelistUserIDs, + WhitelistActionsUser: o.WhitelistActionsUser, EnableMergeWhitelist: o.EnableMergeWhitelist, MergeWhitelistTeamIDs: o.MergeWhitelistTeamIDs, + MergeWhitelistUserIDs: o.MergeWhitelistUserIDs, + MergeWhitelistActionsUser: o.MergeWhitelistActionsUser, CanForcePush: o.CanForcePush, EnableForcePushAllowlist: o.EnableForcePushAllowlist, ForcePushAllowlistTeamIDs: o.ForcePushAllowlistTeamIDs, + ForcePushAllowlistUserIDs: o.ForcePushAllowlistUserIDs, + ForcePushAllowlistActionsUser: o.ForcePushAllowlistActionsUser, + CanDelete: o.CanDelete, + EnableDeleteAllowlist: o.EnableDeleteAllowlist, + DeleteAllowlistTeamIDs: o.DeleteAllowlistTeamIDs, + DeleteAllowlistUserIDs: o.DeleteAllowlistUserIDs, + DeleteAllowlistActionsUser: o.DeleteAllowlistActionsUser, EnableStatusCheck: o.EnableStatusCheck, StatusCheckContexts: o.StatusCheckContexts, RequiredApprovals: o.RequiredApprovals, EnableApprovalsWhitelist: o.EnableApprovalsWhitelist, ApprovalsWhitelistTeamIDs: o.ApprovalsWhitelistTeamIDs, + ApprovalsWhitelistUserIDs: o.ApprovalsWhitelistUserIDs, BlockOnRejectedReviews: o.BlockOnRejectedReviews, BlockOnOfficialReviewRequests: o.BlockOnOfficialReviewRequests, BlockOnOutdatedBranch: o.BlockOnOutdatedBranch, diff --git a/models/git/org_protected_tag.go b/models/git/org_protected_tag.go new file mode 100644 index 0000000000..0ba6c23526 --- /dev/null +++ b/models/git/org_protected_tag.go @@ -0,0 +1,133 @@ +// Copyright 2026 Moko Consulting +// 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) +} diff --git a/models/git/org_push_policy.go b/models/git/org_push_policy.go new file mode 100644 index 0000000000..356c002940 --- /dev/null +++ b/models/git/org_push_policy.go @@ -0,0 +1,130 @@ +// Copyright 2026 Moko Consulting +// 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 +} diff --git a/models/git/org_repo_defaults.go b/models/git/org_repo_defaults.go new file mode 100644 index 0000000000..7a4d831ce0 --- /dev/null +++ b/models/git/org_repo_defaults.go @@ -0,0 +1,88 @@ +// Copyright 2026 Moko Consulting +// 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 +} diff --git a/models/git/protected_branch_list.go b/models/git/protected_branch_list.go index 4875857db6..8528177335 100644 --- a/models/git/protected_branch_list.go +++ b/models/git/protected_branch_list.go @@ -85,19 +85,40 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string) return results, 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). +// 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. func GetFirstMatchProtectedBranchRule(ctx context.Context, repoID int64, branchName string) (*ProtectedBranch, error) { rules, err := FindRepoProtectedBranchRules(ctx, repoID) if err != nil { return nil, err } - if matched := rules.GetFirstMatched(branchName); matched != nil { - return matched, nil + repoRule := rules.GetFirstMatched(branchName) + + orgRule, err := getFirstMatchOrgProtectedBranchRule(ctx, repoID, branchName) + if err != nil { + return nil, err } - // Fall back to org-level rules + 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) { repo, err := repo_model.GetRepositoryByID(ctx, repoID) if err != nil { return nil, err @@ -112,14 +133,16 @@ func GetFirstMatchProtectedBranchRule(ctx context.Context, repoID int64, branchN orgRule, err := FindOrgBranchRuleForBranch(ctx, owner.ID, branchName) if err != nil { + // Fail closed: propagate the error so callers keep the org floor in force + // rather than silently falling back to the repo rule on a transient error. log.Error("FindOrgBranchRuleForBranch: %v", err) - return nil, nil + return nil, err } if orgRule == nil { 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 diff --git a/models/git/protected_branch_merge.go b/models/git/protected_branch_merge.go new file mode 100644 index 0000000000..103b23350b --- /dev/null +++ b/models/git/protected_branch_merge.go @@ -0,0 +1,202 @@ +// Copyright 2026 Moko Consulting +// 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) + // Deploy-key allowances are still not expressible in an OrgProtectedBranch, so the + // org rule imposes no constraint on them; carry the repo value through unchanged. + // ANDing against the org side's always-false zero value would silently lock out + // every deploy-key push in any org that has a matching branch rule (see #727 review). + // The Actions-bot toggle IS now expressible org-side, so merge it most-restrictively. + eff.WhitelistDeployKeys = repoRule.WhitelistDeployKeys + eff.WhitelistActionsUser = mergeAllowFlag(repoRule.EnableWhitelist, repoRule.WhitelistActionsUser, orgRule.EnableWhitelist, 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 + eff.ForcePushAllowlistActionsUser = mergeAllowFlag(repoRule.EnableForcePushAllowlist, repoRule.ForcePushAllowlistActionsUser, orgRule.EnableForcePushAllowlist, 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 + eff.DeleteAllowlistActionsUser = mergeAllowFlag(repoRule.EnableDeleteAllowlist, repoRule.DeleteAllowlistActionsUser, orgRule.EnableDeleteAllowlist, 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 = mergeAllowFlag(repoRule.EnableMergeWhitelist, repoRule.MergeWhitelistActionsUser, orgRule.EnableMergeWhitelist, 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 + } +} + +// mergeAllowFlag combines two "allow the Actions bot" toggles under most-restrictive +// semantics, mirroring mergeAllowlist. A toggle only grants access when its Enable flag +// is set; a disabled allowlist means "everyone", so it imposes no constraint. Therefore: +// if both allowlists are enabled the bot is allowed only when BOTH sides allow it; if +// only one is enabled that side's flag governs; if neither is enabled the flag is +// irrelevant (fall back to the repo/a value). +func mergeAllowFlag(aEnabled, aFlag, bEnabled, bFlag bool) bool { + switch { + case aEnabled && bEnabled: + return aFlag && bFlag + case aEnabled: + return aFlag + case bEnabled: + return bFlag + default: + return aFlag + } +} + +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 +} diff --git a/models/git/protected_branch_merge_test.go b/models/git/protected_branch_merge_test.go new file mode 100644 index 0000000000..716f3a6174 --- /dev/null +++ b/models/git/protected_branch_merge_test.go @@ -0,0 +1,100 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package git + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMergeAllowFlag(t *testing.T) { + cases := []struct { + name string + aEnabled bool + aFlag bool + bEnabled bool + bFlag bool + expected bool + }{ + // Both allowlists enabled: bot allowed only when BOTH allow it. + {"both enabled, both allow", true, true, true, true, true}, + {"both enabled, a denies", true, false, true, true, false}, + {"both enabled, b denies", true, true, true, false, false}, + {"both enabled, both deny", true, false, true, false, false}, + // Only one side enabled: that side governs. + {"only a enabled, a allows", true, true, false, false, true}, + {"only a enabled, a denies", true, false, false, true, false}, + {"only b enabled, b allows", false, false, true, true, true}, + {"only b enabled, b denies", false, true, true, false, false}, + // Neither enabled: flag irrelevant, falls back to a's flag. + {"neither enabled, a true", false, true, false, false, true}, + {"neither enabled, a false", false, false, false, true, false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + assert.Equal(t, c.expected, mergeAllowFlag(c.aEnabled, c.aFlag, c.bEnabled, c.bFlag)) + }) + } +} + +func TestMergeMostRestrictiveUserIDsIntersect(t *testing.T) { + repoRule := &ProtectedBranch{ + EnableWhitelist: true, + WhitelistUserIDs: []int64{1, 2, 3}, + // Deploy keys are repo-only and must always pass through unchanged. + WhitelistDeployKeys: true, + ForcePushAllowlistDeployKeys: true, + DeleteAllowlistDeployKeys: true, + } + orgRule := &ProtectedBranch{ + EnableWhitelist: true, + WhitelistUserIDs: []int64{2, 3, 4}, + // Org rule cannot express deploy keys; these zero values must NOT override repo. + } + + eff := mergeMostRestrictive(repoRule, orgRule) + + // User IDs are intersected when both allowlists are enabled. + assert.True(t, eff.EnableWhitelist) + assert.ElementsMatch(t, []int64{2, 3}, eff.WhitelistUserIDs) + + // Deploy-key allowances still pass through from the repo rule. + assert.True(t, eff.WhitelistDeployKeys) + assert.True(t, eff.ForcePushAllowlistDeployKeys) + assert.True(t, eff.DeleteAllowlistDeployKeys) +} + +func TestMergeMostRestrictiveActionsUserFlags(t *testing.T) { + // Both sides enable their allowlist and both allow the bot -> allowed. + repoRule := &ProtectedBranch{ + EnableWhitelist: true, + WhitelistActionsUser: true, + EnableForcePushAllowlist: true, + // force-push: org denies -> effective deny. + ForcePushAllowlistActionsUser: true, + EnableDeleteAllowlist: true, + DeleteAllowlistActionsUser: true, + // merge: repo allowlist disabled, org enabled+denies -> org governs (deny). + EnableMergeWhitelist: false, + MergeWhitelistActionsUser: true, + } + orgRule := &ProtectedBranch{ + EnableWhitelist: true, + WhitelistActionsUser: true, + EnableForcePushAllowlist: true, + ForcePushAllowlistActionsUser: false, + EnableDeleteAllowlist: true, + DeleteAllowlistActionsUser: true, + EnableMergeWhitelist: true, + MergeWhitelistActionsUser: false, + } + + eff := mergeMostRestrictive(repoRule, orgRule) + + assert.True(t, eff.WhitelistActionsUser, "push: both allow") + assert.False(t, eff.ForcePushAllowlistActionsUser, "force-push: org denies") + assert.True(t, eff.DeleteAllowlistActionsUser, "delete: both allow") + assert.False(t, eff.MergeWhitelistActionsUser, "merge: org enabled and denies") +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 557a1ede75..24ff05cc46 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -439,6 +439,12 @@ func prepareMigrationTasks() []*migration { newMigration(359, "Add deploy fields to repo manifest", v1_27.AddDeployFieldsToRepoManifest), newMigration(360, "Add delete allowlist to protected branch", v1_27.AddDeleteAllowlistToProtectedBranch), newMigration(361, "Add cascade merge rule table", v1_27.AddCascadeMergeRuleTable), + newMigration(362, "Add delete allowlist to org protected branch", v1_27.AddDeleteAllowlistToOrgProtectedBranch), + newMigration(363, "Add org protected tag table", v1_27.AddOrgProtectedTagTable), + newMigration(364, "Add org push policy table", v1_27.AddOrgPushPolicyTable), + newMigration(365, "Add org repo defaults table", v1_27.AddOrgRepoDefaultsTable), + newMigration(366, "Add org email domain policy table", v1_27.AddOrgEmailDomainPolicyTable), + newMigration(367, "Add user allowlists to org protected branch", v1_27.AddUserAllowlistsToOrgProtectedBranch), } return preparedMigrations } diff --git a/models/migrations/v1_27/v347.go b/models/migrations/v1_27/v347.go index 44422ef710..483154fcbc 100644 --- a/models/migrations/v1_27/v347.go +++ b/models/migrations/v1_27/v347.go @@ -8,7 +8,7 @@ import ( ) // AddRepoManifestTable creates the repo_manifest table for storing -// mokoplatform manifest settings per repository. +// mokocli manifest settings per repository. func AddRepoManifestTable(x *xorm.Engine) error { type RepoManifest struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/migrations/v1_27/v363.go b/models/migrations/v1_27/v363.go new file mode 100644 index 0000000000..8e2e3faa22 --- /dev/null +++ b/models/migrations/v1_27/v363.go @@ -0,0 +1,18 @@ +// Copyright 2026 Moko Consulting +// 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)) +} diff --git a/models/migrations/v1_27/v364.go b/models/migrations/v1_27/v364.go new file mode 100644 index 0000000000..62f1923713 --- /dev/null +++ b/models/migrations/v1_27/v364.go @@ -0,0 +1,25 @@ +// Copyright 2026 Moko Consulting +// 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)) +} diff --git a/models/migrations/v1_27/v365.go b/models/migrations/v1_27/v365.go new file mode 100644 index 0000000000..2c550f08b7 --- /dev/null +++ b/models/migrations/v1_27/v365.go @@ -0,0 +1,27 @@ +// Copyright 2026 Moko Consulting +// 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)) +} diff --git a/models/migrations/v1_27/v366.go b/models/migrations/v1_27/v366.go new file mode 100644 index 0000000000..8ae8a2861a --- /dev/null +++ b/models/migrations/v1_27/v366.go @@ -0,0 +1,31 @@ +// Copyright 2026 Moko Consulting +// 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)) +} diff --git a/models/migrations/v1_27/v367.go b/models/migrations/v1_27/v367.go new file mode 100644 index 0000000000..bf7612e21d --- /dev/null +++ b/models/migrations/v1_27/v367.go @@ -0,0 +1,23 @@ +// Copyright 2026 Moko Consulting +// 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)) +} diff --git a/models/migrations/v1_27/v368.go b/models/migrations/v1_27/v368.go new file mode 100644 index 0000000000..654e9c2fda --- /dev/null +++ b/models/migrations/v1_27/v368.go @@ -0,0 +1,26 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package v1_27 + +import "xorm.io/xorm" + +// AddUserAllowlistsToOrgProtectedBranch adds per-user allowlist columns (username or +// email resolved to user IDs) plus the "allow Actions bot" toggles to org-level branch +// protection rules for the push, merge, force-push and delete categories, mirroring the +// per-repo user allowlists so an org rule can also grant access to individual users and +// the Actions bot. See issue #727. +func AddUserAllowlistsToOrgProtectedBranch(x *xorm.Engine) error { + type OrgProtectedBranch struct { + WhitelistUserIDs []int64 `xorm:"JSON TEXT"` + WhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"` + MergeWhitelistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"` + ForcePushAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + DeleteAllowlistUserIDs []int64 `xorm:"JSON TEXT"` + DeleteAllowlistActionsUser bool `xorm:"NOT NULL DEFAULT false"` + ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"` + } + return x.Sync(new(OrgProtectedBranch)) +} diff --git a/models/repo/repo_manifest.go b/models/repo/repo_manifest.go index 51b7d688e4..47f13cfc78 100644 --- a/models/repo/repo_manifest.go +++ b/models/repo/repo_manifest.go @@ -15,50 +15,50 @@ func init() { db.RegisterModel(new(RepoMetadata)) } -// RepoMetadata stores mokoplatform metadata settings for a repository. -// These fields correspond to the .mokogitea/manifest.xml schema and are -// exposed via API for use by Actions workflows and the mokoplatform CLI. +// RepoMetadata stores mokocli metadata settings for a repository. +// These first-class fields are the authoritative repository metadata, +// exposed via API for use by Actions workflows and mokocli. type RepoMetadata struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"` + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"` // identity section - Name string `xorm:"TEXT 'name'"` // project name - Org string `xorm:"TEXT 'org'"` // organization name - Description string `xorm:"TEXT 'description'"` // project description + Name string `xorm:"TEXT 'name'"` // project name + Org string `xorm:"TEXT 'org'"` // organization name + Description string `xorm:"TEXT 'description'"` // project description LicenseSPDX string `xorm:"VARCHAR(50) 'license_spdx'"` // SPDX identifier, e.g. "GPL-3.0-or-later" - LicenseName string `xorm:"TEXT 'license_name'"` // human-readable license name + LicenseName string `xorm:"TEXT 'license_name'"` // human-readable license name // governance section Platform string `xorm:"VARCHAR(50) 'platform'"` // go, php, node, python, etc. - StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // mokoplatform standards version - StandardsSource string `xorm:"TEXT 'standards_source'"` // URL to standards repo + StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // mokocli standards version + StandardsSource string `xorm:"TEXT 'standards_source'"` // URL to standards repo // versioning - VersionPrefix string `xorm:"TEXT 'version_prefix'"` // tag prefix stripped for version display, e.g. "v1.26.1-moko." - ElementName string `xorm:"TEXT 'element_name'"` // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty) + VersionPrefix string `xorm:"TEXT 'version_prefix'"` // tag prefix stripped for version display, e.g. "v1.26.1-moko." + ElementName string `xorm:"TEXT 'element_name'"` // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty) // distribution metadata (used by update server feed generation) - Maintainer string `xorm:"TEXT 'maintainer'"` // maintainer/author name - MaintainerURL string `xorm:"TEXT 'maintainer_url'"` // maintainer website - InfoURL string `xorm:"TEXT 'info_url'"` // extension info/product page URL - TargetVersion string `xorm:"TEXT 'target_version'"` // target platform version regex, e.g. "(5|6)\..*" + Maintainer string `xorm:"TEXT 'maintainer'"` // maintainer/author name + MaintainerURL string `xorm:"TEXT 'maintainer_url'"` // maintainer website + InfoURL string `xorm:"TEXT 'info_url'"` // extension info/product page URL + TargetVersion string `xorm:"TEXT 'target_version'"` // target platform version regex, e.g. "(5|6)\..*" PHPMinimum string `xorm:"VARCHAR(20) 'php_minimum'"` // minimum PHP version, e.g. "8.1" // build section - Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc. + Language string `xorm:"VARCHAR(50) 'language'"` // Go, PHP, TypeScript, etc. ExtensionType string `xorm:"VARCHAR(50) 'extension_type'"` // component, module, plugin, package, template, library, file - EntryPoint string `xorm:"TEXT 'entry_point'"` // build entry point path + EntryPoint string `xorm:"TEXT 'entry_point'"` // build entry point path // deploy section DeployHost string `xorm:"VARCHAR(255) 'deploy_host'"` // SSH host for deploy DeployPort string `xorm:"VARCHAR(10) 'deploy_port'"` // SSH port (default 2918) DeployUser string `xorm:"VARCHAR(100) 'deploy_user'"` // SSH user - DeployPath string `xorm:"TEXT 'deploy_path'"` // remote path for source/compose + DeployPath string `xorm:"TEXT 'deploy_path'"` // remote path for source/compose DockerImage string `xorm:"VARCHAR(255) 'docker_image'"` // e.g. mokoconsulting/mokogitea DockerRegistry string `xorm:"VARCHAR(255) 'docker_registry'"` // e.g. git.mokoconsulting.tech ContainerName string `xorm:"VARCHAR(100) 'container_name'"` // Docker container name - HealthURL string `xorm:"TEXT 'health_url'"` // health check URL after deploy + HealthURL string `xorm:"TEXT 'health_url'"` // health check URL after deploy CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"` UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"` diff --git a/modules/structs/org_branch.go b/modules/structs/org_branch.go index 7333a4192d..a89269391b 100644 --- a/modules/structs/org_branch.go +++ b/modules/structs/org_branch.go @@ -14,16 +14,28 @@ type OrgBranchProtection struct { EnablePush bool `json:"enable_push"` EnablePushWhitelist bool `json:"enable_push_whitelist"` PushWhitelistTeams []string `json:"push_whitelist_teams"` + PushWhitelistUsernames []string `json:"push_whitelist_usernames"` + PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"` EnableForcePush bool `json:"enable_force_push"` EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"` ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"` + ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"` + ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"` + EnableDelete bool `json:"enable_delete"` + EnableDeleteAllowlist bool `json:"enable_delete_allowlist"` + DeleteAllowlistTeams []string `json:"delete_allowlist_teams"` + DeleteAllowlistUsernames []string `json:"delete_allowlist_usernames"` + DeleteAllowlistActionsUser bool `json:"delete_allowlist_actions_user"` EnableMergeWhitelist bool `json:"enable_merge_whitelist"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` + MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"` EnableStatusCheck bool `json:"enable_status_check"` StatusCheckContexts []string `json:"status_check_contexts"` RequiredApprovals int64 `json:"required_approvals"` EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"` ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"` + ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"` BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"` BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"` BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"` @@ -46,16 +58,28 @@ type CreateOrgBranchProtectionOption struct { EnablePush bool `json:"enable_push"` EnablePushWhitelist bool `json:"enable_push_whitelist"` PushWhitelistTeams []string `json:"push_whitelist_teams"` + PushWhitelistUsernames []string `json:"push_whitelist_usernames"` + PushWhitelistActionsUser bool `json:"push_whitelist_actions_user"` EnableForcePush bool `json:"enable_force_push"` EnableForcePushAllowlist bool `json:"enable_force_push_allowlist"` ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"` + ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"` + ForcePushAllowlistActionsUser bool `json:"force_push_allowlist_actions_user"` + EnableDelete bool `json:"enable_delete"` + EnableDeleteAllowlist bool `json:"enable_delete_allowlist"` + DeleteAllowlistTeams []string `json:"delete_allowlist_teams"` + DeleteAllowlistUsernames []string `json:"delete_allowlist_usernames"` + DeleteAllowlistActionsUser bool `json:"delete_allowlist_actions_user"` EnableMergeWhitelist bool `json:"enable_merge_whitelist"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` + MergeWhitelistActionsUser bool `json:"merge_whitelist_actions_user"` EnableStatusCheck bool `json:"enable_status_check"` StatusCheckContexts []string `json:"status_check_contexts"` RequiredApprovals int64 `json:"required_approvals"` EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"` ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"` + ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"` BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"` BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests"` BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"` @@ -73,16 +97,28 @@ type EditOrgBranchProtectionOption struct { EnablePush *bool `json:"enable_push"` EnablePushWhitelist *bool `json:"enable_push_whitelist"` PushWhitelistTeams []string `json:"push_whitelist_teams"` + PushWhitelistUsernames []string `json:"push_whitelist_usernames"` + PushWhitelistActionsUser *bool `json:"push_whitelist_actions_user"` EnableForcePush *bool `json:"enable_force_push"` EnableForcePushAllowlist *bool `json:"enable_force_push_allowlist"` ForcePushAllowlistTeams []string `json:"force_push_allowlist_teams"` + ForcePushAllowlistUsernames []string `json:"force_push_allowlist_usernames"` + ForcePushAllowlistActionsUser *bool `json:"force_push_allowlist_actions_user"` + EnableDelete *bool `json:"enable_delete"` + EnableDeleteAllowlist *bool `json:"enable_delete_allowlist"` + DeleteAllowlistTeams []string `json:"delete_allowlist_teams"` + DeleteAllowlistUsernames []string `json:"delete_allowlist_usernames"` + DeleteAllowlistActionsUser *bool `json:"delete_allowlist_actions_user"` EnableMergeWhitelist *bool `json:"enable_merge_whitelist"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` + MergeWhitelistActionsUser *bool `json:"merge_whitelist_actions_user"` EnableStatusCheck *bool `json:"enable_status_check"` StatusCheckContexts []string `json:"status_check_contexts"` RequiredApprovals *int64 `json:"required_approvals"` EnableApprovalsWhitelist *bool `json:"enable_approvals_whitelist"` ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"` + ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"` BlockOnRejectedReviews *bool `json:"block_on_rejected_reviews"` BlockOnOfficialReviewRequests *bool `json:"block_on_official_review_requests"` BlockOnOutdatedBranch *bool `json:"block_on_outdated_branch"` diff --git a/modules/structs/org_email_domain.go b/modules/structs/org_email_domain.go new file mode 100644 index 0000000000..fc80f9f1ca --- /dev/null +++ b/modules/structs/org_email_domain.go @@ -0,0 +1,15 @@ +// 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"` +} diff --git a/modules/structs/org_push_policy.go b/modules/structs/org_push_policy.go new file mode 100644 index 0000000000..36af0e3635 --- /dev/null +++ b/modules/structs/org_push_policy.go @@ -0,0 +1,30 @@ +// 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"` +} diff --git a/modules/structs/org_repo_defaults.go b/modules/structs/org_repo_defaults.go new file mode 100644 index 0000000000..5839e9433c --- /dev/null +++ b/modules/structs/org_repo_defaults.go @@ -0,0 +1,32 @@ +// 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"` +} diff --git a/modules/structs/org_tag.go b/modules/structs/org_tag.go new file mode 100644 index 0000000000..626c1176c9 --- /dev/null +++ b/modules/structs/org_tag.go @@ -0,0 +1,30 @@ +// 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"` +} diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 7dbb14e374..a11a756ab8 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -86,31 +86,35 @@ func Test_NormalizeEOL(t *testing.T) { } func Test_RandomInt(t *testing.T) { - randInt := CryptoRandomInt(255) + randInt, err := CryptoRandomInt(255) + assert.NoError(t, err) assert.GreaterOrEqual(t, randInt, int64(0)) assert.LessOrEqual(t, randInt, int64(255)) } func Test_RandomString(t *testing.T) { - str1 := CryptoRandomString(32) - var err error + str1, err := CryptoRandomString(32) + assert.NoError(t, err) matches, err := regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1) assert.NoError(t, err) assert.True(t, matches) - str2 := CryptoRandomString(32) + str2, err := CryptoRandomString(32) + assert.NoError(t, err) matches, err = regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1) assert.NoError(t, err) assert.True(t, matches) assert.NotEqual(t, str1, str2) - str3 := CryptoRandomString(256) + str3, err := CryptoRandomString(256) + assert.NoError(t, err) matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str3) assert.NoError(t, err) assert.True(t, matches) - str4 := CryptoRandomString(256) + str4, err := CryptoRandomString(256) + assert.NoError(t, err) matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str4) assert.NoError(t, err) assert.True(t, matches) @@ -119,15 +123,19 @@ func Test_RandomString(t *testing.T) { } func Test_RandomBytes(t *testing.T) { - bytes1 := CryptoRandomBytes(32) + bytes1, err := CryptoRandomBytes(32) + assert.NoError(t, err) - bytes2 := CryptoRandomBytes(32) + bytes2, err := CryptoRandomBytes(32) + assert.NoError(t, err) assert.NotEqual(t, bytes1, bytes2) - bytes3 := CryptoRandomBytes(256) + bytes3, err := CryptoRandomBytes(256) + assert.NoError(t, err) - bytes4 := CryptoRandomBytes(256) + bytes4, err := CryptoRandomBytes(256) + assert.NoError(t, err) assert.NotEqual(t, bytes3, bytes4) } diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index ea52615095..2866af1ac3 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2411,6 +2411,31 @@ "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", @@ -2439,7 +2464,7 @@ "repo.settings.protect_force_push_allowlist_teams": "Allowlisted teams for force pushing:", "repo.settings.protect_force_push_allowlist_deploy_keys": "Allowlist deploy keys with push access to force push.", "repo.settings.protect_force_push_allowlist_actions_user": "Allowlist actions bot user to force push.", - "repo.settings.event_delete": "Branch Deletion", + "repo.settings.protect_branch_deletion": "Branch Deletion", "repo.settings.protect_disable_delete": "Disable Deletion", "repo.settings.protect_disable_delete_desc": "This branch cannot be deleted.", "repo.settings.protect_enable_delete_all": "Enable Deletion", @@ -2747,7 +2772,7 @@ "repo.settings.support_url_help": "Shown when downloads are gated. Can point to your wiki, product page, or external support site.", "repo.settings.custom_fields": "Custom Fields", "repo.settings.manifest": "Manifest", - "repo.settings.manifest_desc": "Project identity, governance, and build settings from the MokoPlatform manifest. These are accessible via API for Actions workflows and the MokoPlatform CLI.", + "repo.settings.manifest_desc": "Project identity, governance, and build settings for the repository. These are accessible via API for Actions workflows and MokoCLI.", "repo.settings.manifest_identity": "Identity", "repo.settings.manifest_name": "Project Name", "repo.settings.manifest_element_name": "Element Name", diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 6395a6cf97..425b033c83 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -516,7 +516,7 @@ func reqOrgVisible() func(ctx *context.APIContext) { ctx.APIErrorInternal(errors.New("reqOrgVisible: unprepared context")) return } - if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) { + if !organization.IsOwnerVisibleToDoer(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) { ctx.APIErrorNotFound() return } @@ -1543,8 +1543,6 @@ func Routes() *web.Router { }, reqAnyRepoReader()) m.Get("/metadata", repo.GetRepoMetadata) m.Put("/metadata", reqToken(), reqAdmin(), repo.UpdateRepoMetadata) - m.Get("/manifest", repo.GetRepoMetadata) // backward compat - m.Put("/manifest", reqToken(), reqAdmin(), repo.UpdateRepoMetadata) // MokoGitea badge engine m.Get("/badge/{type}.svg", repo.GetRepoBadge) m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates) @@ -1698,29 +1696,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,6 +1821,34 @@ 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() { diff --git a/routers/api/v1/org/branch_protection.go b/routers/api/v1/org/branch_protection.go index ff188f278d..e06bcb28d3 100644 --- a/routers/api/v1/org/branch_protection.go +++ b/routers/api/v1/org/branch_protection.go @@ -8,6 +8,7 @@ import ( git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization" + user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user" api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" @@ -36,6 +37,18 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec return names } + resolveUserNames := func(ids []int64) []string { + names := make([]string, 0, len(ids)) + for _, id := range ids { + u, err := user_model.GetUserByID(ctx, id) + if err != nil { + continue + } + names = append(names, u.Name) + } + return names + } + return &api.OrgBranchProtection{ ID: rule.ID, OrgID: rule.OrgID, @@ -44,16 +57,28 @@ func toAPIOrgBranchProtection(ctx *context.APIContext, rule *git_model.OrgProtec EnablePush: rule.CanPush, EnablePushWhitelist: rule.EnableWhitelist, PushWhitelistTeams: resolveTeamNames(rule.WhitelistTeamIDs), + PushWhitelistUsernames: resolveUserNames(rule.WhitelistUserIDs), + PushWhitelistActionsUser: rule.WhitelistActionsUser, EnableForcePush: rule.CanForcePush, EnableForcePushAllowlist: rule.EnableForcePushAllowlist, ForcePushAllowlistTeams: resolveTeamNames(rule.ForcePushAllowlistTeamIDs), + ForcePushAllowlistUsernames: resolveUserNames(rule.ForcePushAllowlistUserIDs), + ForcePushAllowlistActionsUser: rule.ForcePushAllowlistActionsUser, + EnableDelete: rule.CanDelete, + EnableDeleteAllowlist: rule.EnableDeleteAllowlist, + DeleteAllowlistTeams: resolveTeamNames(rule.DeleteAllowlistTeamIDs), + DeleteAllowlistUsernames: resolveUserNames(rule.DeleteAllowlistUserIDs), + DeleteAllowlistActionsUser: rule.DeleteAllowlistActionsUser, EnableMergeWhitelist: rule.EnableMergeWhitelist, MergeWhitelistTeams: resolveTeamNames(rule.MergeWhitelistTeamIDs), + MergeWhitelistUsernames: resolveUserNames(rule.MergeWhitelistUserIDs), + MergeWhitelistActionsUser: rule.MergeWhitelistActionsUser, EnableStatusCheck: rule.EnableStatusCheck, StatusCheckContexts: rule.StatusCheckContexts, RequiredApprovals: rule.RequiredApprovals, EnableApprovalsWhitelist: rule.EnableApprovalsWhitelist, ApprovalsWhitelistTeams: resolveTeamNames(rule.ApprovalsWhitelistTeamIDs), + ApprovalsWhitelistUsernames: resolveUserNames(rule.ApprovalsWhitelistUserIDs), BlockOnRejectedReviews: rule.BlockOnRejectedReviews, BlockOnOfficialReviewRequests: rule.BlockOnOfficialReviewRequests, BlockOnOutdatedBranch: rule.BlockOnOutdatedBranch, @@ -85,6 +110,41 @@ func resolveTeamIDs(ctx *context.APIContext, orgID int64, names []string) ([]int return ids, true } +// resolveUserIDs converts a list of user identifiers (each a username or an email +// address) to deduped user IDs, returning 422 on the first unknown identifier. Each +// entry is looked up by username first, then falls back to an activated email address. +func resolveUserIDs(ctx *context.APIContext, names []string) ([]int64, bool) { + if len(names) == 0 { + return nil, true + } + seen := make(map[int64]struct{}, len(names)) + ids := make([]int64, 0, len(names)) + for _, name := range names { + u, err := user_model.GetUserByName(ctx, name) + if err != nil { + if !user_model.IsErrUserNotExist(err) { + ctx.APIErrorInternal(err) + return nil, false + } + u, err = user_model.GetUserByEmail(ctx, name) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, "unknown user: "+name) + } else { + ctx.APIErrorInternal(err) + } + return nil, false + } + } + if _, dup := seen[u.ID]; dup { + continue + } + seen[u.ID] = struct{}{} + ids = append(ids, u.ID) + } + return ids, true +} + // ListOrgBranchProtections list org-level branch protection rules func ListOrgBranchProtections(ctx *context.APIContext) { // swagger:operation GET /orgs/{org}/branch_protections organization orgListBranchProtections @@ -211,6 +271,30 @@ func CreateOrgBranchProtection(ctx *context.APIContext) { if !ok { return } + deleteTeams, ok := resolveTeamIDs(ctx, orgID, form.DeleteAllowlistTeams) + if !ok { + return + } + pushUsers, ok := resolveUserIDs(ctx, form.PushWhitelistUsernames) + if !ok { + return + } + forcePushUsers, ok := resolveUserIDs(ctx, form.ForcePushAllowlistUsernames) + if !ok { + return + } + mergeUsers, ok := resolveUserIDs(ctx, form.MergeWhitelistUsernames) + if !ok { + return + } + approvalsUsers, ok := resolveUserIDs(ctx, form.ApprovalsWhitelistUsernames) + if !ok { + return + } + deleteUsers, ok := resolveUserIDs(ctx, form.DeleteAllowlistUsernames) + if !ok { + return + } rule := &git_model.OrgProtectedBranch{ OrgID: orgID, @@ -219,16 +303,28 @@ func CreateOrgBranchProtection(ctx *context.APIContext) { CanPush: form.EnablePush, EnableWhitelist: form.EnablePush && form.EnablePushWhitelist, WhitelistTeamIDs: pushTeams, + WhitelistUserIDs: pushUsers, + WhitelistActionsUser: form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistActionsUser, CanForcePush: form.EnablePush && form.EnableForcePush, EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist, ForcePushAllowlistTeamIDs: forcePushTeams, + ForcePushAllowlistUserIDs: forcePushUsers, + ForcePushAllowlistActionsUser: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistActionsUser, + CanDelete: form.EnableDelete, + EnableDeleteAllowlist: form.EnableDelete && form.EnableDeleteAllowlist, + DeleteAllowlistTeamIDs: deleteTeams, + DeleteAllowlistUserIDs: deleteUsers, + DeleteAllowlistActionsUser: form.EnableDelete && form.EnableDeleteAllowlist && form.DeleteAllowlistActionsUser, EnableMergeWhitelist: form.EnableMergeWhitelist, MergeWhitelistTeamIDs: mergeTeams, + MergeWhitelistUserIDs: mergeUsers, + MergeWhitelistActionsUser: form.EnableMergeWhitelist && form.MergeWhitelistActionsUser, EnableStatusCheck: form.EnableStatusCheck, StatusCheckContexts: form.StatusCheckContexts, RequiredApprovals: form.RequiredApprovals, EnableApprovalsWhitelist: form.EnableApprovalsWhitelist, ApprovalsWhitelistTeamIDs: approvalsTeams, + ApprovalsWhitelistUserIDs: approvalsUsers, BlockOnRejectedReviews: form.BlockOnRejectedReviews, BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests, BlockOnOutdatedBranch: form.BlockOnOutdatedBranch, @@ -310,6 +406,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) { } rule.WhitelistTeamIDs = ids } + if form.PushWhitelistUsernames != nil { + ids, ok := resolveUserIDs(ctx, form.PushWhitelistUsernames) + if !ok { + return + } + rule.WhitelistUserIDs = ids + } + if form.PushWhitelistActionsUser != nil { + rule.WhitelistActionsUser = *form.PushWhitelistActionsUser + } if form.EnableForcePush != nil { rule.CanForcePush = *form.EnableForcePush } @@ -323,6 +429,39 @@ func EditOrgBranchProtection(ctx *context.APIContext) { } rule.ForcePushAllowlistTeamIDs = ids } + if form.ForcePushAllowlistUsernames != nil { + ids, ok := resolveUserIDs(ctx, form.ForcePushAllowlistUsernames) + if !ok { + return + } + rule.ForcePushAllowlistUserIDs = ids + } + if form.ForcePushAllowlistActionsUser != nil { + rule.ForcePushAllowlistActionsUser = *form.ForcePushAllowlistActionsUser + } + 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.DeleteAllowlistUsernames != nil { + ids, ok := resolveUserIDs(ctx, form.DeleteAllowlistUsernames) + if !ok { + return + } + rule.DeleteAllowlistUserIDs = ids + } + if form.DeleteAllowlistActionsUser != nil { + rule.DeleteAllowlistActionsUser = *form.DeleteAllowlistActionsUser + } if form.EnableMergeWhitelist != nil { rule.EnableMergeWhitelist = *form.EnableMergeWhitelist } @@ -333,6 +472,16 @@ func EditOrgBranchProtection(ctx *context.APIContext) { } rule.MergeWhitelistTeamIDs = ids } + if form.MergeWhitelistUsernames != nil { + ids, ok := resolveUserIDs(ctx, form.MergeWhitelistUsernames) + if !ok { + return + } + rule.MergeWhitelistUserIDs = ids + } + if form.MergeWhitelistActionsUser != nil { + rule.MergeWhitelistActionsUser = *form.MergeWhitelistActionsUser + } if form.EnableStatusCheck != nil { rule.EnableStatusCheck = *form.EnableStatusCheck } @@ -352,6 +501,13 @@ func EditOrgBranchProtection(ctx *context.APIContext) { } rule.ApprovalsWhitelistTeamIDs = ids } + if form.ApprovalsWhitelistUsernames != nil { + ids, ok := resolveUserIDs(ctx, form.ApprovalsWhitelistUsernames) + if !ok { + return + } + rule.ApprovalsWhitelistUserIDs = ids + } if form.BlockOnRejectedReviews != nil { rule.BlockOnRejectedReviews = *form.BlockOnRejectedReviews } diff --git a/routers/api/v1/org/email_domain.go b/routers/api/v1/org/email_domain.go new file mode 100644 index 0000000000..92eeba1241 --- /dev/null +++ b/routers/api/v1/org/email_domain.go @@ -0,0 +1,124 @@ +// 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) +} diff --git a/routers/api/v1/org/push_policy.go b/routers/api/v1/org/push_policy.go new file mode 100644 index 0000000000..8f13bcc935 --- /dev/null +++ b/routers/api/v1/org/push_policy.go @@ -0,0 +1,145 @@ +// 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) +} diff --git a/routers/api/v1/org/repo_defaults.go b/routers/api/v1/org/repo_defaults.go new file mode 100644 index 0000000000..02ac671472 --- /dev/null +++ b/routers/api/v1/org/repo_defaults.go @@ -0,0 +1,174 @@ +// 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) +} diff --git a/routers/api/v1/org/tag_protection.go b/routers/api/v1/org/tag_protection.go new file mode 100644 index 0000000000..abec3e8caf --- /dev/null +++ b/routers/api/v1/org/tag_protection.go @@ -0,0 +1,271 @@ +// 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) +} diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index d93aab1c5a..07583d5f6f 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -9,6 +9,7 @@ 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" @@ -491,6 +492,8 @@ 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) } diff --git a/routers/api/v1/repo/manifest.go b/routers/api/v1/repo/manifest.go index 36b7b87bf3..642eb76d5b 100644 --- a/routers/api/v1/repo/manifest.go +++ b/routers/api/v1/repo/manifest.go @@ -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,6 +44,13 @@ 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 @@ -51,6 +58,17 @@ 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" @@ -71,9 +89,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, @@ -89,7 +107,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, @@ -111,9 +129,21 @@ 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 { @@ -173,9 +203,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, @@ -191,7 +221,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, diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 176ab4656e..2c54ba24d8 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -159,6 +159,44 @@ 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 diff --git a/routers/api/v1/swagger/org.go b/routers/api/v1/swagger/org.go index 8379dd24cc..f3accaff57 100644 --- a/routers/api/v1/swagger/org.go +++ b/routers/api/v1/swagger/org.go @@ -41,3 +41,52 @@ 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"` +} diff --git a/routers/init.go b/routers/init.go index 0b6c0e5916..cda1e5f05b 100644 --- a/routers/init.go +++ b/routers/init.go @@ -48,6 +48,7 @@ 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" @@ -155,6 +156,7 @@ 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() diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index a4d9763ff2..efbbbfcbe8 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -8,6 +8,8 @@ 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" @@ -160,6 +162,10 @@ 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 { @@ -455,6 +461,10 @@ 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) @@ -468,7 +478,7 @@ func preReceiveTag(ctx *preReceiveContext, refFullName git.RefName) { ctx.gotProtectedTags = true } - isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, ctx.protectedTags, tagName, ctx.opts.UserID) + isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, ctx.protectedTags, ctx.Repo.Repository, tagName, ctx.opts.UserID) if err != nil { ctx.JSON(http.StatusInternalServerError, private.Response{ Err: err.Error(), @@ -596,3 +606,158 @@ 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(oldCommitID, 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) added or modified by the +// push (oldCommitID..newCommitID) whose blob exceeds limit bytes, or ("", 0) if none — +// or on any error (fail open). Only the pushed delta is inspected so a pre-existing +// oversized file committed before the policy existed cannot permanently block unrelated +// pushes. For a new branch (no old commit) the full new tree is scanned, since every +// blob is effectively introduced by the push. +func (ctx *preReceiveContext) largestBlobOverLimit(oldCommitID, newCommitID string, limit int64) (string, int64) { + emptyID := ctx.Repo.GetObjectFormat().EmptyObjectID().String() + + // New branch: no base commit to diff against, so scan the full tip tree. ls-tree + // --long carries the blob size inline, so no extra sizing pass is needed. + if oldCommitID == "" || oldCommitID == emptyID { + output, _, err := gitrepo.RunCmdString(ctx, ctx.Repo.Repository, + gitcmd.NewCommand("ls-tree", "-r", "--long").AddDynamicArguments(newCommitID).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 + } + if size, perr := strconv.ParseInt(fields[3], 10, 64); perr == nil && size > limit { + return line[tab+1:], size + } + } + return "", 0 + } + + // Existing branch: inspect only blobs added or modified by this push. diff-tree + // gives the changed paths and their new object ids but not sizes, so collect the + // candidate blob ids and size them in a single cat-file --batch-check pass. + output, _, err := gitrepo.RunCmdString(ctx, ctx.Repo.Repository, + gitcmd.NewCommand("diff-tree", "--no-commit-id", "-r").AddDynamicArguments(oldCommitID, newCommitID).WithEnv(ctx.env)) + if err != nil { + log.Error("org push policy diff-tree for %-v: %v", ctx.Repo.Repository, err) + return "", 0 + } + shaToPath := make(map[string]string) + var order []string + for _, line := range strings.Split(output, "\n") { + tab := strings.IndexByte(line, '\t') + if tab < 0 { + continue + } + fields := strings.Fields(line[:tab]) // ":oldmode newmode oldsha newsha status" + if len(fields) < 5 { + continue + } + newMode, newSha, status := fields[1], fields[3], fields[4] + // Only regular, executable, or symlink blobs; skip submodule gitlinks (160000). + if newMode != "100644" && newMode != "100755" && newMode != "120000" { + continue + } + if status == "D" || newSha == emptyID { + continue + } + if _, seen := shaToPath[newSha]; !seen { + order = append(order, newSha) + } + shaToPath[newSha] = line[tab+1:] + } + if len(order) == 0 { + return "", 0 + } + + output, _, err = gitrepo.RunCmdString(ctx, ctx.Repo.Repository, + gitcmd.NewCommand("cat-file", "--batch-check").WithStdinBytes([]byte(strings.Join(order, "\n")+"\n")).WithEnv(ctx.env)) + if err != nil { + log.Error("org push policy cat-file for %-v: %v", ctx.Repo.Repository, err) + return "", 0 + } + for _, line := range strings.Split(output, "\n") { + fields := strings.Fields(line) // " " or " missing" + if len(fields) != 3 || fields[1] != "blob" { + continue + } + if size, perr := strconv.ParseInt(fields[2], 10, 64); perr == nil && size > limit { + return shaToPath[fields[0]], size + } + } + return "", 0 +} diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go index 45bdfac975..48e846f57d 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -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 != "" && statusIDStr != "" { + if statusIDStr := ctx.Req.FormValue("status_id"); statusIDStr != "" { if statusIDStr == "reopen" { // Reopen via dropdown if issue.IsClosed { diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index c8881838ab..1b670038ba 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -34,6 +34,64 @@ 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") @@ -46,6 +104,16 @@ 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 diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go index bb2956bfe2..28cf018fac 100644 --- a/routers/web/repo/setting/protected_tag.go +++ b/routers/web/repo/setting/protected_tag.go @@ -138,6 +138,13 @@ 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 @@ -163,6 +170,36 @@ 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 diff --git a/services/org/notifier.go b/services/org/notifier.go new file mode 100644 index 0000000000..b4d63faf52 --- /dev/null +++ b/services/org/notifier.go @@ -0,0 +1,92 @@ +// Copyright 2026 Moko Consulting +// 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) + } + } +} diff --git a/services/org/team.go b/services/org/team.go index 26f9507c8e..ccf6c8b0a9 100644 --- a/services/org/team.go +++ b/services/org/team.go @@ -220,6 +220,13 @@ 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 } diff --git a/services/release/release.go b/services/release/release.go index 04cf233c5b..8458c909fe 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -11,8 +11,8 @@ import ( "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git" - updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" + updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver" 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.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID) + isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, protectedTags, rel.Repo, 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.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, doer.ID) + isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, protectedTags, repo, rel.TagName, doer.ID) if err != nil { return err } diff --git a/services/repository/manifest_sync.go b/services/repository/manifest_sync.go deleted file mode 100644 index 3b33d9b777..0000000000 --- a/services/repository/manifest_sync.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2026 Moko Consulting -// SPDX-License-Identifier: GPL-3.0-or-later - -package repository - -import ( - "context" - "encoding/xml" - - repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" - "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git" - "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" -) - -// manifestXML mirrors the .mokogitea/manifest.xml schema for XML parsing. -type manifestXML struct { - XMLName xml.Name `xml:"mokoplatform"` - Identity manifestIdentity `xml:"identity"` - Governance manifestGovernance `xml:"governance"` - Distribution manifestDistribution `xml:"distribution"` - Build manifestBuild `xml:"build"` -} - -type manifestDistribution struct { - DisplayName string `xml:"display-name"` - Maintainer string `xml:"maintainer"` - MaintainerURL string `xml:"maintainer-url"` - InfoURL string `xml:"info-url"` - TargetVersion string `xml:"target-version"` - PHPMinimum string `xml:"php-minimum"` -} - -type manifestIdentity struct { - Name string `xml:"name"` - Org string `xml:"org"` - Description string `xml:"description"` - VersionPrefix string `xml:"version-prefix"` - ElementName string `xml:"element-name"` - License manifestLicense `xml:"license"` -} - -type manifestLicense struct { - SPDX string `xml:"spdx,attr"` - Name string `xml:",chardata"` -} - -type manifestGovernance struct { - Platform string `xml:"platform"` - StandardsVersion string `xml:"standards-version"` - StandardsSource string `xml:"standards-source"` -} - -type manifestBuild struct { - Language string `xml:"language"` - ExtensionType string `xml:"package-type"` - EntryPoint string `xml:"entry-point"` -} - -// SyncMetadataFromCommit reads .mokogitea/manifest.xml from the given commit -// and upserts the values into the repo_manifest database table. -// This is called on push to the default branch to keep the database in sync -// with the XML file. If no manifest.xml exists, this is a no-op. -func SyncMetadataFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) { - if commit == nil { - return - } - - entry, err := commit.GetTreeEntryByPath(".mokogitea/manifest.xml") - if err != nil || entry == nil { - return // no manifest.xml — not an error - } - - reader, err := entry.Blob().DataAsync() - if err != nil { - log.Error("SyncMetadata: read blob for %s: %v", repo.FullName(), err) - return - } - defer reader.Close() - - var mxml manifestXML - decoder := xml.NewDecoder(reader) - if err := decoder.Decode(&mxml); err != nil { - log.Error("SyncMetadata: parse XML for %s: %v", repo.FullName(), err) - return - } - - manifest := &repo_model.RepoMetadata{ - RepoID: repo.ID, - Name: mxml.Identity.Name, - Org: mxml.Identity.Org, - Description: mxml.Identity.Description, - VersionPrefix: mxml.Identity.VersionPrefix, - ElementName: mxml.Identity.ElementName, - LicenseSPDX: mxml.Identity.License.SPDX, - LicenseName: mxml.Identity.License.Name, - Platform: mxml.Governance.Platform, - StandardsVersion: mxml.Governance.StandardsVersion, - StandardsSource: mxml.Governance.StandardsSource, - Maintainer: mxml.Distribution.Maintainer, - MaintainerURL: mxml.Distribution.MaintainerURL, - InfoURL: mxml.Distribution.InfoURL, - TargetVersion: mxml.Distribution.TargetVersion, - PHPMinimum: mxml.Distribution.PHPMinimum, - Language: mxml.Build.Language, - ExtensionType: mxml.Build.ExtensionType, - EntryPoint: mxml.Build.EntryPoint, - } - - if err := repo_model.CreateOrUpdateRepoMetadata(ctx, manifest); err != nil { - log.Error("SyncMetadata: save for %s: %v", repo.FullName(), err) - return - } - - log.Info("SyncMetadata: synced .mokogitea/manifest.xml for %s", repo.FullName()) -} diff --git a/services/repository/push.go b/services/repository/push.go index efc9134c3b..fdf99d821d 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -194,8 +194,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { if err := DelRepoDivergenceFromCache(ctx, repo.ID); err != nil { log.Error("DelRepoDivergenceFromCache: %v", err) } - // Auto-sync .mokogitea/manifest.xml to database on default branch push - SyncMetadataFromCommit(ctx, repo, newCommit) // Run security scanners on default branch push security_service.ScanOnPush(ctx, repo, newCommit) } else { diff --git a/services/security/code_scanner.go b/services/security/code_scanner.go index 61788173fe..633fcb0a24 100644 --- a/services/security/code_scanner.go +++ b/services/security/code_scanner.go @@ -160,7 +160,7 @@ var DefaultCodeRules = []CodeRule{ { ID: "deserialize-yaml-py", Title: "Insecure Deserialization: yaml.load() (Python)", Severity: security_model.SeverityHigh, CWE: "CWE-502", - Pattern: regexp.MustCompile(`yaml\.load\s*\([^)]*(?:Loader\s*=\s*yaml\.(?:Unsafe|Full)Loader|[^)]*\)(?!\s*#))`), + Pattern: regexp.MustCompile(`(?i)yaml\.load\s*\(`), Description: "yaml.load() without SafeLoader allows arbitrary code execution — use yaml.safe_load()", Languages: []string{".py"}, }, diff --git a/services/security/code_scanner_test.go b/services/security/code_scanner_test.go new file mode 100644 index 0000000000..55c6a2c124 --- /dev/null +++ b/services/security/code_scanner_test.go @@ -0,0 +1,23 @@ +// Copyright (C) 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package security + +import "testing" + +// TestDefaultCodeRulesCompile forces the package init, which builds every scanner +// rule's regexp via regexp.MustCompile, and asserts each pattern is present. Go's +// regexp engine is RE2 and rejects Perl-only constructs (lookahead/lookbehind/ +// backreferences) by panicking in MustCompile at init — which crash-loops the +// server at startup. This test executes that init so such a pattern fails CI here +// instead of on a live deploy. +func TestDefaultCodeRulesCompile(t *testing.T) { + if len(DefaultCodeRules) == 0 { + t.Fatal("DefaultCodeRules is empty") + } + for _, r := range DefaultCodeRules { + if r.Pattern == nil { + t.Errorf("rule %q has a nil Pattern", r.ID) + } + } +} diff --git a/services/security/orchestrator.go b/services/security/orchestrator.go index 95d453aacf..ce0fe82d96 100644 --- a/services/security/orchestrator.go +++ b/services/security/orchestrator.go @@ -6,6 +6,7 @@ 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" @@ -21,7 +22,11 @@ func ScanPushForSecrets(ctx context.Context, repoID int64, commit *git.Commit) [ return nil } if !cfg.Enabled || !cfg.BlockOnPush || !cfg.SecretScanner { - return nil + // The owning organization may mandate secret blocking regardless of the + // repository's own scanner config (org push policy). + if !orgRequiresSecretBlock(ctx, repoID) { + return nil + } } scanner := NewSecretScanner() @@ -33,6 +38,22 @@ 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) { diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl index 6ec80c057f..40947c86b9 100644 --- a/templates/repo/settings/branches.tmpl +++ b/templates/repo/settings/branches.tmpl @@ -62,6 +62,88 @@ {{end}} + + {{if .OrgProtectedBranches}} +

+ {{ctx.Locale.Tr "repo.settings.org_protected_branch"}} +

+
+

{{ctx.Locale.Tr "repo.settings.org_protected_branch_desc"}}

+
+ {{range .OrgProtectedBranches}} +
+
+
+ +
{{.Rule.RuleName}}
+ {{svg "octicon-organization" 12}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.inherited"}} + {{svg "octicon-lock" 12}} {{ctx.Locale.Tr "repo.settings.org_protected_branch.read_only"}} +
+ + + + + + + + + + + + + + + {{if gt .Rule.RequiredApprovals 0}} + + + + + {{end}} + {{if .Rule.EnableMergeWhitelist}} + + + + + {{end}} + {{if .Rule.EnableStatusCheck}} + + + + + {{end}} + {{if .Rule.RequireSignedCommits}} + + + + + {{end}} + {{if .Rule.ProtectedFilePatterns}} + + + + + {{end}} + {{if or .Rule.BlockOnOutdatedBranch .Rule.BlockOnRejectedReviews .Rule.BlockAdminMergeOverride}} + + + + + {{end}} + +
{{ctx.Locale.Tr "repo.settings.org_protected_branch.direct_push"}}{{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}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.force_push"}}{{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}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.deletion"}}{{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}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.approvals"}}{{.Rule.RequiredApprovals}}{{if .ApprovalTeams}} ({{.ApprovalTeams}}){{end}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.merge"}}{{if .MergeTeams}}{{.MergeTeams}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.restricted"}}{{end}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.status_check"}}{{if .StatusContexts}}{{.StatusContexts}}{{else}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.any"}}{{end}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.signed"}}{{svg "octicon-check" 14}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.protected_files"}}{{.Rule.ProtectedFilePatterns}}
{{ctx.Locale.Tr "repo.settings.org_protected_branch.also"}} +
+ {{if .Rule.BlockOnOutdatedBranch}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_outdated"}}{{end}} + {{if .Rule.BlockOnRejectedReviews}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_rejected"}}{{end}} + {{if .Rule.BlockAdminMergeOverride}}{{ctx.Locale.Tr "repo.settings.org_protected_branch.block_admin"}}{{end}} +
+
+
+
+
+ {{end}} +
+
+ {{end}} {{end}} diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl index 2c7bd8dcc0..e8d3081ae8 100644 --- a/templates/repo/settings/protected_branch.tmpl +++ b/templates/repo/settings/protected_branch.tmpl @@ -172,7 +172,7 @@ -
{{ctx.Locale.Tr "repo.settings.event_delete"}}
+
{{ctx.Locale.Tr "repo.settings.protect_branch_deletion"}}
diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl index b5494d9447..ca2bd7dd88 100644 --- a/templates/repo/settings/tags.tmpl +++ b/templates/repo/settings/tags.tmpl @@ -116,6 +116,30 @@ {{end}} + {{if .OrgProtectedTags}} +
+ {{svg "octicon-organization" 14}} {{ctx.Locale.Tr "repo.settings.org_protected_tag"}} +
+
+

{{ctx.Locale.Tr "repo.settings.org_protected_tag_desc"}}

+ + + + + + + + {{range .OrgProtectedTags}} + + + + + + {{end}} + +
{{ctx.Locale.Tr "repo.settings.tags.protection.pattern"}}{{ctx.Locale.Tr "repo.settings.tags.protection.allowed"}}{{ctx.Locale.Tr "repo.settings.org_protected_tag.read_only"}}
{{.Rule.NamePattern}}
{{if .Teams}}{{.Teams}}{{else}}{{ctx.Locale.Tr "repo.settings.tags.protection.allowed.noone"}}{{end}}{{svg "octicon-lock" 14}}
+
+ {{end}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index e096ed8bc4..6a1d0dd762 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -11,8 +11,8 @@ ], "swagger": "2.0", "info": { - "description": "This documentation describes the {{.SwaggerAppName}} API.", - "title": "{{.SwaggerAppName}} API", + "description": "This documentation describes the Gitea API.", + "title": "Gitea API", "license": { "name": "MIT", "url": "http://opensource.org/licenses/MIT" @@ -2858,6 +2858,285 @@ } } }, + "/orgs/{org}/branch_protections": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "List an organization's branch protection rules", + "operationId": "orgListBranchProtections", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/OrgBranchProtectionList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Create an org-level branch protection rule", + "operationId": "orgCreateBranchProtection", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateOrgBranchProtectionOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/OrgBranchProtection" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, + "/orgs/{org}/branch_protections/{name}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get a specific org-level branch protection rule", + "operationId": "orgGetBranchProtection", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the branch protection rule", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/OrgBranchProtection" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "tags": [ + "organization" + ], + "summary": "Delete an org-level branch protection rule", + "operationId": "orgDeleteBranchProtection", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the branch protection rule", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Edit an org-level branch protection rule. Only fields that are set will be changed", + "operationId": "orgEditBranchProtection", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the branch protection rule", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/EditOrgBranchProtectionOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/OrgBranchProtection" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, + "/orgs/{org}/email_domain_policy": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get the organization's email domain policy", + "operationId": "orgGetEmailDomainPolicy", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/OrgEmailDomainPolicy" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "tags": [ + "organization" + ], + "summary": "Remove the organization's email domain policy", + "operationId": "orgDeleteEmailDomainPolicy", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Create or update the organization's email domain policy. Only fields that are set will be changed", + "operationId": "orgEditEmailDomainPolicy", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/EditOrgEmailDomainPolicyOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/OrgEmailDomainPolicy" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/orgs/{org}/hooks": { "get": { "produces": [ @@ -3054,6 +3333,203 @@ } } }, + "/orgs/{org}/issue-priorities": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "List an organization's issue priority definitions", + "operationId": "orgListIssuePriorities", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "IssuePriorityDefList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/IssuePriorityDef" + } + } + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/orgs/{org}/issue-statuses": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "List an organization's issue status definitions", + "operationId": "orgListIssueStatuses", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "IssueStatusDefList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/IssueStatusDef" + } + } + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/orgs/{org}/issue-statuses/copy/{source_org}": { + "post": { + "tags": [ + "organization" + ], + "summary": "Copy issue statuses from another organization", + "operationId": "orgCopyIssueStatuses", + "parameters": [ + { + "type": "string", + "description": "target organization name", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "source organization name to copy from", + "name": "source_org", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "StatusesCopied" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/orgs/{org}/issue-statuses/presets": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "List available issue status presets", + "operationId": "orgListIssueStatusPresets", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "StatusPresetList" + } + } + } + }, + "/orgs/{org}/issue-statuses/presets/{preset}": { + "post": { + "tags": [ + "organization" + ], + "summary": "Apply a status preset to an organization", + "operationId": "orgApplyIssueStatusPreset", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "preset name", + "name": "preset", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "StatusPresetApplied" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/orgs/{org}/issue-types": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "List an organization's issue type definitions", + "operationId": "orgListIssueTypes", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "IssueTypeDefList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/IssueTypeDef" + } + } + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/orgs/{org}/labels": { "get": { "produces": [ @@ -3511,6 +3987,99 @@ } } }, + "/orgs/{org}/push_policy": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get the organization's push policy", + "operationId": "orgGetPushPolicy", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/OrgPushPolicy" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "tags": [ + "organization" + ], + "summary": "Remove the organization's push policy", + "operationId": "orgDeletePushPolicy", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Create or update the organization's push policy. Only fields that are set will be changed", + "operationId": "orgEditPushPolicy", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/EditOrgPushPolicyOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/OrgPushPolicy" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/orgs/{org}/rename": { "post": { "produces": [ @@ -3551,6 +4120,99 @@ } } }, + "/orgs/{org}/repo_defaults": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get the organization's default repository settings", + "operationId": "orgGetRepoDefaults", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/OrgRepoDefaults" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "tags": [ + "organization" + ], + "summary": "Remove the organization's default repository settings", + "operationId": "orgDeleteRepoDefaults", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Create or update the organization's default repository settings. Only fields that are set will be changed", + "operationId": "orgEditRepoDefaults", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/EditOrgRepoDefaultsOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/OrgRepoDefaults" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/orgs/{org}/repos": { "get": { "produces": [ @@ -3668,6 +4330,195 @@ } } }, + "/orgs/{org}/tag_protections": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "List an organization's tag protection rules", + "operationId": "orgListTagProtections", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/OrgTagProtectionList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Create an org-level tag protection rule", + "operationId": "orgCreateTagProtection", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "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" + } + } + } + }, + "/orgs/{org}/tag_protections/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get a specific org-level tag protection rule", + "operationId": "orgGetTagProtection", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the tag protection rule", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/OrgTagProtection" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "tags": [ + "organization" + ], + "summary": "Delete an org-level tag protection rule", + "operationId": "orgDeleteTagProtection", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the tag protection rule", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Edit an org-level tag protection rule. Only fields that are set will be changed", + "operationId": "orgEditTagProtection", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the tag protection rule", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/EditOrgTagProtectionOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/OrgTagProtection" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/orgs/{org}/teams": { "get": { "produces": [ @@ -9776,6 +10627,205 @@ } } }, + "/repos/{owner}/{repo}/issues/bulk/assignees": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Set assignees on multiple issues", + "operationId": "issueBulkSetAssignees", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/IssueBulkAssigneesOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/IssueBulkResult" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, + "/repos/{owner}/{repo}/issues/bulk/labels": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Add, remove, or replace labels on multiple issues", + "operationId": "issueBulkSetLabels", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/IssueBulkLabelsOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/IssueBulkResult" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, + "/repos/{owner}/{repo}/issues/bulk/milestone": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Assign a milestone to multiple issues", + "operationId": "issueBulkSetMilestone", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/IssueBulkMilestoneOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/IssueBulkResult" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, + "/repos/{owner}/{repo}/issues/bulk/state": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Close or reopen multiple issues", + "operationId": "issueBulkSetState", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/IssueBulkStateOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/IssueBulkResult" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/repos/{owner}/{repo}/issues/comments": { "get": { "produces": [ @@ -13382,6 +14432,76 @@ } } }, + "/repos/{owner}/{repo}/manifest": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get repo manifest settings", + "operationId": "repoGetManifest", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Manifest" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Update repo manifest settings", + "operationId": "repoUpdateManifest", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Manifest" + } + } + } + }, "/repos/{owner}/{repo}/media/{filepath}": { "get": { "produces": [ @@ -18159,6 +19279,61 @@ } } }, + "/repos/{owner}/{repo}/wiki/search": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Search wiki pages", + "operationId": "repoSearchWikiPages", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "search query", + "name": "q", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "SearchResults" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{template_owner}/{template_repo}/generate": { "post": { "consumes": [ @@ -21460,6 +22635,59 @@ } } }, + "/users/{username}/tokens/{id}": { + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Update an access token's scopes", + "operationId": "userUpdateAccessToken", + "parameters": [ + { + "type": "string", + "description": "username of the user whose token is to be updated", + "name": "username", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the token to update", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/EditAccessTokenOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/AccessToken" + }, + "400": { + "$ref": "#/responses/error" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/users/{username}/tokens/{token}": { "delete": { "produces": [ @@ -22611,6 +23839,28 @@ "format": "date-time", "x-go-name": "Created" }, + "delete_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "DeleteAllowlistActionsUser" + }, + "delete_allowlist_deploy_keys": { + "type": "boolean", + "x-go-name": "DeleteAllowlistDeployKeys" + }, + "delete_allowlist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "DeleteAllowlistTeams" + }, + "delete_allowlist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "DeleteAllowlistUsernames" + }, "dismiss_stale_approvals": { "type": "boolean", "x-go-name": "DismissStaleApprovals" @@ -22619,6 +23869,14 @@ "type": "boolean", "x-go-name": "EnableApprovalsWhitelist" }, + "enable_delete": { + "type": "boolean", + "x-go-name": "EnableDelete" + }, + "enable_delete_allowlist": { + "type": "boolean", + "x-go-name": "EnableDeleteAllowlist" + }, "enable_force_push": { "type": "boolean", "x-go-name": "EnableForcePush" @@ -22643,6 +23901,10 @@ "type": "boolean", "x-go-name": "EnableStatusCheck" }, + "force_push_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "ForcePushAllowlistActionsUser" + }, "force_push_allowlist_deploy_keys": { "type": "boolean", "x-go-name": "ForcePushAllowlistDeployKeys" @@ -22665,6 +23927,15 @@ "type": "boolean", "x-go-name": "IgnoreStaleApprovals" }, + "inherited_from": { + "description": "InheritedFrom indicates where this rule originates (\"org\" if inherited from org-level rules, empty if repo-level)", + "type": "string", + "x-go-name": "InheritedFrom" + }, + "merge_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "MergeWhitelistActionsUser" + }, "merge_whitelist_teams": { "type": "array", "items": { @@ -22689,6 +23960,10 @@ "type": "string", "x-go-name": "ProtectedFilePatterns" }, + "push_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "PushWhitelistActionsUser" + }, "push_whitelist_deploy_keys": { "type": "boolean", "x-go-name": "PushWhitelistDeployKeys" @@ -23462,6 +24737,28 @@ "type": "string", "x-go-name": "BranchName" }, + "delete_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "DeleteAllowlistActionsUser" + }, + "delete_allowlist_deploy_keys": { + "type": "boolean", + "x-go-name": "DeleteAllowlistDeployKeys" + }, + "delete_allowlist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "DeleteAllowlistTeams" + }, + "delete_allowlist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "DeleteAllowlistUsernames" + }, "dismiss_stale_approvals": { "type": "boolean", "x-go-name": "DismissStaleApprovals" @@ -23470,6 +24767,14 @@ "type": "boolean", "x-go-name": "EnableApprovalsWhitelist" }, + "enable_delete": { + "type": "boolean", + "x-go-name": "EnableDelete" + }, + "enable_delete_allowlist": { + "type": "boolean", + "x-go-name": "EnableDeleteAllowlist" + }, "enable_force_push": { "type": "boolean", "x-go-name": "EnableForcePush" @@ -23494,6 +24799,10 @@ "type": "boolean", "x-go-name": "EnableStatusCheck" }, + "force_push_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "ForcePushAllowlistActionsUser" + }, "force_push_allowlist_deploy_keys": { "type": "boolean", "x-go-name": "ForcePushAllowlistDeployKeys" @@ -23516,6 +24825,10 @@ "type": "boolean", "x-go-name": "IgnoreStaleApprovals" }, + "merge_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "MergeWhitelistActionsUser" + }, "merge_whitelist_teams": { "type": "array", "items": { @@ -23539,6 +24852,10 @@ "type": "string", "x-go-name": "ProtectedFilePatterns" }, + "push_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "PushWhitelistActionsUser" + }, "push_whitelist_deploy_keys": { "type": "boolean", "x-go-name": "PushWhitelistDeployKeys" @@ -23821,6 +25138,14 @@ "type": "boolean", "x-go-name": "Closed" }, + "custom_fields": { + "description": "custom field values keyed by field name", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "CustomFields" + }, "due_date": { "type": "string", "format": "date-time", @@ -23841,6 +25166,11 @@ "format": "int64", "x-go-name": "Milestone" }, + "priority_id": { + "type": "integer", + "format": "int64", + "x-go-name": "PriorityID" + }, "projects": { "description": "list of project ids", "type": "array", @@ -23854,9 +25184,20 @@ "type": "string", "x-go-name": "Ref" }, + "status_id": { + "description": "org-level issue metadata IDs (auto-assigned from org defaults when 0)", + "type": "integer", + "format": "int64", + "x-go-name": "StatusID" + }, "title": { "type": "string", "x-go-name": "Title" + }, + "type_id": { + "type": "integer", + "format": "int64", + "x-go-name": "TypeID" } }, "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" @@ -24004,6 +25345,192 @@ }, "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "CreateOrgBranchProtectionOption": { + "description": "CreateOrgBranchProtectionOption options for creating an org-level branch protection", + "type": "object", + "properties": { + "approvals_whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ApprovalsWhitelistTeams" + }, + "approvals_whitelist_username": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ApprovalsWhitelistUsernames" + }, + "block_admin_merge_override": { + "type": "boolean", + "x-go-name": "BlockAdminMergeOverride" + }, + "block_on_official_review_requests": { + "type": "boolean", + "x-go-name": "BlockOnOfficialReviewRequests" + }, + "block_on_outdated_branch": { + "type": "boolean", + "x-go-name": "BlockOnOutdatedBranch" + }, + "block_on_rejected_reviews": { + "type": "boolean", + "x-go-name": "BlockOnRejectedReviews" + }, + "delete_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "DeleteAllowlistActionsUser" + }, + "delete_allowlist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "DeleteAllowlistTeams" + }, + "delete_allowlist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "DeleteAllowlistUsernames" + }, + "dismiss_stale_approvals": { + "type": "boolean", + "x-go-name": "DismissStaleApprovals" + }, + "enable_approvals_whitelist": { + "type": "boolean", + "x-go-name": "EnableApprovalsWhitelist" + }, + "enable_delete": { + "type": "boolean", + "x-go-name": "EnableDelete" + }, + "enable_delete_allowlist": { + "type": "boolean", + "x-go-name": "EnableDeleteAllowlist" + }, + "enable_force_push": { + "type": "boolean", + "x-go-name": "EnableForcePush" + }, + "enable_force_push_allowlist": { + "type": "boolean", + "x-go-name": "EnableForcePushAllowlist" + }, + "enable_merge_whitelist": { + "type": "boolean", + "x-go-name": "EnableMergeWhitelist" + }, + "enable_push": { + "type": "boolean", + "x-go-name": "EnablePush" + }, + "enable_push_whitelist": { + "type": "boolean", + "x-go-name": "EnablePushWhitelist" + }, + "enable_status_check": { + "type": "boolean", + "x-go-name": "EnableStatusCheck" + }, + "force_push_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "ForcePushAllowlistActionsUser" + }, + "force_push_allowlist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ForcePushAllowlistTeams" + }, + "force_push_allowlist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ForcePushAllowlistUsernames" + }, + "ignore_stale_approvals": { + "type": "boolean", + "x-go-name": "IgnoreStaleApprovals" + }, + "merge_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "MergeWhitelistActionsUser" + }, + "merge_whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "MergeWhitelistTeams" + }, + "merge_whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "MergeWhitelistUsernames" + }, + "priority": { + "type": "integer", + "format": "int64", + "x-go-name": "Priority" + }, + "protected_file_patterns": { + "type": "string", + "x-go-name": "ProtectedFilePatterns" + }, + "push_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "PushWhitelistActionsUser" + }, + "push_whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "PushWhitelistTeams" + }, + "push_whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "PushWhitelistUsernames" + }, + "require_signed_commits": { + "type": "boolean", + "x-go-name": "RequireSignedCommits" + }, + "required_approvals": { + "type": "integer", + "format": "int64", + "x-go-name": "RequiredApprovals" + }, + "rule_name": { + "type": "string", + "x-go-name": "RuleName" + }, + "status_check_contexts": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "StatusCheckContexts" + }, + "unprotected_file_patterns": { + "type": "string", + "x-go-name": "UnprotectedFilePatterns" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "CreateOrgOption": { "description": "CreateOrgOption options for creating an organization", "type": "object", @@ -24060,6 +25587,24 @@ }, "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "CreateOrgTagProtectionOption": { + "description": "CreateOrgTagProtectionOption options for creating an org-level tag protection", + "type": "object", + "properties": { + "name_pattern": { + "type": "string", + "x-go-name": "NamePattern" + }, + "whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistTeams" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "CreatePullRequestOption": { "description": "CreatePullRequestOption options when creating a pull request", "type": "object", @@ -24809,6 +26354,30 @@ }, "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "EditAccessTokenOption": { + "description": "EditAccessTokenOption options when editing access token scopes", + "type": "object", + "properties": { + "name": { + "description": "The new name for the token (optional)", + "type": "string", + "x-go-name": "Name" + }, + "scopes": { + "description": "The new scopes for the token", + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Scopes", + "example": [ + "read:repository", + "write:issue" + ] + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "EditActionRunnerOption": { "type": "object", "title": "EditActionRunnerOption represents the editable fields for a runner.", @@ -24869,6 +26438,28 @@ "type": "boolean", "x-go-name": "BlockOnRejectedReviews" }, + "delete_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "DeleteAllowlistActionsUser" + }, + "delete_allowlist_deploy_keys": { + "type": "boolean", + "x-go-name": "DeleteAllowlistDeployKeys" + }, + "delete_allowlist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "DeleteAllowlistTeams" + }, + "delete_allowlist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "DeleteAllowlistUsernames" + }, "dismiss_stale_approvals": { "type": "boolean", "x-go-name": "DismissStaleApprovals" @@ -24877,6 +26468,14 @@ "type": "boolean", "x-go-name": "EnableApprovalsWhitelist" }, + "enable_delete": { + "type": "boolean", + "x-go-name": "EnableDelete" + }, + "enable_delete_allowlist": { + "type": "boolean", + "x-go-name": "EnableDeleteAllowlist" + }, "enable_force_push": { "type": "boolean", "x-go-name": "EnableForcePush" @@ -24901,6 +26500,10 @@ "type": "boolean", "x-go-name": "EnableStatusCheck" }, + "force_push_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "ForcePushAllowlistActionsUser" + }, "force_push_allowlist_deploy_keys": { "type": "boolean", "x-go-name": "ForcePushAllowlistDeployKeys" @@ -24923,6 +26526,10 @@ "type": "boolean", "x-go-name": "IgnoreStaleApprovals" }, + "merge_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "MergeWhitelistActionsUser" + }, "merge_whitelist_teams": { "type": "array", "items": { @@ -24946,6 +26553,10 @@ "type": "string", "x-go-name": "ProtectedFilePatterns" }, + "push_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "PushWhitelistActionsUser" + }, "push_whitelist_deploy_keys": { "type": "boolean", "x-go-name": "PushWhitelistDeployKeys" @@ -25107,6 +26718,11 @@ "format": "int64", "x-go-name": "Milestone" }, + "priority_id": { + "type": "integer", + "format": "int64", + "x-go-name": "PriorityID" + }, "projects": { "description": "list of project ids to set (replaces existing projects)", "type": "array", @@ -25124,10 +26740,21 @@ "type": "string", "x-go-name": "State" }, + "status_id": { + "description": "org-level issue metadata IDs", + "type": "integer", + "format": "int64", + "x-go-name": "StatusID" + }, "title": { "type": "string", "x-go-name": "Title" }, + "type_id": { + "type": "integer", + "format": "int64", + "x-go-name": "TypeID" + }, "unset_due_date": { "type": "boolean", "x-go-name": "RemoveDeadline" @@ -25199,6 +26826,199 @@ }, "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "EditOrgBranchProtectionOption": { + "description": "EditOrgBranchProtectionOption options for editing an org-level branch protection", + "type": "object", + "properties": { + "approvals_whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ApprovalsWhitelistTeams" + }, + "approvals_whitelist_username": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ApprovalsWhitelistUsernames" + }, + "block_admin_merge_override": { + "type": "boolean", + "x-go-name": "BlockAdminMergeOverride" + }, + "block_on_official_review_requests": { + "type": "boolean", + "x-go-name": "BlockOnOfficialReviewRequests" + }, + "block_on_outdated_branch": { + "type": "boolean", + "x-go-name": "BlockOnOutdatedBranch" + }, + "block_on_rejected_reviews": { + "type": "boolean", + "x-go-name": "BlockOnRejectedReviews" + }, + "delete_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "DeleteAllowlistActionsUser" + }, + "delete_allowlist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "DeleteAllowlistTeams" + }, + "delete_allowlist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "DeleteAllowlistUsernames" + }, + "dismiss_stale_approvals": { + "type": "boolean", + "x-go-name": "DismissStaleApprovals" + }, + "enable_approvals_whitelist": { + "type": "boolean", + "x-go-name": "EnableApprovalsWhitelist" + }, + "enable_delete": { + "type": "boolean", + "x-go-name": "EnableDelete" + }, + "enable_delete_allowlist": { + "type": "boolean", + "x-go-name": "EnableDeleteAllowlist" + }, + "enable_force_push": { + "type": "boolean", + "x-go-name": "EnableForcePush" + }, + "enable_force_push_allowlist": { + "type": "boolean", + "x-go-name": "EnableForcePushAllowlist" + }, + "enable_merge_whitelist": { + "type": "boolean", + "x-go-name": "EnableMergeWhitelist" + }, + "enable_push": { + "type": "boolean", + "x-go-name": "EnablePush" + }, + "enable_push_whitelist": { + "type": "boolean", + "x-go-name": "EnablePushWhitelist" + }, + "enable_status_check": { + "type": "boolean", + "x-go-name": "EnableStatusCheck" + }, + "force_push_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "ForcePushAllowlistActionsUser" + }, + "force_push_allowlist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ForcePushAllowlistTeams" + }, + "force_push_allowlist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ForcePushAllowlistUsernames" + }, + "ignore_stale_approvals": { + "type": "boolean", + "x-go-name": "IgnoreStaleApprovals" + }, + "merge_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "MergeWhitelistActionsUser" + }, + "merge_whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "MergeWhitelistTeams" + }, + "merge_whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "MergeWhitelistUsernames" + }, + "priority": { + "type": "integer", + "format": "int64", + "x-go-name": "Priority" + }, + "protected_file_patterns": { + "type": "string", + "x-go-name": "ProtectedFilePatterns" + }, + "push_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "PushWhitelistActionsUser" + }, + "push_whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "PushWhitelistTeams" + }, + "push_whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "PushWhitelistUsernames" + }, + "require_signed_commits": { + "type": "boolean", + "x-go-name": "RequireSignedCommits" + }, + "required_approvals": { + "type": "integer", + "format": "int64", + "x-go-name": "RequiredApprovals" + }, + "status_check_contexts": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "StatusCheckContexts" + }, + "unprotected_file_patterns": { + "type": "string", + "x-go-name": "UnprotectedFilePatterns" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "EditOrgEmailDomainPolicyOption": { + "description": "EditOrgEmailDomainPolicyOption options for editing an org's email domain policy", + "type": "object", + "properties": { + "allowed_domains": { + "type": "string", + "x-go-name": "AllowedDomains" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "EditOrgOption": { "description": "EditOrgOption options for editing an organization", "type": "object", @@ -25247,6 +27067,95 @@ }, "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "EditOrgPushPolicyOption": { + "description": "EditOrgPushPolicyOption options for editing an organization's push policy. Only\nfields that are set will be changed.", + "type": "object", + "properties": { + "blocked_file_patterns": { + "type": "string", + "x-go-name": "BlockedFilePatterns" + }, + "branch_name_pattern": { + "type": "string", + "x-go-name": "BranchNamePattern" + }, + "max_file_size": { + "type": "integer", + "format": "int64", + "x-go-name": "MaxFileSize" + }, + "require_secret_block": { + "type": "boolean", + "x-go-name": "RequireSecretBlock" + }, + "tag_name_pattern": { + "type": "string", + "x-go-name": "TagNamePattern" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "EditOrgRepoDefaultsOption": { + "description": "EditOrgRepoDefaultsOption options for editing an org's repo defaults. Only fields\nthat are set will be changed.", + "type": "object", + "properties": { + "allow_fast_forward_only": { + "type": "boolean", + "x-go-name": "AllowFastForwardOnly" + }, + "allow_merge": { + "type": "boolean", + "x-go-name": "AllowMerge" + }, + "allow_rebase": { + "type": "boolean", + "x-go-name": "AllowRebase" + }, + "allow_rebase_merge": { + "type": "boolean", + "x-go-name": "AllowRebaseMerge" + }, + "allow_squash": { + "type": "boolean", + "x-go-name": "AllowSquash" + }, + "apply_pr_defaults": { + "type": "boolean", + "x-go-name": "ApplyPRDefaults" + }, + "default_merge_style": { + "type": "string", + "x-go-name": "DefaultMergeStyle" + }, + "delete_branch_after_merge": { + "type": "boolean", + "x-go-name": "DeleteBranchAfterMerge" + }, + "force_private": { + "type": "boolean", + "x-go-name": "ForcePrivate" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "EditOrgTagProtectionOption": { + "description": "EditOrgTagProtectionOption options for editing an org-level tag protection", + "type": "object", + "properties": { + "name_pattern": { + "type": "string", + "x-go-name": "NamePattern" + }, + "whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistTeams" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "EditPullRequestOption": { "description": "EditPullRequestOption options when modify pull request", "type": "object", @@ -26655,6 +28564,15 @@ "format": "int64", "x-go-name": "PinOrder" }, + "priority_id": { + "type": "integer", + "format": "int64", + "x-go-name": "PriorityID" + }, + "priority_name": { + "type": "string", + "x-go-name": "PriorityName" + }, "projects": { "type": "array", "items": { @@ -26681,6 +28599,16 @@ "x-go-enum-desc": "open StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed", "x-go-name": "State" }, + "status_id": { + "description": "Issue metadata (org-level definitions)", + "type": "integer", + "format": "int64", + "x-go-name": "StatusID" + }, + "status_name": { + "type": "string", + "x-go-name": "StatusName" + }, "time_estimate": { "type": "integer", "format": "int64", @@ -26690,6 +28618,15 @@ "type": "string", "x-go-name": "Title" }, + "type_id": { + "type": "integer", + "format": "int64", + "x-go-name": "TypeID" + }, + "type_name": { + "type": "string", + "x-go-name": "TypeName" + }, "updated_at": { "type": "string", "format": "date-time", @@ -26705,6 +28642,139 @@ }, "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "IssueBulkAssigneesOption": { + "description": "IssueBulkAssigneesOption options for bulk assignee operations on issues", + "type": "object", + "required": [ + "issues", + "assignees" + ], + "properties": { + "assignees": { + "description": "list of assignee usernames to add or set", + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Assignees" + }, + "issues": { + "description": "issue indexes to operate on", + "type": "array", + "items": { + "type": "integer", + "format": "int64" + }, + "x-go-name": "Issues" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "IssueBulkLabelsOption": { + "description": "IssueBulkLabelsOption options for bulk label operations on issues", + "type": "object", + "required": [ + "issues", + "labels" + ], + "properties": { + "action": { + "description": "action to perform: \"add\" (default), \"remove\", or \"replace\"", + "type": "string", + "x-go-name": "Action" + }, + "issues": { + "description": "issue indexes to operate on", + "type": "array", + "items": { + "type": "integer", + "format": "int64" + }, + "x-go-name": "Issues" + }, + "labels": { + "description": "Labels can be a list of integers representing label IDs\nor a list of strings representing label names", + "type": "array", + "items": {}, + "x-go-name": "Labels" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "IssueBulkMilestoneOption": { + "description": "IssueBulkMilestoneOption options for bulk milestone assignment on issues", + "type": "object", + "required": [ + "issues", + "milestone_id" + ], + "properties": { + "issues": { + "description": "issue indexes to operate on", + "type": "array", + "items": { + "type": "integer", + "format": "int64" + }, + "x-go-name": "Issues" + }, + "milestone_id": { + "description": "milestone id to assign, 0 to remove milestone", + "type": "integer", + "format": "int64", + "x-go-name": "MilestoneID" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "IssueBulkResult": { + "description": "IssueBulkResult represents the result of a bulk issue operation", + "type": "object", + "properties": { + "failure_count": { + "description": "count of issues that failed to update", + "type": "integer", + "format": "int64", + "x-go-name": "FailureCount" + }, + "failures": { + "description": "details of failures, keyed by issue index", + "x-go-name": "Failures" + }, + "success_count": { + "description": "count of successfully updated issues", + "type": "integer", + "format": "int64", + "x-go-name": "SuccessCount" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "IssueBulkStateOption": { + "description": "IssueBulkStateOption options for bulk state changes on issues", + "type": "object", + "required": [ + "issues", + "state" + ], + "properties": { + "issues": { + "description": "issue indexes to operate on", + "type": "array", + "items": { + "type": "integer", + "format": "int64" + }, + "x-go-name": "Issues" + }, + "state": { + "description": "new state for the issues, \"open\" or \"closed\"", + "type": "string", + "x-go-name": "State" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "IssueConfig": { "type": "object", "properties": { @@ -26845,6 +28915,76 @@ }, "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "IssuePriorityDef": { + "description": "IssuePriorityDef represents an org-level issue priority definition", + "type": "object", + "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, + "description": { + "type": "string", + "x-go-name": "Description" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "is_default": { + "type": "boolean", + "x-go-name": "IsDefault" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "sort_order": { + "type": "integer", + "format": "int64", + "x-go-name": "SortOrder" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "IssueStatusDef": { + "description": "IssueStatusDef represents an org-level issue status definition", + "type": "object", + "properties": { + "closes_issue": { + "type": "boolean", + "x-go-name": "ClosesIssue" + }, + "color": { + "type": "string", + "x-go-name": "Color" + }, + "description": { + "type": "string", + "x-go-name": "Description" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "is_required": { + "type": "boolean", + "x-go-name": "IsRequired" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "sort_order": { + "type": "integer", + "format": "int64", + "x-go-name": "SortOrder" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "IssueTemplate": { "description": "IssueTemplate represents an issue template for a repository", "type": "object", @@ -26867,6 +29007,13 @@ "type": "string", "x-go-name": "Content" }, + "custom_fields": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "CustomFields" + }, "file_name": { "type": "string", "x-go-name": "FileName" @@ -26896,6 +29043,39 @@ }, "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "IssueTypeDef": { + "description": "IssueTypeDef represents an org-level issue type definition", + "type": "object", + "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, + "description": { + "type": "string", + "x-go-name": "Description" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "is_default": { + "type": "boolean", + "x-go-name": "IsDefault" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "sort_order": { + "type": "integer", + "format": "int64", + "x-go-name": "SortOrder" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "Label": { "description": "Label a label to an issue or a pr", "type": "object", @@ -27660,6 +29840,357 @@ }, "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "OrgBranchProtection": { + "description": "OrgBranchProtection represents an org-level branch protection ruleset", + "type": "object", + "properties": { + "approvals_whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ApprovalsWhitelistTeams" + }, + "approvals_whitelist_username": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ApprovalsWhitelistUsernames" + }, + "block_admin_merge_override": { + "type": "boolean", + "x-go-name": "BlockAdminMergeOverride" + }, + "block_on_official_review_requests": { + "type": "boolean", + "x-go-name": "BlockOnOfficialReviewRequests" + }, + "block_on_outdated_branch": { + "type": "boolean", + "x-go-name": "BlockOnOutdatedBranch" + }, + "block_on_rejected_reviews": { + "type": "boolean", + "x-go-name": "BlockOnRejectedReviews" + }, + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Created" + }, + "delete_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "DeleteAllowlistActionsUser" + }, + "delete_allowlist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "DeleteAllowlistTeams" + }, + "delete_allowlist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "DeleteAllowlistUsernames" + }, + "dismiss_stale_approvals": { + "type": "boolean", + "x-go-name": "DismissStaleApprovals" + }, + "enable_approvals_whitelist": { + "type": "boolean", + "x-go-name": "EnableApprovalsWhitelist" + }, + "enable_delete": { + "type": "boolean", + "x-go-name": "EnableDelete" + }, + "enable_delete_allowlist": { + "type": "boolean", + "x-go-name": "EnableDeleteAllowlist" + }, + "enable_force_push": { + "type": "boolean", + "x-go-name": "EnableForcePush" + }, + "enable_force_push_allowlist": { + "type": "boolean", + "x-go-name": "EnableForcePushAllowlist" + }, + "enable_merge_whitelist": { + "type": "boolean", + "x-go-name": "EnableMergeWhitelist" + }, + "enable_push": { + "type": "boolean", + "x-go-name": "EnablePush" + }, + "enable_push_whitelist": { + "type": "boolean", + "x-go-name": "EnablePushWhitelist" + }, + "enable_status_check": { + "type": "boolean", + "x-go-name": "EnableStatusCheck" + }, + "force_push_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "ForcePushAllowlistActionsUser" + }, + "force_push_allowlist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ForcePushAllowlistTeams" + }, + "force_push_allowlist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "ForcePushAllowlistUsernames" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "ignore_stale_approvals": { + "type": "boolean", + "x-go-name": "IgnoreStaleApprovals" + }, + "merge_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "MergeWhitelistActionsUser" + }, + "merge_whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "MergeWhitelistTeams" + }, + "merge_whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "MergeWhitelistUsernames" + }, + "org_id": { + "type": "integer", + "format": "int64", + "x-go-name": "OrgID" + }, + "priority": { + "type": "integer", + "format": "int64", + "x-go-name": "Priority" + }, + "protected_file_patterns": { + "type": "string", + "x-go-name": "ProtectedFilePatterns" + }, + "push_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "PushWhitelistActionsUser" + }, + "push_whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "PushWhitelistTeams" + }, + "push_whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "PushWhitelistUsernames" + }, + "require_signed_commits": { + "type": "boolean", + "x-go-name": "RequireSignedCommits" + }, + "required_approvals": { + "type": "integer", + "format": "int64", + "x-go-name": "RequiredApprovals" + }, + "rule_name": { + "type": "string", + "x-go-name": "RuleName" + }, + "status_check_contexts": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "StatusCheckContexts" + }, + "unprotected_file_patterns": { + "type": "string", + "x-go-name": "UnprotectedFilePatterns" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Updated" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "OrgEmailDomainPolicy": { + "description": "OrgEmailDomainPolicy represents an organization's email domain policy", + "type": "object", + "properties": { + "allowed_domains": { + "type": "string", + "x-go-name": "AllowedDomains" + }, + "org_id": { + "type": "integer", + "format": "int64", + "x-go-name": "OrgID" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "OrgPushPolicy": { + "description": "OrgPushPolicy represents an organization's push policy (one per org)", + "type": "object", + "properties": { + "blocked_file_patterns": { + "type": "string", + "x-go-name": "BlockedFilePatterns" + }, + "branch_name_pattern": { + "type": "string", + "x-go-name": "BranchNamePattern" + }, + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Created" + }, + "max_file_size": { + "type": "integer", + "format": "int64", + "x-go-name": "MaxFileSize" + }, + "org_id": { + "type": "integer", + "format": "int64", + "x-go-name": "OrgID" + }, + "require_secret_block": { + "type": "boolean", + "x-go-name": "RequireSecretBlock" + }, + "tag_name_pattern": { + "type": "string", + "x-go-name": "TagNamePattern" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Updated" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "OrgRepoDefaults": { + "description": "OrgRepoDefaults represents an organization's default repository settings", + "type": "object", + "properties": { + "allow_fast_forward_only": { + "type": "boolean", + "x-go-name": "AllowFastForwardOnly" + }, + "allow_merge": { + "type": "boolean", + "x-go-name": "AllowMerge" + }, + "allow_rebase": { + "type": "boolean", + "x-go-name": "AllowRebase" + }, + "allow_rebase_merge": { + "type": "boolean", + "x-go-name": "AllowRebaseMerge" + }, + "allow_squash": { + "type": "boolean", + "x-go-name": "AllowSquash" + }, + "apply_pr_defaults": { + "type": "boolean", + "x-go-name": "ApplyPRDefaults" + }, + "default_merge_style": { + "type": "string", + "x-go-name": "DefaultMergeStyle" + }, + "delete_branch_after_merge": { + "type": "boolean", + "x-go-name": "DeleteBranchAfterMerge" + }, + "force_private": { + "type": "boolean", + "x-go-name": "ForcePrivate" + }, + "org_id": { + "type": "integer", + "format": "int64", + "x-go-name": "OrgID" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "OrgTagProtection": { + "description": "OrgTagProtection represents an org-level tag protection rule", + "type": "object", + "properties": { + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Created" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "name_pattern": { + "type": "string", + "x-go-name": "NamePattern" + }, + "org_id": { + "type": "integer", + "format": "int64", + "x-go-name": "OrgID" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Updated" + }, + "whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistTeams" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "Organization": { "description": "Organization represents an organization", "type": "object", @@ -30266,6 +32797,122 @@ } }, "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "apiMetadata": { + "type": "object", + "title": "apiMetadata is the JSON representation of a repo manifest.", + "properties": { + "container_name": { + "type": "string", + "x-go-name": "ContainerName" + }, + "deploy_host": { + "description": "deploy", + "type": "string", + "x-go-name": "DeployHost" + }, + "deploy_path": { + "type": "string", + "x-go-name": "DeployPath" + }, + "deploy_port": { + "type": "string", + "x-go-name": "DeployPort" + }, + "deploy_user": { + "type": "string", + "x-go-name": "DeployUser" + }, + "description": { + "type": "string", + "x-go-name": "Description" + }, + "display_name": { + "type": "string", + "x-go-name": "DisplayName" + }, + "docker_image": { + "type": "string", + "x-go-name": "DockerImage" + }, + "docker_registry": { + "type": "string", + "x-go-name": "DockerRegistry" + }, + "element_name": { + "type": "string", + "x-go-name": "ElementName" + }, + "entry_point": { + "type": "string", + "x-go-name": "EntryPoint" + }, + "extension_type": { + "type": "string", + "x-go-name": "ExtensionType" + }, + "health_url": { + "type": "string", + "x-go-name": "HealthURL" + }, + "info_url": { + "type": "string", + "x-go-name": "InfoURL" + }, + "language": { + "type": "string", + "x-go-name": "Language" + }, + "license_name": { + "type": "string", + "x-go-name": "LicenseName" + }, + "license_spdx": { + "type": "string", + "x-go-name": "LicenseSPDX" + }, + "maintainer": { + "type": "string", + "x-go-name": "Maintainer" + }, + "maintainer_url": { + "type": "string", + "x-go-name": "MaintainerURL" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "org": { + "type": "string", + "x-go-name": "Org" + }, + "php_minimum": { + "type": "string", + "x-go-name": "PHPMinimum" + }, + "platform": { + "type": "string", + "x-go-name": "Platform" + }, + "standards_source": { + "type": "string", + "x-go-name": "StandardsSource" + }, + "standards_version": { + "type": "string", + "x-go-name": "StandardsVersion" + }, + "target_version": { + "type": "string", + "x-go-name": "TargetVersion" + }, + "version_prefix": { + "type": "string", + "x-go-name": "VersionPrefix" + } + }, + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/routers/api/v1/repo" } }, "responses": { @@ -30681,6 +33328,12 @@ "$ref": "#/definitions/Issue" } }, + "IssueBulkResult": { + "description": "IssueBulkResult", + "schema": { + "$ref": "#/definitions/IssueBulkResult" + } + }, "IssueDeadline": { "description": "IssueDeadline", "schema": { @@ -30772,6 +33425,12 @@ } } }, + "Manifest": { + "description": "Manifest", + "schema": { + "$ref": "#/definitions/apiMetadata" + } + }, "MarkdownRender": { "description": "MarkdownRender is a rendered markdown document", "schema": { @@ -30859,6 +33518,54 @@ } } }, + "OrgBranchProtection": { + "description": "OrgBranchProtection", + "schema": { + "$ref": "#/definitions/OrgBranchProtection" + } + }, + "OrgBranchProtectionList": { + "description": "OrgBranchProtectionList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/OrgBranchProtection" + } + } + }, + "OrgEmailDomainPolicy": { + "description": "OrgEmailDomainPolicy", + "schema": { + "$ref": "#/definitions/OrgEmailDomainPolicy" + } + }, + "OrgPushPolicy": { + "description": "OrgPushPolicy", + "schema": { + "$ref": "#/definitions/OrgPushPolicy" + } + }, + "OrgRepoDefaults": { + "description": "OrgRepoDefaults", + "schema": { + "$ref": "#/definitions/OrgRepoDefaults" + } + }, + "OrgTagProtection": { + "description": "OrgTagProtection", + "schema": { + "$ref": "#/definitions/OrgTagProtection" + } + }, + "OrgTagProtectionList": { + "description": "OrgTagProtectionList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/OrgTagProtection" + } + } + }, "Organization": { "description": "Organization", "schema": { diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index 971a9c847a..d47403d808 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -637,6 +637,16 @@ }, "description": "Issue" }, + "IssueBulkResult": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssueBulkResult" + } + } + }, + "description": "IssueBulkResult" + }, "IssueDeadline": { "content": { "application/json": { @@ -772,6 +782,16 @@ }, "description": "LicensesList" }, + "Manifest": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/apiMetadata" + } + } + }, + "description": "Manifest" + }, "MarkdownRender": { "content": { "application/json": { @@ -911,6 +931,82 @@ }, "description": "OAuth2ApplicationList represents a list of OAuth2 applications." }, + "OrgBranchProtection": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgBranchProtection" + } + } + }, + "description": "OrgBranchProtection" + }, + "OrgBranchProtectionList": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/OrgBranchProtection" + }, + "type": "array" + } + } + }, + "description": "OrgBranchProtectionList" + }, + "OrgEmailDomainPolicy": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgEmailDomainPolicy" + } + } + }, + "description": "OrgEmailDomainPolicy" + }, + "OrgPushPolicy": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgPushPolicy" + } + } + }, + "description": "OrgPushPolicy" + }, + "OrgRepoDefaults": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgRepoDefaults" + } + } + }, + "description": "OrgRepoDefaults" + }, + "OrgTagProtection": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTagProtection" + } + } + }, + "description": "OrgTagProtection" + }, + "OrgTagProtectionList": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/OrgTagProtection" + }, + "type": "array" + } + } + }, + "description": "OrgTagProtectionList" + }, "Organization": { "content": { "application/json": { @@ -2855,6 +2951,28 @@ "type": "string", "x-go-name": "Created" }, + "delete_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "DeleteAllowlistActionsUser" + }, + "delete_allowlist_deploy_keys": { + "type": "boolean", + "x-go-name": "DeleteAllowlistDeployKeys" + }, + "delete_allowlist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "DeleteAllowlistTeams" + }, + "delete_allowlist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "DeleteAllowlistUsernames" + }, "dismiss_stale_approvals": { "type": "boolean", "x-go-name": "DismissStaleApprovals" @@ -2863,6 +2981,14 @@ "type": "boolean", "x-go-name": "EnableApprovalsWhitelist" }, + "enable_delete": { + "type": "boolean", + "x-go-name": "EnableDelete" + }, + "enable_delete_allowlist": { + "type": "boolean", + "x-go-name": "EnableDeleteAllowlist" + }, "enable_force_push": { "type": "boolean", "x-go-name": "EnableForcePush" @@ -2887,6 +3013,10 @@ "type": "boolean", "x-go-name": "EnableStatusCheck" }, + "force_push_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "ForcePushAllowlistActionsUser" + }, "force_push_allowlist_deploy_keys": { "type": "boolean", "x-go-name": "ForcePushAllowlistDeployKeys" @@ -2909,6 +3039,15 @@ "type": "boolean", "x-go-name": "IgnoreStaleApprovals" }, + "inherited_from": { + "description": "InheritedFrom indicates where this rule originates (\"org\" if inherited from org-level rules, empty if repo-level)", + "type": "string", + "x-go-name": "InheritedFrom" + }, + "merge_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "MergeWhitelistActionsUser" + }, "merge_whitelist_teams": { "items": { "type": "string" @@ -2933,6 +3072,10 @@ "type": "string", "x-go-name": "ProtectedFilePatterns" }, + "push_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "PushWhitelistActionsUser" + }, "push_whitelist_deploy_keys": { "type": "boolean", "x-go-name": "PushWhitelistDeployKeys" @@ -3724,6 +3867,28 @@ "type": "string", "x-go-name": "BranchName" }, + "delete_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "DeleteAllowlistActionsUser" + }, + "delete_allowlist_deploy_keys": { + "type": "boolean", + "x-go-name": "DeleteAllowlistDeployKeys" + }, + "delete_allowlist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "DeleteAllowlistTeams" + }, + "delete_allowlist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "DeleteAllowlistUsernames" + }, "dismiss_stale_approvals": { "type": "boolean", "x-go-name": "DismissStaleApprovals" @@ -3732,6 +3897,14 @@ "type": "boolean", "x-go-name": "EnableApprovalsWhitelist" }, + "enable_delete": { + "type": "boolean", + "x-go-name": "EnableDelete" + }, + "enable_delete_allowlist": { + "type": "boolean", + "x-go-name": "EnableDeleteAllowlist" + }, "enable_force_push": { "type": "boolean", "x-go-name": "EnableForcePush" @@ -3756,6 +3929,10 @@ "type": "boolean", "x-go-name": "EnableStatusCheck" }, + "force_push_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "ForcePushAllowlistActionsUser" + }, "force_push_allowlist_deploy_keys": { "type": "boolean", "x-go-name": "ForcePushAllowlistDeployKeys" @@ -3778,6 +3955,10 @@ "type": "boolean", "x-go-name": "IgnoreStaleApprovals" }, + "merge_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "MergeWhitelistActionsUser" + }, "merge_whitelist_teams": { "items": { "type": "string" @@ -3801,6 +3982,10 @@ "type": "string", "x-go-name": "ProtectedFilePatterns" }, + "push_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "PushWhitelistActionsUser" + }, "push_whitelist_deploy_keys": { "type": "boolean", "x-go-name": "PushWhitelistDeployKeys" @@ -4082,6 +4267,14 @@ "type": "boolean", "x-go-name": "Closed" }, + "custom_fields": { + "additionalProperties": { + "type": "string" + }, + "description": "custom field values keyed by field name", + "type": "object", + "x-go-name": "CustomFields" + }, "due_date": { "format": "date-time", "type": "string", @@ -4102,6 +4295,11 @@ "type": "integer", "x-go-name": "Milestone" }, + "priority_id": { + "format": "int64", + "type": "integer", + "x-go-name": "PriorityID" + }, "projects": { "description": "list of project ids", "items": { @@ -4115,9 +4313,20 @@ "type": "string", "x-go-name": "Ref" }, + "status_id": { + "description": "org-level issue metadata IDs (auto-assigned from org defaults when 0)", + "format": "int64", + "type": "integer", + "x-go-name": "StatusID" + }, "title": { "type": "string", "x-go-name": "Title" + }, + "type_id": { + "format": "int64", + "type": "integer", + "x-go-name": "TypeID" } }, "required": [ @@ -4264,6 +4473,192 @@ "type": "object", "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "CreateOrgBranchProtectionOption": { + "description": "CreateOrgBranchProtectionOption options for creating an org-level branch protection", + "properties": { + "approvals_whitelist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "ApprovalsWhitelistTeams" + }, + "approvals_whitelist_username": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "ApprovalsWhitelistUsernames" + }, + "block_admin_merge_override": { + "type": "boolean", + "x-go-name": "BlockAdminMergeOverride" + }, + "block_on_official_review_requests": { + "type": "boolean", + "x-go-name": "BlockOnOfficialReviewRequests" + }, + "block_on_outdated_branch": { + "type": "boolean", + "x-go-name": "BlockOnOutdatedBranch" + }, + "block_on_rejected_reviews": { + "type": "boolean", + "x-go-name": "BlockOnRejectedReviews" + }, + "delete_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "DeleteAllowlistActionsUser" + }, + "delete_allowlist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "DeleteAllowlistTeams" + }, + "delete_allowlist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "DeleteAllowlistUsernames" + }, + "dismiss_stale_approvals": { + "type": "boolean", + "x-go-name": "DismissStaleApprovals" + }, + "enable_approvals_whitelist": { + "type": "boolean", + "x-go-name": "EnableApprovalsWhitelist" + }, + "enable_delete": { + "type": "boolean", + "x-go-name": "EnableDelete" + }, + "enable_delete_allowlist": { + "type": "boolean", + "x-go-name": "EnableDeleteAllowlist" + }, + "enable_force_push": { + "type": "boolean", + "x-go-name": "EnableForcePush" + }, + "enable_force_push_allowlist": { + "type": "boolean", + "x-go-name": "EnableForcePushAllowlist" + }, + "enable_merge_whitelist": { + "type": "boolean", + "x-go-name": "EnableMergeWhitelist" + }, + "enable_push": { + "type": "boolean", + "x-go-name": "EnablePush" + }, + "enable_push_whitelist": { + "type": "boolean", + "x-go-name": "EnablePushWhitelist" + }, + "enable_status_check": { + "type": "boolean", + "x-go-name": "EnableStatusCheck" + }, + "force_push_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "ForcePushAllowlistActionsUser" + }, + "force_push_allowlist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "ForcePushAllowlistTeams" + }, + "force_push_allowlist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "ForcePushAllowlistUsernames" + }, + "ignore_stale_approvals": { + "type": "boolean", + "x-go-name": "IgnoreStaleApprovals" + }, + "merge_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "MergeWhitelistActionsUser" + }, + "merge_whitelist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "MergeWhitelistTeams" + }, + "merge_whitelist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "MergeWhitelistUsernames" + }, + "priority": { + "format": "int64", + "type": "integer", + "x-go-name": "Priority" + }, + "protected_file_patterns": { + "type": "string", + "x-go-name": "ProtectedFilePatterns" + }, + "push_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "PushWhitelistActionsUser" + }, + "push_whitelist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "PushWhitelistTeams" + }, + "push_whitelist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "PushWhitelistUsernames" + }, + "require_signed_commits": { + "type": "boolean", + "x-go-name": "RequireSignedCommits" + }, + "required_approvals": { + "format": "int64", + "type": "integer", + "x-go-name": "RequiredApprovals" + }, + "rule_name": { + "type": "string", + "x-go-name": "RuleName" + }, + "status_check_contexts": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "StatusCheckContexts" + }, + "unprotected_file_patterns": { + "type": "string", + "x-go-name": "UnprotectedFilePatterns" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "CreateOrgOption": { "description": "CreateOrgOption options for creating an organization", "properties": { @@ -4317,6 +4712,24 @@ "type": "object", "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "CreateOrgTagProtectionOption": { + "description": "CreateOrgTagProtectionOption options for creating an org-level tag protection", + "properties": { + "name_pattern": { + "type": "string", + "x-go-name": "NamePattern" + }, + "whitelist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "WhitelistTeams" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "CreatePullRequestOption": { "description": "CreatePullRequestOption options when creating a pull request", "properties": { @@ -5041,6 +5454,30 @@ "type": "object", "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "EditAccessTokenOption": { + "description": "EditAccessTokenOption options when editing access token scopes", + "properties": { + "name": { + "description": "The new name for the token (optional)", + "type": "string", + "x-go-name": "Name" + }, + "scopes": { + "description": "The new scopes for the token", + "example": [ + "read:repository", + "write:issue" + ], + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "Scopes" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "EditActionRunnerOption": { "properties": { "disabled": { @@ -5100,6 +5537,28 @@ "type": "boolean", "x-go-name": "BlockOnRejectedReviews" }, + "delete_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "DeleteAllowlistActionsUser" + }, + "delete_allowlist_deploy_keys": { + "type": "boolean", + "x-go-name": "DeleteAllowlistDeployKeys" + }, + "delete_allowlist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "DeleteAllowlistTeams" + }, + "delete_allowlist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "DeleteAllowlistUsernames" + }, "dismiss_stale_approvals": { "type": "boolean", "x-go-name": "DismissStaleApprovals" @@ -5108,6 +5567,14 @@ "type": "boolean", "x-go-name": "EnableApprovalsWhitelist" }, + "enable_delete": { + "type": "boolean", + "x-go-name": "EnableDelete" + }, + "enable_delete_allowlist": { + "type": "boolean", + "x-go-name": "EnableDeleteAllowlist" + }, "enable_force_push": { "type": "boolean", "x-go-name": "EnableForcePush" @@ -5132,6 +5599,10 @@ "type": "boolean", "x-go-name": "EnableStatusCheck" }, + "force_push_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "ForcePushAllowlistActionsUser" + }, "force_push_allowlist_deploy_keys": { "type": "boolean", "x-go-name": "ForcePushAllowlistDeployKeys" @@ -5154,6 +5625,10 @@ "type": "boolean", "x-go-name": "IgnoreStaleApprovals" }, + "merge_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "MergeWhitelistActionsUser" + }, "merge_whitelist_teams": { "items": { "type": "string" @@ -5177,6 +5652,10 @@ "type": "string", "x-go-name": "ProtectedFilePatterns" }, + "push_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "PushWhitelistActionsUser" + }, "push_whitelist_deploy_keys": { "type": "boolean", "x-go-name": "PushWhitelistDeployKeys" @@ -5339,6 +5818,11 @@ "type": "integer", "x-go-name": "Milestone" }, + "priority_id": { + "format": "int64", + "type": "integer", + "x-go-name": "PriorityID" + }, "projects": { "description": "list of project ids to set (replaces existing projects)", "items": { @@ -5356,10 +5840,21 @@ "type": "string", "x-go-name": "State" }, + "status_id": { + "description": "org-level issue metadata IDs", + "format": "int64", + "type": "integer", + "x-go-name": "StatusID" + }, "title": { "type": "string", "x-go-name": "Title" }, + "type_id": { + "format": "int64", + "type": "integer", + "x-go-name": "TypeID" + }, "unset_due_date": { "type": "boolean", "x-go-name": "RemoveDeadline" @@ -5431,6 +5926,199 @@ "type": "object", "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "EditOrgBranchProtectionOption": { + "description": "EditOrgBranchProtectionOption options for editing an org-level branch protection", + "properties": { + "approvals_whitelist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "ApprovalsWhitelistTeams" + }, + "approvals_whitelist_username": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "ApprovalsWhitelistUsernames" + }, + "block_admin_merge_override": { + "type": "boolean", + "x-go-name": "BlockAdminMergeOverride" + }, + "block_on_official_review_requests": { + "type": "boolean", + "x-go-name": "BlockOnOfficialReviewRequests" + }, + "block_on_outdated_branch": { + "type": "boolean", + "x-go-name": "BlockOnOutdatedBranch" + }, + "block_on_rejected_reviews": { + "type": "boolean", + "x-go-name": "BlockOnRejectedReviews" + }, + "delete_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "DeleteAllowlistActionsUser" + }, + "delete_allowlist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "DeleteAllowlistTeams" + }, + "delete_allowlist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "DeleteAllowlistUsernames" + }, + "dismiss_stale_approvals": { + "type": "boolean", + "x-go-name": "DismissStaleApprovals" + }, + "enable_approvals_whitelist": { + "type": "boolean", + "x-go-name": "EnableApprovalsWhitelist" + }, + "enable_delete": { + "type": "boolean", + "x-go-name": "EnableDelete" + }, + "enable_delete_allowlist": { + "type": "boolean", + "x-go-name": "EnableDeleteAllowlist" + }, + "enable_force_push": { + "type": "boolean", + "x-go-name": "EnableForcePush" + }, + "enable_force_push_allowlist": { + "type": "boolean", + "x-go-name": "EnableForcePushAllowlist" + }, + "enable_merge_whitelist": { + "type": "boolean", + "x-go-name": "EnableMergeWhitelist" + }, + "enable_push": { + "type": "boolean", + "x-go-name": "EnablePush" + }, + "enable_push_whitelist": { + "type": "boolean", + "x-go-name": "EnablePushWhitelist" + }, + "enable_status_check": { + "type": "boolean", + "x-go-name": "EnableStatusCheck" + }, + "force_push_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "ForcePushAllowlistActionsUser" + }, + "force_push_allowlist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "ForcePushAllowlistTeams" + }, + "force_push_allowlist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "ForcePushAllowlistUsernames" + }, + "ignore_stale_approvals": { + "type": "boolean", + "x-go-name": "IgnoreStaleApprovals" + }, + "merge_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "MergeWhitelistActionsUser" + }, + "merge_whitelist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "MergeWhitelistTeams" + }, + "merge_whitelist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "MergeWhitelistUsernames" + }, + "priority": { + "format": "int64", + "type": "integer", + "x-go-name": "Priority" + }, + "protected_file_patterns": { + "type": "string", + "x-go-name": "ProtectedFilePatterns" + }, + "push_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "PushWhitelistActionsUser" + }, + "push_whitelist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "PushWhitelistTeams" + }, + "push_whitelist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "PushWhitelistUsernames" + }, + "require_signed_commits": { + "type": "boolean", + "x-go-name": "RequireSignedCommits" + }, + "required_approvals": { + "format": "int64", + "type": "integer", + "x-go-name": "RequiredApprovals" + }, + "status_check_contexts": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "StatusCheckContexts" + }, + "unprotected_file_patterns": { + "type": "string", + "x-go-name": "UnprotectedFilePatterns" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "EditOrgEmailDomainPolicyOption": { + "description": "EditOrgEmailDomainPolicyOption options for editing an org's email domain policy", + "properties": { + "allowed_domains": { + "type": "string", + "x-go-name": "AllowedDomains" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "EditOrgOption": { "description": "EditOrgOption options for editing an organization", "properties": { @@ -5476,6 +6164,95 @@ "type": "object", "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "EditOrgPushPolicyOption": { + "description": "EditOrgPushPolicyOption options for editing an organization's push policy. Only\nfields that are set will be changed.", + "properties": { + "blocked_file_patterns": { + "type": "string", + "x-go-name": "BlockedFilePatterns" + }, + "branch_name_pattern": { + "type": "string", + "x-go-name": "BranchNamePattern" + }, + "max_file_size": { + "format": "int64", + "type": "integer", + "x-go-name": "MaxFileSize" + }, + "require_secret_block": { + "type": "boolean", + "x-go-name": "RequireSecretBlock" + }, + "tag_name_pattern": { + "type": "string", + "x-go-name": "TagNamePattern" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "EditOrgRepoDefaultsOption": { + "description": "EditOrgRepoDefaultsOption options for editing an org's repo defaults. Only fields\nthat are set will be changed.", + "properties": { + "allow_fast_forward_only": { + "type": "boolean", + "x-go-name": "AllowFastForwardOnly" + }, + "allow_merge": { + "type": "boolean", + "x-go-name": "AllowMerge" + }, + "allow_rebase": { + "type": "boolean", + "x-go-name": "AllowRebase" + }, + "allow_rebase_merge": { + "type": "boolean", + "x-go-name": "AllowRebaseMerge" + }, + "allow_squash": { + "type": "boolean", + "x-go-name": "AllowSquash" + }, + "apply_pr_defaults": { + "type": "boolean", + "x-go-name": "ApplyPRDefaults" + }, + "default_merge_style": { + "type": "string", + "x-go-name": "DefaultMergeStyle" + }, + "delete_branch_after_merge": { + "type": "boolean", + "x-go-name": "DeleteBranchAfterMerge" + }, + "force_private": { + "type": "boolean", + "x-go-name": "ForcePrivate" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "EditOrgTagProtectionOption": { + "description": "EditOrgTagProtectionOption options for editing an org-level tag protection", + "properties": { + "name_pattern": { + "type": "string", + "x-go-name": "NamePattern" + }, + "whitelist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "WhitelistTeams" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "EditPullRequestOption": { "description": "EditPullRequestOption options when modify pull request", "properties": { @@ -6882,6 +7659,15 @@ "type": "integer", "x-go-name": "PinOrder" }, + "priority_id": { + "format": "int64", + "type": "integer", + "x-go-name": "PriorityID" + }, + "priority_name": { + "type": "string", + "x-go-name": "PriorityName" + }, "projects": { "items": { "$ref": "#/components/schemas/Project" @@ -6902,6 +7688,16 @@ "state": { "$ref": "#/components/schemas/StateType" }, + "status_id": { + "description": "Issue metadata (org-level definitions)", + "format": "int64", + "type": "integer", + "x-go-name": "StatusID" + }, + "status_name": { + "type": "string", + "x-go-name": "StatusName" + }, "time_estimate": { "format": "int64", "type": "integer", @@ -6911,6 +7707,15 @@ "type": "string", "x-go-name": "Title" }, + "type_id": { + "format": "int64", + "type": "integer", + "x-go-name": "TypeID" + }, + "type_name": { + "type": "string", + "x-go-name": "TypeName" + }, "updated_at": { "format": "date-time", "type": "string", @@ -6928,6 +7733,139 @@ "type": "object", "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "IssueBulkAssigneesOption": { + "description": "IssueBulkAssigneesOption options for bulk assignee operations on issues", + "properties": { + "assignees": { + "description": "list of assignee usernames to add or set", + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "Assignees" + }, + "issues": { + "description": "issue indexes to operate on", + "items": { + "format": "int64", + "type": "integer" + }, + "type": "array", + "x-go-name": "Issues" + } + }, + "required": [ + "issues", + "assignees" + ], + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "IssueBulkLabelsOption": { + "description": "IssueBulkLabelsOption options for bulk label operations on issues", + "properties": { + "action": { + "description": "action to perform: \"add\" (default), \"remove\", or \"replace\"", + "type": "string", + "x-go-name": "Action" + }, + "issues": { + "description": "issue indexes to operate on", + "items": { + "format": "int64", + "type": "integer" + }, + "type": "array", + "x-go-name": "Issues" + }, + "labels": { + "description": "Labels can be a list of integers representing label IDs\nor a list of strings representing label names", + "items": {}, + "type": "array", + "x-go-name": "Labels" + } + }, + "required": [ + "issues", + "labels" + ], + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "IssueBulkMilestoneOption": { + "description": "IssueBulkMilestoneOption options for bulk milestone assignment on issues", + "properties": { + "issues": { + "description": "issue indexes to operate on", + "items": { + "format": "int64", + "type": "integer" + }, + "type": "array", + "x-go-name": "Issues" + }, + "milestone_id": { + "description": "milestone id to assign, 0 to remove milestone", + "format": "int64", + "type": "integer", + "x-go-name": "MilestoneID" + } + }, + "required": [ + "issues", + "milestone_id" + ], + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "IssueBulkResult": { + "description": "IssueBulkResult represents the result of a bulk issue operation", + "properties": { + "failure_count": { + "description": "count of issues that failed to update", + "format": "int64", + "type": "integer", + "x-go-name": "FailureCount" + }, + "failures": { + "description": "details of failures, keyed by issue index", + "x-go-name": "Failures" + }, + "success_count": { + "description": "count of successfully updated issues", + "format": "int64", + "type": "integer", + "x-go-name": "SuccessCount" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "IssueBulkStateOption": { + "description": "IssueBulkStateOption options for bulk state changes on issues", + "properties": { + "issues": { + "description": "issue indexes to operate on", + "items": { + "format": "int64", + "type": "integer" + }, + "type": "array", + "x-go-name": "Issues" + }, + "state": { + "description": "new state for the issues, \"open\" or \"closed\"", + "type": "string", + "x-go-name": "State" + } + }, + "required": [ + "issues", + "state" + ], + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "IssueConfig": { "properties": { "blank_issues_enabled": { @@ -7069,6 +8007,76 @@ "type": "object", "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "IssuePriorityDef": { + "description": "IssuePriorityDef represents an org-level issue priority definition", + "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, + "description": { + "type": "string", + "x-go-name": "Description" + }, + "id": { + "format": "int64", + "type": "integer", + "x-go-name": "ID" + }, + "is_default": { + "type": "boolean", + "x-go-name": "IsDefault" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "sort_order": { + "format": "int64", + "type": "integer", + "x-go-name": "SortOrder" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "IssueStatusDef": { + "description": "IssueStatusDef represents an org-level issue status definition", + "properties": { + "closes_issue": { + "type": "boolean", + "x-go-name": "ClosesIssue" + }, + "color": { + "type": "string", + "x-go-name": "Color" + }, + "description": { + "type": "string", + "x-go-name": "Description" + }, + "id": { + "format": "int64", + "type": "integer", + "x-go-name": "ID" + }, + "is_required": { + "type": "boolean", + "x-go-name": "IsRequired" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "sort_order": { + "format": "int64", + "type": "integer", + "x-go-name": "SortOrder" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "IssueTemplate": { "description": "IssueTemplate represents an issue template for a repository", "properties": { @@ -7090,6 +8098,13 @@ "type": "string", "x-go-name": "Content" }, + "custom_fields": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "x-go-name": "CustomFields" + }, "file_name": { "type": "string", "x-go-name": "FileName" @@ -7120,6 +8135,39 @@ "type": "array", "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" }, + "IssueTypeDef": { + "description": "IssueTypeDef represents an org-level issue type definition", + "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, + "description": { + "type": "string", + "x-go-name": "Description" + }, + "id": { + "format": "int64", + "type": "integer", + "x-go-name": "ID" + }, + "is_default": { + "type": "boolean", + "x-go-name": "IsDefault" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "sort_order": { + "format": "int64", + "type": "integer", + "x-go-name": "SortOrder" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "Label": { "description": "Label a label to an issue or a pr", "properties": { @@ -7900,6 +8948,357 @@ ], "type": "string" }, + "OrgBranchProtection": { + "description": "OrgBranchProtection represents an org-level branch protection ruleset", + "properties": { + "approvals_whitelist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "ApprovalsWhitelistTeams" + }, + "approvals_whitelist_username": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "ApprovalsWhitelistUsernames" + }, + "block_admin_merge_override": { + "type": "boolean", + "x-go-name": "BlockAdminMergeOverride" + }, + "block_on_official_review_requests": { + "type": "boolean", + "x-go-name": "BlockOnOfficialReviewRequests" + }, + "block_on_outdated_branch": { + "type": "boolean", + "x-go-name": "BlockOnOutdatedBranch" + }, + "block_on_rejected_reviews": { + "type": "boolean", + "x-go-name": "BlockOnRejectedReviews" + }, + "created_at": { + "format": "date-time", + "type": "string", + "x-go-name": "Created" + }, + "delete_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "DeleteAllowlistActionsUser" + }, + "delete_allowlist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "DeleteAllowlistTeams" + }, + "delete_allowlist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "DeleteAllowlistUsernames" + }, + "dismiss_stale_approvals": { + "type": "boolean", + "x-go-name": "DismissStaleApprovals" + }, + "enable_approvals_whitelist": { + "type": "boolean", + "x-go-name": "EnableApprovalsWhitelist" + }, + "enable_delete": { + "type": "boolean", + "x-go-name": "EnableDelete" + }, + "enable_delete_allowlist": { + "type": "boolean", + "x-go-name": "EnableDeleteAllowlist" + }, + "enable_force_push": { + "type": "boolean", + "x-go-name": "EnableForcePush" + }, + "enable_force_push_allowlist": { + "type": "boolean", + "x-go-name": "EnableForcePushAllowlist" + }, + "enable_merge_whitelist": { + "type": "boolean", + "x-go-name": "EnableMergeWhitelist" + }, + "enable_push": { + "type": "boolean", + "x-go-name": "EnablePush" + }, + "enable_push_whitelist": { + "type": "boolean", + "x-go-name": "EnablePushWhitelist" + }, + "enable_status_check": { + "type": "boolean", + "x-go-name": "EnableStatusCheck" + }, + "force_push_allowlist_actions_user": { + "type": "boolean", + "x-go-name": "ForcePushAllowlistActionsUser" + }, + "force_push_allowlist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "ForcePushAllowlistTeams" + }, + "force_push_allowlist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "ForcePushAllowlistUsernames" + }, + "id": { + "format": "int64", + "type": "integer", + "x-go-name": "ID" + }, + "ignore_stale_approvals": { + "type": "boolean", + "x-go-name": "IgnoreStaleApprovals" + }, + "merge_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "MergeWhitelistActionsUser" + }, + "merge_whitelist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "MergeWhitelistTeams" + }, + "merge_whitelist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "MergeWhitelistUsernames" + }, + "org_id": { + "format": "int64", + "type": "integer", + "x-go-name": "OrgID" + }, + "priority": { + "format": "int64", + "type": "integer", + "x-go-name": "Priority" + }, + "protected_file_patterns": { + "type": "string", + "x-go-name": "ProtectedFilePatterns" + }, + "push_whitelist_actions_user": { + "type": "boolean", + "x-go-name": "PushWhitelistActionsUser" + }, + "push_whitelist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "PushWhitelistTeams" + }, + "push_whitelist_usernames": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "PushWhitelistUsernames" + }, + "require_signed_commits": { + "type": "boolean", + "x-go-name": "RequireSignedCommits" + }, + "required_approvals": { + "format": "int64", + "type": "integer", + "x-go-name": "RequiredApprovals" + }, + "rule_name": { + "type": "string", + "x-go-name": "RuleName" + }, + "status_check_contexts": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "StatusCheckContexts" + }, + "unprotected_file_patterns": { + "type": "string", + "x-go-name": "UnprotectedFilePatterns" + }, + "updated_at": { + "format": "date-time", + "type": "string", + "x-go-name": "Updated" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "OrgEmailDomainPolicy": { + "description": "OrgEmailDomainPolicy represents an organization's email domain policy", + "properties": { + "allowed_domains": { + "type": "string", + "x-go-name": "AllowedDomains" + }, + "org_id": { + "format": "int64", + "type": "integer", + "x-go-name": "OrgID" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "OrgPushPolicy": { + "description": "OrgPushPolicy represents an organization's push policy (one per org)", + "properties": { + "blocked_file_patterns": { + "type": "string", + "x-go-name": "BlockedFilePatterns" + }, + "branch_name_pattern": { + "type": "string", + "x-go-name": "BranchNamePattern" + }, + "created_at": { + "format": "date-time", + "type": "string", + "x-go-name": "Created" + }, + "max_file_size": { + "format": "int64", + "type": "integer", + "x-go-name": "MaxFileSize" + }, + "org_id": { + "format": "int64", + "type": "integer", + "x-go-name": "OrgID" + }, + "require_secret_block": { + "type": "boolean", + "x-go-name": "RequireSecretBlock" + }, + "tag_name_pattern": { + "type": "string", + "x-go-name": "TagNamePattern" + }, + "updated_at": { + "format": "date-time", + "type": "string", + "x-go-name": "Updated" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "OrgRepoDefaults": { + "description": "OrgRepoDefaults represents an organization's default repository settings", + "properties": { + "allow_fast_forward_only": { + "type": "boolean", + "x-go-name": "AllowFastForwardOnly" + }, + "allow_merge": { + "type": "boolean", + "x-go-name": "AllowMerge" + }, + "allow_rebase": { + "type": "boolean", + "x-go-name": "AllowRebase" + }, + "allow_rebase_merge": { + "type": "boolean", + "x-go-name": "AllowRebaseMerge" + }, + "allow_squash": { + "type": "boolean", + "x-go-name": "AllowSquash" + }, + "apply_pr_defaults": { + "type": "boolean", + "x-go-name": "ApplyPRDefaults" + }, + "default_merge_style": { + "type": "string", + "x-go-name": "DefaultMergeStyle" + }, + "delete_branch_after_merge": { + "type": "boolean", + "x-go-name": "DeleteBranchAfterMerge" + }, + "force_private": { + "type": "boolean", + "x-go-name": "ForcePrivate" + }, + "org_id": { + "format": "int64", + "type": "integer", + "x-go-name": "OrgID" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "OrgTagProtection": { + "description": "OrgTagProtection represents an org-level tag protection rule", + "properties": { + "created_at": { + "format": "date-time", + "type": "string", + "x-go-name": "Created" + }, + "id": { + "format": "int64", + "type": "integer", + "x-go-name": "ID" + }, + "name_pattern": { + "type": "string", + "x-go-name": "NamePattern" + }, + "org_id": { + "format": "int64", + "type": "integer", + "x-go-name": "OrgID" + }, + "updated_at": { + "format": "date-time", + "type": "string", + "x-go-name": "Updated" + }, + "whitelist_teams": { + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "WhitelistTeams" + } + }, + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, "Organization": { "description": "Organization represents an organization", "properties": { @@ -10542,6 +11941,125 @@ }, "type": "object", "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs" + }, + "apiMetadata": { + "properties": { + "container_name": { + "type": "string", + "x-go-name": "ContainerName" + }, + "deploy_host": { + "description": "deploy", + "type": "string", + "x-go-name": "DeployHost" + }, + "deploy_path": { + "type": "string", + "x-go-name": "DeployPath" + }, + "deploy_port": { + "type": "string", + "x-go-name": "DeployPort" + }, + "deploy_user": { + "type": "string", + "x-go-name": "DeployUser" + }, + "description": { + "type": "string", + "x-go-name": "Description" + }, + "display_name": { + "type": "string", + "x-go-name": "DisplayName" + }, + "docker_image": { + "type": "string", + "x-go-name": "DockerImage" + }, + "docker_registry": { + "type": "string", + "x-go-name": "DockerRegistry" + }, + "element_name": { + "type": "string", + "x-go-name": "ElementName" + }, + "entry_point": { + "type": "string", + "x-go-name": "EntryPoint" + }, + "extension_type": { + "type": "string", + "x-go-name": "ExtensionType" + }, + "health_url": { + "format": "uri", + "type": "string", + "x-go-name": "HealthURL" + }, + "info_url": { + "format": "uri", + "type": "string", + "x-go-name": "InfoURL" + }, + "language": { + "type": "string", + "x-go-name": "Language" + }, + "license_name": { + "type": "string", + "x-go-name": "LicenseName" + }, + "license_spdx": { + "type": "string", + "x-go-name": "LicenseSPDX" + }, + "maintainer": { + "type": "string", + "x-go-name": "Maintainer" + }, + "maintainer_url": { + "format": "uri", + "type": "string", + "x-go-name": "MaintainerURL" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "org": { + "type": "string", + "x-go-name": "Org" + }, + "php_minimum": { + "type": "string", + "x-go-name": "PHPMinimum" + }, + "platform": { + "type": "string", + "x-go-name": "Platform" + }, + "standards_source": { + "type": "string", + "x-go-name": "StandardsSource" + }, + "standards_version": { + "type": "string", + "x-go-name": "StandardsVersion" + }, + "target_version": { + "type": "string", + "x-go-name": "TargetVersion" + }, + "version_prefix": { + "type": "string", + "x-go-name": "VersionPrefix" + } + }, + "title": "apiMetadata is the JSON representation of a repo manifest.", + "type": "object", + "x-go-package": "code.mokoconsulting.tech/MokoConsulting/MokoGitea/routers/api/v1/repo" } }, "securitySchemes": { @@ -10588,12 +12106,12 @@ } }, "info": { - "description": "This documentation describes the {{.SwaggerAppName}} API.", + "description": "This documentation describes the Gitea API.", "license": { "name": "MIT", "url": "http://opensource.org/licenses/MIT" }, - "title": "{{.SwaggerAppName}} API", + "title": "Gitea API", "version": "{{.SwaggerAppVer}}" }, "openapi": "3.0.3", @@ -13484,6 +15002,289 @@ ] } }, + "/orgs/{org}/branch_protections": { + "get": { + "operationId": "orgListBranchProtections", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/OrgBranchProtectionList" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "List an organization's branch protection rules", + "tags": [ + "organization" + ] + }, + "post": { + "operationId": "orgCreateBranchProtection", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgBranchProtectionOption" + } + } + }, + "x-originalParamName": "body" + }, + "responses": { + "201": { + "$ref": "#/components/responses/OrgBranchProtection" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/notFound" + }, + "422": { + "$ref": "#/components/responses/validationError" + } + }, + "summary": "Create an org-level branch protection rule", + "tags": [ + "organization" + ] + } + }, + "/orgs/{org}/branch_protections/{name}": { + "delete": { + "operationId": "orgDeleteBranchProtection", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "name of the branch protection rule", + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "$ref": "#/components/responses/empty" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Delete an org-level branch protection rule", + "tags": [ + "organization" + ] + }, + "get": { + "operationId": "orgGetBranchProtection", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "name of the branch protection rule", + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/OrgBranchProtection" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Get a specific org-level branch protection rule", + "tags": [ + "organization" + ] + }, + "patch": { + "operationId": "orgEditBranchProtection", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "name of the branch protection rule", + "in": "path", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EditOrgBranchProtectionOption" + } + } + }, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "$ref": "#/components/responses/OrgBranchProtection" + }, + "404": { + "$ref": "#/components/responses/notFound" + }, + "422": { + "$ref": "#/components/responses/validationError" + } + }, + "summary": "Edit an org-level branch protection rule. Only fields that are set will be changed", + "tags": [ + "organization" + ] + } + }, + "/orgs/{org}/email_domain_policy": { + "delete": { + "operationId": "orgDeleteEmailDomainPolicy", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "$ref": "#/components/responses/empty" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Remove the organization's email domain policy", + "tags": [ + "organization" + ] + }, + "get": { + "operationId": "orgGetEmailDomainPolicy", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/OrgEmailDomainPolicy" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Get the organization's email domain policy", + "tags": [ + "organization" + ] + }, + "patch": { + "operationId": "orgEditEmailDomainPolicy", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EditOrgEmailDomainPolicyOption" + } + } + }, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "$ref": "#/components/responses/OrgEmailDomainPolicy" + }, + "404": { + "$ref": "#/components/responses/notFound" + }, + "422": { + "$ref": "#/components/responses/validationError" + } + }, + "summary": "Create or update the organization's email domain policy. Only fields that are set will be changed", + "tags": [ + "organization" + ] + } + }, "/orgs/{org}/hooks": { "get": { "operationId": "orgListHooks", @@ -13685,6 +15486,219 @@ ] } }, + "/orgs/{org}/issue-priorities": { + "get": { + "operationId": "orgListIssuePriorities", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/IssuePriorityDef" + }, + "type": "array" + } + } + }, + "description": "IssuePriorityDefList" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "List an organization's issue priority definitions", + "tags": [ + "organization" + ] + } + }, + "/orgs/{org}/issue-statuses": { + "get": { + "operationId": "orgListIssueStatuses", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/IssueStatusDef" + }, + "type": "array" + } + } + }, + "description": "IssueStatusDefList" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "List an organization's issue status definitions", + "tags": [ + "organization" + ] + } + }, + "/orgs/{org}/issue-statuses/copy/{source_org}": { + "post": { + "operationId": "orgCopyIssueStatuses", + "parameters": [ + { + "description": "target organization name", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "source organization name to copy from", + "in": "path", + "name": "source_org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "StatusesCopied" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Copy issue statuses from another organization", + "tags": [ + "organization" + ] + } + }, + "/orgs/{org}/issue-statuses/presets": { + "get": { + "operationId": "orgListIssueStatusPresets", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "StatusPresetList" + } + }, + "summary": "List available issue status presets", + "tags": [ + "organization" + ] + } + }, + "/orgs/{org}/issue-statuses/presets/{preset}": { + "post": { + "operationId": "orgApplyIssueStatusPreset", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "preset name", + "in": "path", + "name": "preset", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "StatusPresetApplied" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Apply a status preset to an organization", + "tags": [ + "organization" + ] + } + }, + "/orgs/{org}/issue-types": { + "get": { + "operationId": "orgListIssueTypes", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/IssueTypeDef" + }, + "type": "array" + } + } + }, + "description": "IssueTypeDefList" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "List an organization's issue type definitions", + "tags": [ + "organization" + ] + } + }, "/orgs/{org}/labels": { "get": { "operationId": "orgListLabels", @@ -14167,6 +16181,99 @@ ] } }, + "/orgs/{org}/push_policy": { + "delete": { + "operationId": "orgDeletePushPolicy", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "$ref": "#/components/responses/empty" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Remove the organization's push policy", + "tags": [ + "organization" + ] + }, + "get": { + "operationId": "orgGetPushPolicy", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/OrgPushPolicy" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Get the organization's push policy", + "tags": [ + "organization" + ] + }, + "patch": { + "operationId": "orgEditPushPolicy", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EditOrgPushPolicyOption" + } + } + }, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "$ref": "#/components/responses/OrgPushPolicy" + }, + "404": { + "$ref": "#/components/responses/notFound" + }, + "422": { + "$ref": "#/components/responses/validationError" + } + }, + "summary": "Create or update the organization's push policy. Only fields that are set will be changed", + "tags": [ + "organization" + ] + } + }, "/orgs/{org}/rename": { "post": { "operationId": "renameOrg", @@ -14209,6 +16316,99 @@ ] } }, + "/orgs/{org}/repo_defaults": { + "delete": { + "operationId": "orgDeleteRepoDefaults", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "$ref": "#/components/responses/empty" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Remove the organization's default repository settings", + "tags": [ + "organization" + ] + }, + "get": { + "operationId": "orgGetRepoDefaults", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/OrgRepoDefaults" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Get the organization's default repository settings", + "tags": [ + "organization" + ] + }, + "patch": { + "operationId": "orgEditRepoDefaults", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EditOrgRepoDefaultsOption" + } + } + }, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "$ref": "#/components/responses/OrgRepoDefaults" + }, + "404": { + "$ref": "#/components/responses/notFound" + }, + "422": { + "$ref": "#/components/responses/validationError" + } + }, + "summary": "Create or update the organization's default repository settings. Only fields that are set will be changed", + "tags": [ + "organization" + ] + } + }, "/orgs/{org}/repos": { "delete": { "operationId": "orgDeleteRepos", @@ -14327,6 +16527,199 @@ ] } }, + "/orgs/{org}/tag_protections": { + "get": { + "operationId": "orgListTagProtections", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/OrgTagProtectionList" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "List an organization's tag protection rules", + "tags": [ + "organization" + ] + }, + "post": { + "operationId": "orgCreateTagProtection", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgTagProtectionOption" + } + } + }, + "x-originalParamName": "body" + }, + "responses": { + "201": { + "$ref": "#/components/responses/OrgTagProtection" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/notFound" + }, + "422": { + "$ref": "#/components/responses/validationError" + } + }, + "summary": "Create an org-level tag protection rule", + "tags": [ + "organization" + ] + } + }, + "/orgs/{org}/tag_protections/{id}": { + "delete": { + "operationId": "orgDeleteTagProtection", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "id of the tag protection rule", + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "responses": { + "204": { + "$ref": "#/components/responses/empty" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Delete an org-level tag protection rule", + "tags": [ + "organization" + ] + }, + "get": { + "operationId": "orgGetTagProtection", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "id of the tag protection rule", + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/OrgTagProtection" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Get a specific org-level tag protection rule", + "tags": [ + "organization" + ] + }, + "patch": { + "operationId": "orgEditTagProtection", + "parameters": [ + { + "description": "name of the organization", + "in": "path", + "name": "org", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "id of the tag protection rule", + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EditOrgTagProtectionOption" + } + } + }, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "$ref": "#/components/responses/OrgTagProtection" + }, + "404": { + "$ref": "#/components/responses/notFound" + }, + "422": { + "$ref": "#/components/responses/validationError" + } + }, + "summary": "Edit an org-level tag protection rule. Only fields that are set will be changed", + "tags": [ + "organization" + ] + } + }, "/orgs/{org}/teams": { "get": { "operationId": "orgListTeams", @@ -20991,6 +23384,209 @@ ] } }, + "/repos/{owner}/{repo}/issues/bulk/assignees": { + "post": { + "operationId": "issueBulkSetAssignees", + "parameters": [ + { + "description": "owner of the repo", + "in": "path", + "name": "owner", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "name of the repo", + "in": "path", + "name": "repo", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssueBulkAssigneesOption" + } + } + }, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "$ref": "#/components/responses/IssueBulkResult" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "422": { + "$ref": "#/components/responses/validationError" + } + }, + "summary": "Set assignees on multiple issues", + "tags": [ + "issue" + ] + } + }, + "/repos/{owner}/{repo}/issues/bulk/labels": { + "post": { + "operationId": "issueBulkSetLabels", + "parameters": [ + { + "description": "owner of the repo", + "in": "path", + "name": "owner", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "name of the repo", + "in": "path", + "name": "repo", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssueBulkLabelsOption" + } + } + }, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "$ref": "#/components/responses/IssueBulkResult" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/notFound" + }, + "422": { + "$ref": "#/components/responses/validationError" + } + }, + "summary": "Add, remove, or replace labels on multiple issues", + "tags": [ + "issue" + ] + } + }, + "/repos/{owner}/{repo}/issues/bulk/milestone": { + "post": { + "operationId": "issueBulkSetMilestone", + "parameters": [ + { + "description": "owner of the repo", + "in": "path", + "name": "owner", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "name of the repo", + "in": "path", + "name": "repo", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssueBulkMilestoneOption" + } + } + }, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "$ref": "#/components/responses/IssueBulkResult" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "422": { + "$ref": "#/components/responses/validationError" + } + }, + "summary": "Assign a milestone to multiple issues", + "tags": [ + "issue" + ] + } + }, + "/repos/{owner}/{repo}/issues/bulk/state": { + "post": { + "operationId": "issueBulkSetState", + "parameters": [ + { + "description": "owner of the repo", + "in": "path", + "name": "owner", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "name of the repo", + "in": "path", + "name": "repo", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssueBulkStateOption" + } + } + }, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "$ref": "#/components/responses/IssueBulkResult" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "422": { + "$ref": "#/components/responses/validationError" + } + }, + "summary": "Close or reopen multiple issues", + "tags": [ + "issue" + ] + } + }, "/repos/{owner}/{repo}/issues/comments": { "get": { "operationId": "issueGetRepoComments", @@ -24890,6 +27486,75 @@ ] } }, + "/repos/{owner}/{repo}/manifest": { + "get": { + "operationId": "repoGetManifest", + "parameters": [ + { + "description": "owner of the repo", + "in": "path", + "name": "owner", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "name of the repo", + "in": "path", + "name": "repo", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/Manifest" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Get repo manifest settings", + "tags": [ + "repository" + ] + }, + "put": { + "operationId": "repoUpdateManifest", + "parameters": [ + { + "description": "owner of the repo", + "in": "path", + "name": "owner", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "name of the repo", + "in": "path", + "name": "repo", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/Manifest" + } + }, + "summary": "Update repo manifest settings", + "tags": [ + "repository" + ] + } + }, "/repos/{owner}/{repo}/media/{filepath}": { "get": { "operationId": "repoGetRawFileOrLFS", @@ -30115,6 +32780,68 @@ ] } }, + "/repos/{owner}/{repo}/wiki/search": { + "get": { + "operationId": "repoSearchWikiPages", + "parameters": [ + { + "description": "owner of the repo", + "in": "path", + "name": "owner", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "name of the repo", + "in": "path", + "name": "repo", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "search query", + "in": "query", + "name": "q", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "page number of results to return (1-based)", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "page size of results", + "in": "query", + "name": "limit", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "SearchResults" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Search wiki pages", + "tags": [ + "repository" + ] + } + }, "/repos/{template_owner}/{template_repo}/generate": { "post": { "operationId": "generateRepo", @@ -33463,6 +36190,60 @@ ] } }, + "/users/{username}/tokens/{id}": { + "patch": { + "operationId": "userUpdateAccessToken", + "parameters": [ + { + "description": "username of the user whose token is to be updated", + "in": "path", + "name": "username", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "id of the token to update", + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EditAccessTokenOption" + } + } + }, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "$ref": "#/components/responses/AccessToken" + }, + "400": { + "$ref": "#/components/responses/error" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/notFound" + } + }, + "summary": "Update an access token's scopes", + "tags": [ + "user" + ] + } + }, "/users/{username}/tokens/{token}": { "delete": { "operationId": "userDeleteAccessToken", diff --git a/tests/integration/api_license_keys_test.go b/tests/integration/api_license_keys_test.go index d9807b9856..1e30d0f864 100644 --- a/tests/integration/api_license_keys_test.go +++ b/tests/integration/api_license_keys_test.go @@ -6,6 +6,7 @@ package integration import ( "fmt" "net/http" + "strings" "testing" auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth" @@ -36,7 +37,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", []byte(body)). + req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)). AddTokenAuth(token). SetHeader("Content-Type", "application/json") resp := MakeRequest(t, req, http.StatusCreated) @@ -51,7 +52,7 @@ func TestAPILicensePackages(t *testing.T) { t.Run("CreatePackageNoName", func(t *testing.T) { body := `{"description":"Missing name"}` - req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)). + req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)). AddTokenAuth(token). SetHeader("Content-Type", "application/json") MakeRequest(t, req, http.StatusUnprocessableEntity) @@ -68,7 +69,7 @@ func TestAPILicenseKeys(t *testing.T) { // Create a package first. body := `{"name":"Test Package","duration_days":30}` - req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)). + req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)). AddTokenAuth(token). SetHeader("Content-Type", "application/json") resp := MakeRequest(t, req, http.StatusCreated) @@ -80,7 +81,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", []byte(body)). + req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys", strings.NewReader(body)). AddTokenAuth(token). SetHeader("Content-Type", "application/json") resp := MakeRequest(t, req, http.StatusCreated) @@ -104,7 +105,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), []byte(body)). + req := NewRequestWithBody(t, "PATCH", fmt.Sprintf("%s/license-keys/%d", urlPrefix, createdKeyID), strings.NewReader(body)). AddTokenAuth(token). SetHeader("Content-Type", "application/json") resp := MakeRequest(t, req, http.StatusOK) @@ -124,7 +125,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", []byte(body)). + req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", strings.NewReader(body)). SetHeader("Content-Type", "application/json") // Note: no token — this is a public endpoint. resp := MakeRequest(t, req, http.StatusOK) @@ -136,7 +137,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", []byte(body)). + req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/validate", strings.NewReader(body)). SetHeader("Content-Type", "application/json") resp := MakeRequest(t, req, http.StatusOK) var result api.ValidateLicenseKeyResponse @@ -161,7 +162,7 @@ func TestAPILicensePurchaseWebhook(t *testing.T) { // Create a package. body := `{"name":"Purchase Test","duration_days":90}` - req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", []byte(body)). + req := NewRequestWithBody(t, "POST", urlPrefix+"/license-packages", strings.NewReader(body)). AddTokenAuth(token). SetHeader("Content-Type", "application/json") resp := MakeRequest(t, req, http.StatusCreated) @@ -170,7 +171,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", []byte(body)). + req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", strings.NewReader(body)). AddTokenAuth(token). SetHeader("Content-Type", "application/json") resp := MakeRequest(t, req, http.StatusCreated) @@ -183,7 +184,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", []byte(body)). + req := NewRequestWithBody(t, "POST", urlPrefix+"/license-keys/purchase", strings.NewReader(body)). AddTokenAuth(token). SetHeader("Content-Type", "application/json") resp := MakeRequest(t, req, http.StatusOK) diff --git a/tests/integration/api_packages_composer_test.go b/tests/integration/api_packages_composer_test.go index b97aebfb1f..7093b1b38a 100644 --- a/tests/integration/api_packages_composer_test.go +++ b/tests/integration/api_packages_composer_test.go @@ -255,7 +255,7 @@ func TestPackageComposer(t *testing.T) { AddBasicAuth(user.Name) resp = MakeRequest(t, req, http.StatusOK) - result = composer.PackageMetadataResponse{} + result = &composer.PackageMetadataResponse{} DecodeJSON(t, resp, &result) pkgs = result.Packages[packageName] assert.Len(t, pkgs, 1) @@ -268,7 +268,7 @@ func TestPackageComposer(t *testing.T) { AddBasicAuth(otherUser.Name) resp = MakeRequest(t, req, http.StatusOK) - result = composer.PackageMetadataResponse{} + result = &composer.PackageMetadataResponse{} DecodeJSON(t, resp, &result) pkgs = result.Packages[packageName] assert.Len(t, pkgs, 1) diff --git a/tests/integration/auth_oauth2_test.go b/tests/integration/auth_oauth2_test.go index e6a032d803..90794f9ff9 100644 --- a/tests/integration/auth_oauth2_test.go +++ b/tests/integration/auth_oauth2_test.go @@ -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 := newFakeOIDCServer(t, FakeOIDCConfig{Sub: "test-sub", Email: "test@example.com", Name: "Test User"}) + srv := newFakeOIDCServerWithConfig(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 newFakeOIDCServer(t *testing.T, cfg FakeOIDCConfig) *httptest.Server { +func newFakeOIDCServerWithConfig(t *testing.T, cfg FakeOIDCConfig) *httptest.Server { t.Helper() var srv *httptest.Server diff --git a/tests/integration/oauth_avatar_test.go b/tests/integration/oauth_avatar_test.go index d86f2f2cbe..2e756e8833 100644 --- a/tests/integration/oauth_avatar_test.go +++ b/tests/integration/oauth_avatar_test.go @@ -7,16 +7,16 @@ import ( "net/http" "testing" - auth_model "code.gitea.io/gitea/models/auth" - "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/routers/web/auth" - "code.gitea.io/gitea/services/auth/source/oauth2" - "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/tests" + auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unittest" + user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/test" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/routers/web/auth" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/oauth2" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/tests" "github.com/markbates/goth" "github.com/markbates/goth/gothic"