Compare commits

..
11 Commits
Author SHA1 Message Date
wxiaoguang 63bc4eae68 no need to "clean up test fixture" 2026-06-16 08:51:21 +08:00
wxiaoguang 6b416d6135 fix 2026-06-16 08:48:53 +08:00
Nicolas 95a300e1f4 Merge remote-tracking branch 'origin/main' into various-sec-fixes
# Conflicts:
#	routers/api/v1/api.go
2026-06-15 22:12:27 +02:00
Nicolas 2f5a735c75 unexport 2026-06-15 22:11:23 +02:00
Nicolas 8819eee696 derive maintainer edit 2026-06-15 22:08:10 +02:00
wxiaoguang 0c8be7bee0 FIXME 2026-06-15 14:22:38 +08:00
Nicolas f25811942c fix: re-check branch write permission for every ref in a push
The pre-receive hook cached the result of CanWriteCode() after the first
ref in a batch push, but CanMaintainerWriteToBranch depends on the current
branch name. A user holding a per-branch maintainer-edit grant (an open PR
with "allow edits from maintainers") could batch that branch with protected
branches or tags and have the cached approval reused, escalating to full
repository write. Evaluate the permission fresh for every ref; the pusher
and base permission remain cached via loadPusherAndPermission.

Assisted-by: Claude:claude-opus-4-8
2026-06-13 18:38:06 +02:00
Nicolas c0c11c551c fix: enforce single-use TOTP passcodes across all 2FA surfaces
The web 2FA login and password-reset paths validated the passcode and then
wrote LastUsedPasscode in a non-atomic read-check-write sequence, so two
parallel submissions of the same code could each authenticate (TOCTOU). The
Basic-Auth X-Gitea-OTP path never recorded the used passcode at all, letting
a captured code be replayed for its whole validity window.

Add TwoFactor.ValidateAndConsumeTOTP, which validates and atomically marks
the passcode used via a conditional UPDATE (rejecting replays and racing
duplicates), and route the web login, password-reset, and Basic-Auth paths
through it.

Assisted-by: Claude:claude-opus-4-8
2026-06-13 18:38:06 +02:00
Nicolas e99e24cb04 fix: stop trusting all proxies by default in docker app.ini templates
The Docker app.ini templates hard-coded REVERSE_PROXY_TRUSTED_PROXIES = *,
so with ENABLE_REVERSE_PROXY_AUTHENTICATION enabled any source IP reaching
the container could impersonate any user via the X-WEBAUTH-USER header.
Align the templates with the documented loopback-only default
(127.0.0.0/8,::1/128), matching app.example.ini and the in-code default.

Assisted-by: Claude:claude-opus-4-8
2026-06-13 18:38:06 +02:00
Nicolas c20df84548 fix: block fork sync when base repo is no longer readable
POST /api/v1/repos/{owner}/{repo}/merge-upstream kept importing commits
from the parent repository even after the parent was switched from public
to private, leaking commits a fork owner could no longer access directly.
Require the doer to still have read access to the base repo's code before
syncing, and map the permission error to 403 (API) / not-found (web).

Assisted-by: Claude:claude-opus-4-8
2026-06-13 17:36:35 +02:00
Nicolas 24ce5ae082 fix: enforce org visibility on organization label read endpoints
The GET /api/v1/orgs/{org}/labels and GET /api/v1/orgs/{org}/labels/{id}
endpoints did not check whether the caller could see the organization, so
labels of a private org were disclosed to non-members (and anonymously for
the list route). Add a reqOrgVisible() middleware mirroring the visibility
check used by org.Get and apply it to the labels group.
2026-06-13 17:30:10 +02:00
383 changed files with 5448 additions and 10925 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ runs:
using: composite
steps:
- uses: ./.github/actions/free-disk-space
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
+1 -1
View File
@@ -9,7 +9,7 @@ inputs:
runs:
using: composite
steps:
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- if: ${{ inputs.cache == 'true' }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
gobuild:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
- run: make deps-backend deps-tools
- run: TAGS="bindata" make backend
@@ -59,7 +59,7 @@ jobs:
include:
- { tags: "bindata", target: "lint-backend" }
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
with:
lint-cache: "true"
+2 -2
View File
@@ -12,8 +12,8 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
+2 -2
View File
@@ -20,8 +20,8 @@ jobs:
if: github.repository == 'go-gitea/gitea' # prevent running on forks
timeout-minutes: 30
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: renovatebot/github-action@6d859fc95779be83a0335ca704879b47e5d79641 # v46.1.16
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd # v46.1.15
with:
renovate-version: ${{ env.RENOVATE_VERSION }}
configurationFile: renovate.json5
+2 -2
View File
@@ -12,8 +12,8 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
with:
upload_sources: true
upload_translations: false
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
e2e: ${{ steps.changes.outputs.e2e }}
shell: ${{ steps.changes.outputs.shell }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4
- uses: go-gitea/giteabot@f8a6f4c14d46920b4b5448852be3de72d00066f0 # v1.0.3
with:
github_token: ${{ secrets.GITEABOT_TOKEN }}
gitea_fork: giteabot/gitea
+1 -14
View File
@@ -1,16 +1,9 @@
name: giteabot
on:
# When main advances, rerun merge queue maintenance so the oldest
# reviewed/wait-merge PR can be updated against the new base promptly.
push:
branches:
- main
# pull_request_target gives this workflow access to GITEABOT_TOKEN on PRs from
# forks, which the bot needs to write labels, statuses and comments. Safe here
# because the job only runs a pinned action and never checks out PR HEAD.
# These PR lifecycle events drive label maintenance, queue maintenance, and
# explicit bot actions triggered by relevant label changes.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types:
- opened
@@ -20,19 +13,13 @@ on:
- closed
- review_requested
- review_request_removed
# Review events keep review-derived state such as lgtm labels and status checks
# in sync after approvals, edits, or dismissals.
pull_request_review:
types:
- submitted
- edited
- dismissed
# Periodic maintenance is still useful as a backstop for queue cleanup and
# other housekeeping, even though main pushes now trigger it promptly.
schedule:
- cron: "15 3 * * *"
# Allow maintainers to rerun selected checks manually when debugging bot
# behavior without waiting for another repository event.
workflow_dispatch:
inputs:
checks:
@@ -58,7 +45,7 @@ jobs:
steps:
# pull_request_review runs without repository secrets on fork PRs, so fall
# back to the workflow token for the non-backport checks handled here.
- uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4
- uses: go-gitea/giteabot@f8a6f4c14d46920b4b5448852be3de72d00066f0 # v1.0.3
with:
github_token: ${{ secrets.GITEABOT_TOKEN || github.token }}
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
+5 -5
View File
@@ -19,7 +19,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
with:
lint-cache: "true"
@@ -31,7 +31,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
with:
cache: "false"
@@ -62,7 +62,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
- run: make deps-backend deps-tools
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
@@ -72,7 +72,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/node-setup
- run: make deps-frontend
- run: make lint-frontend
@@ -85,7 +85,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
- run: make deps-backend generate-go
# no frontend build here as backend should be able to build, even without any frontend files
+7 -7
View File
@@ -42,7 +42,7 @@ jobs:
ports:
- "9000:9000"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
- uses: ./.github/actions/pgsql-shard
with:
@@ -78,7 +78,7 @@ jobs:
ports:
- "9000:9000"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
- uses: ./.github/actions/pgsql-shard
with:
@@ -90,7 +90,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
- run: make deps-backend
- run: make backend
@@ -131,7 +131,7 @@ jobs:
ports:
- "7700:7700"
redis:
image: redis:latest@sha256:c904002d182255b6db3cbe3a1e8ce6c187d15390c39500b59fc07181aabff7bf
image: redis:latest@sha256:a505f8b9d8ac3ff7b0848055b4abf1901d6d77606774aa1e38bd37f1197ed2b5
options: >- # wait until redis has started
--health-cmd "redis-cli ping"
--health-interval 5s
@@ -151,7 +151,7 @@ jobs:
ports:
- 10000:10000
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
@@ -208,7 +208,7 @@ jobs:
- "587:587"
- "993:993"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
@@ -241,7 +241,7 @@ jobs:
ports:
- 10000:10000
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/amd64
@@ -31,7 +31,7 @@ jobs:
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/arm64
@@ -41,7 +41,7 @@ jobs:
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/docker-dryrun
with:
platform: linux/riscv64
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: ./.github/actions/go-setup
- uses: ./.github/actions/node-setup
- run: make deps-frontend
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
pull-requests: write
steps:
# Base-branch checkout only: pull_request_target runs with elevated token; never run PR-head code here.
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.base.sha }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -17,15 +17,11 @@ jobs:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install snapcraft
run: sudo snap install snapcraft --classic
- name: Authenticate snapcraft
shell: bash
run: snapcraft login --with <(printf '%s' "$SNAPCRAFT_STORE_CREDENTIALS")
- name: Remote build
run: |
snapcraft remote-build \
+4 -8
View File
@@ -13,17 +13,16 @@ jobs:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
@@ -34,8 +33,6 @@ jobs:
- run: make release
env:
TAGS: bindata
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
@@ -48,7 +45,6 @@ jobs:
GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
run: |
for f in dist/release/*; do
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
@@ -79,7 +75,7 @@ jobs:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
+4 -8
View File
@@ -14,17 +14,16 @@ jobs:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
@@ -35,8 +34,6 @@ jobs:
- run: make release
env:
TAGS: bindata
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
@@ -49,7 +46,6 @@ jobs:
GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
run: |
for f in dist/release/*; do
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
@@ -90,7 +86,7 @@ jobs:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
+4 -8
View File
@@ -17,17 +17,16 @@ jobs:
permissions:
contents: read
packages: write # to publish to ghcr.io
id-token: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
@@ -38,8 +37,6 @@ jobs:
- run: make release
env:
TAGS: bindata
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
@@ -52,7 +49,6 @@ jobs:
GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
run: |
for f in dist/release/*; do
cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes
echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
@@ -93,7 +89,7 @@ jobs:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
-1
View File
@@ -107,7 +107,6 @@ linters:
- -QF1008
testifylint:
disable:
- empty
- go-require
- require-error
usetesting:
-50
View File
@@ -4,56 +4,6 @@ This changelog goes through the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.26.4](https://github.com/go-gitea/gitea/releases/tag/1.26.4) - 2026-06-21
* SECURITY
* fix(auth): do not auto-reactivate disabled users on OAuth2 callback (#38009) (#38183)
* BUGFIXES
* fix: walk git log context error handling (#38182) (#38185)
## [1.26.3](https://github.com/go-gitea/gitea/releases/tag/1.26.3) - 2026-06-18
* BREAKING
* fix(actions)!: require merged PR to bypass fork PR approval gate (#38010) (#38041)
* SECURITY
* fix(hostmatcher): patch incorrect private list (#38170) (#38173)
* fix: Various security fixes (#38103) (#38151)
* fix: Various sec fixes (#38108) (#38147)
* fix: allow git clone of private repos with anonymous code access (#38074) (#38146)
* fix(auth): ignore stale OIDC external login links to organizations (#37875) (#38141)
* fix(hostmatcher): block reserved IP ranges from external/private filters (#38039) (#38059)
* fix(lfs): require Code-unit access for cross-repo LFS object reuse (#38006) (#38050)
* fix(lfs): reject unknown SSH LFS sub-verbs to prevent auth bypass (#38008) (#38015)
* fix: bound CODEOWNERS regex match time (#38011) (#38025)
* fix: bound debian ParseControlFile to a single control stanza (#38044) (#38055)
* fix(deps): update module golang.org/x/net to v0.55.0 [security] (#37813) (#37829)
* API
* feat(api): add Link header in ListForks (#38052) (#38063)
* BUGFIXES
* fix: Fix the panic when ssh remote lfs endpoint parsing failure (#38026) (#38158)
* fix(api): nil pointer panic when filtering tracked times by a non-existent user (#38112) (#38115)
* fix: keep literal "false" value displayed in workflow_dispatch choice dropdowns (#38080) (#38096)
* fix: parse HEAD ref (#38119)
* fix: git cmd (#38084) (#38087)
* fix(releases): generate notes for initial tag (#37697) (#37986)
* fix(actions): return 404 when job log blob is missing (#38003) (#38004)
* fix(actions): exclude `workflow_call` from workflow trigger detection (#37894) (#37899)
* fix(actions): keep action run title clickable when commit subject is a URL (#37867) (#37898)
* fix(actions): reject workflow_dispatch for workflows without that trigger (#37660) (#37895)
* fix(actions): ack re-sent `UpdateLog` finalize idempotently (#37885) (#37892)
* fix: http content file render (#37850) (#37856)
* fix(issues): clear stale ReviewTypeRequest when submitting pending review (#37809) (#37815)
* fix: Fix issue target branch selection for non-collaborators (#36916) (#38164)
* BUILD
* fix(deps): update `@playwright/test` to 1.60.0 (#38144)
* ci: add `tools/ci-tools.ts` for the PR labeler workflow (#37831)
* fix(build): swagger css import (#37801) (#37803)
## [1.26.2](https://github.com/go-gitea/gitea/releases/tag/1.26.2) - 2026-05-20
* SECURITY
+35 -15
View File
@@ -2,17 +2,12 @@
This document explains how to contribute changes to the Gitea project. Topic-specific guides live in separate files so the essentials are easier to find.
| Topic | Document |
|:-----------------------|:-----------------------------------------------------------------|
| Setup and requirements | [docs/build-setup.md](docs/build-setup.md) |
| Development workflow | [docs/development.md](docs/development.md) |
| Build from source | [docs/build-source.md](docs/build-source.md) |
| Running the tests | [docs/testing.md](docs/testing.md) |
| Frontend guidelines | [docs/guidelines-frontend.md](docs/guidelines-frontend.md) |
| Backend guidelines | [docs/guidelines-backend.md](docs/guidelines-backend.md) |
| Refactoring | [docs/guidelines-refactoring.md](docs/guidelines-refactoring.md) |
| Community Governance | [docs/community-governance.md](docs/community-governance.md) |
| Release management | [docs/release-management.md](docs/release-management.md) |
| Topic | Document |
| :---- | :------- |
| Backend (Go modules, API v1) | [docs/guideline-backend.md](docs/guideline-backend.md) |
| Frontend (npm, UI guidelines) | [docs/guideline-frontend.md](docs/guideline-frontend.md) |
| Maintainers, TOC, labels, merge queue, commit format for mergers | [docs/community-governance.md](docs/community-governance.md) |
| Release cycle, backports, tagging releases | [docs/release-management.md](docs/release-management.md) |
<details><summary>Table of Contents</summary>
@@ -48,7 +43,7 @@ This document explains how to contribute changes to the Gitea project. Topic-spe
It assumes you have followed the [installation instructions](https://docs.gitea.com/category/installation). \
Sensitive security-related issues should be reported to [security@gitea.io](mailto:security@gitea.io).
For configuring IDEs for Gitea development, see the [IDE setup notes](docs/development.md#ide-configuration) and the [contributed configurations](contrib/development/).
For configuring IDEs for Gitea development, see the [contributed IDE configurations](contrib/ide/).
## AI Contribution Policy
@@ -111,8 +106,7 @@ If further discussion is needed, we encourage you to open a new issue instead an
## Building Gitea
See [docs/setup.md](docs/setup.md) for prerequisites and [docs/development.md](docs/development.md)
for building Gitea and the development workflow.
See the [development setup instructions](https://docs.gitea.com/development/hacking-on-gitea).
## Styleguide
@@ -131,7 +125,33 @@ Afterwards, copyright should only be modified when the copyright author changes.
## Testing
Before submitting a pull request, run the linters (`make lint`, or the scoped `make lint-backend` / `make lint-frontend`) and the tests to make sure your changes don't cause a regression elsewhere. See [docs/testing.md](docs/testing.md) for how to run the unit, integration, end-to-end, and migration tests.
Before submitting a pull request, run all tests to make sure your changes don't cause a regression elsewhere.
Here's how to run the test suite:
- code lint
| | |
| :-------------------- | :--------------------------------------------------------------------------- |
|``make lint`` | lint everything (not needed if you only change the front- **or** backend) |
|``make lint-frontend`` | lint frontend files |
|``make lint-backend`` | lint backend files |
- run tests (we suggest running them on Linux)
| Command | Action | |
|:----------------------------------------------|:-----------------------------------------------------| ------------------------------------------- |
| ``make test-backend[\#SpecificTestName]`` | run unit test(s) | |
| ``make test-integration[\#SpecificTestName]`` | run [integration](tests/integration) test(s) | [More details](tests/integration/README.md) |
| ``make test-e2e`` | run [end-to-end](tests/e2e) test(s) using Playwright | |
- E2E test environment variables
| Variable | Description |
| :-------------------------------- | :---------------------------------------------------------- |
| ``GITEA_TEST_E2E_DEBUG`` | When set, show Gitea server output |
| ``GITEA_TEST_E2E_FLAGS`` | Additional flags passed to Playwright, for example ``--ui`` |
| ``GITEA_TEST_E2E_TIMEOUT_FACTOR`` | Timeout multiplier (default: 4 on CI, 1 locally) |
## Translation
+5 -6
View File
@@ -16,9 +16,9 @@ EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-che
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.1 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.0 # renovate: datasource=go
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.4.0 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.0 # renovate: datasource=go
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker
@@ -127,7 +127,6 @@ BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* mo
GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go
SVG_DEST_DIR := public/assets/img/svg
SVG_DEST_DIRS := $(SVG_DEST_DIR) options/fileicon
AIR_TMP_DIR := .air
@@ -634,10 +633,10 @@ svg: node_modules ## build svg files
.PHONY: svg-check
svg-check: svg
@git add $(SVG_DEST_DIRS)
@diff=$$(git diff --color=always --cached $(SVG_DEST_DIRS)); \
@git add $(SVG_DEST_DIR)
@diff=$$(git diff --color=always --cached $(SVG_DEST_DIR)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make svg' and 'git add $(SVG_DEST_DIRS)' and commit the result:"; \
echo "Please run 'make svg' and 'git add $(SVG_DEST_DIR)' and commit the result:"; \
printf "%s" "$${diff}"; \
exit 1; \
fi
+31 -22
View File
@@ -14,21 +14,21 @@
## Purpose
The goal of Gitea is to make the easiest, fastest, and most painless way of
setting up a self-hosted all-in-one software development service,
including Git hosting, code management, code review, issue tracking, project kanban, wiki,
team collaboration, package registry and CI/CD which can reuse GitHub Actions.
The goal of this project is to make the easiest, fastest, and most
painless way of setting up a self-hosted Git service.
As Gitea is written in Go, it works across **all** the platforms and
architectures that are supported by Go, including Linux, macOS, FreeBSD/OpenBSD and Windows
on x86, amd64, ARM, RISC-V 64 and PowerPC architectures.
architectures that are supported by Go, including Linux, macOS, and
Windows on x86, amd64, ARM and PowerPC architectures.
This project has been
[forked](https://blog.gitea.com/welcome-to-gitea/) from
[Gogs](https://gogs.io) since November of 2016, but a lot has changed.
For online demonstrations, you can visit [demo.gitea.com](https://demo.gitea.com).
For accessing free Gitea service (with a limited number of repositories), you can visit [gitea.com](https://gitea.com/user/login).
To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com),
or use container (docker/podman/etc) to deploy on your own server with the [official image](https://hub.docker.com/r/gitea/gitea).
To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com).
## Documentation
@@ -40,12 +40,27 @@ If you have any suggestions or would like to contribute to it, you can visit the
## Building
See [docs/build-setup.md](docs/build-setup.md) for prerequisites
and [docs/development.md](docs/development.md) for setting up a local development environment, linting, and testing.
From the root of the source tree, run:
If you'd like to build from source or make a distribution package, see [docs/build-source.md](docs/build-source.md) for more information.
TAGS="bindata" make build
After building, you can run `./gitea web` to start the server, or `./gitea help` to see all available commands.
The `build` target is split into two sub-targets:
- `make backend` which requires [Go Stable](https://go.dev/dl/), the required version is defined in [go.mod](/go.mod).
- `make frontend` which requires [Node.js LTS](https://nodejs.org/en/download/) or greater and [pnpm](https://pnpm.io/installation).
Internet connectivity is required to download the go and npm modules. When building from the official source tarballs which include pre-built frontend files, the `frontend` target will not be triggered, making it possible to build without Node.js.
More info: https://docs.gitea.com/installation/install-from-source
## Using
After building, a binary file named `gitea` will be generated in the root of the source tree by default. To run it, use:
./gitea web
> [!NOTE]
> If you're interested in using our APIs, we have experimental support with [documentation](https://docs.gitea.com/api).
## Contributing
@@ -54,8 +69,7 @@ Expected workflow is: Fork -> Patch -> Push -> Pull Request
> [!NOTE]
>
> 1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
> 2. New to the codebase? The [development guide](docs/development.md) walks through setting up a local environment and building from source.
> 3. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
> 2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
## Translating
@@ -112,19 +126,14 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea" with a hard g.
**How do I configure Gitea?**
**Why is this not hosted on a Gitea instance?**
For dynamic config options, you can change it on your admin panel's configuration section.
For static config options, you can edit your `app.ini` file and resart the instance.
See [app.example.ini](https://github.com/go-gitea/gitea/blob/main/custom/conf/app.example.ini) or [configuration documentation](https://docs.gitea.com/administration/config-cheat-sheet) for more details.
We're [working on it](https://github.com/go-gitea/gitea/issues/1029).
**Where can I find the security patches?**
In the [release log](https://github.com/go-gitea/gitea/releases) or the [change log](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md), search for the keyword `SECURITY` to find the security patches.
(more FAQs are listed in [FAQ documentation](https://docs.gitea.com/help/faq))
## License
This project is licensed under the MIT License.
@@ -134,7 +143,7 @@ for the full license text.
## Further information
<details>
<summary>Looking for an overview of the interface? Check it out the screenshots!</summary>
<summary>Looking for an overview of the interface? Check it out!</summary>
### Login/Register Page
+1 -2
View File
@@ -158,8 +158,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
}
isAdmin := c.Bool("admin")
// Only local, existing, regular users should be forced to update their password. Bot users for example are non-interactive
mustChangePassword := userType == user_model.UserTypeIndividual
mustChangePassword := true // always default to true
if c.IsSet("must-change-password") {
if userType != user_model.UserTypeIndividual {
return errors.New("must-change-password flag can only be set for individual users")
-1
View File
@@ -63,7 +63,6 @@ func TestAdminUserCreate(t *testing.T) {
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
assert.Equal(t, user_model.UserTypeBot, u.Type)
assert.Empty(t, u.Passwd)
assert.False(t, u.MustChangePassword, "bot users should not be forced to change password")
})
t.Run("AccessToken", func(t *testing.T) {
+1 -12
View File
@@ -203,7 +203,7 @@ func TestCliCmdError(t *testing.T) {
assert.Error(t, err)
assert.Equal(t, 1, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n", r.Stderr)
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
r, err = runTestApp(app, "./gitea", "test-cmd")
@@ -235,14 +235,3 @@ func TestCliCmdBefore(t *testing.T) {
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
assert.Equal(t, "/dev/null", configValues["action"])
}
func TestCliCmdCompletion(t *testing.T) {
app := newTestApp(cli.Command{
Action: func(ctx context.Context, cmd *cli.Command) error { return nil },
})
res, err := runTestApp(app, "./gitea", "completion", "bash", "--nonexist")
assert.Error(t, err)
assert.Equal(t, 1, res.ExitCode)
assert.Equal(t, "", res.Stdout)
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -nonexist\n", res.Stderr)
}
+4 -25
View File
@@ -5,10 +5,10 @@ package cmd
import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
@@ -154,37 +154,16 @@ func NewMainApp(appVer AppVersion) *cli.Command {
return app
}
// usageErr marks a usage error already reported by cliOnUsageError, so RunMainApp does not print it again.
type usageErr struct{ err error }
func (e usageErr) Error() string { return e.err.Error() }
func (e usageErr) Unwrap() error { return e.err }
// cliOnUsageError reports usage errors itself instead of letting urfave/cli dump the full help to stdout (since urfave/cli v3.10).
func cliOnUsageError(_ context.Context, cmd *cli.Command, err error, _ bool) error {
_, _ = fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n", err.Error())
return usageErr{err}
}
func setCLIOnUsageError(cmd *cli.Command) {
_ = cmd.Walk(func(c *cli.Command) error {
c.OnUsageError = cliOnUsageError
return nil
})
}
func RunMainApp(app *cli.Command, args ...string) error {
ctx, cancel := installSignals()
defer cancel()
setCLIOnUsageError(app)
// the completion subcommands are built during app.Run, after the Walk above, so cover them via this hook
app.ConfigureShellCompletionCommand = setCLIOnUsageError
err := app.Run(ctx, args)
if err == nil {
return nil
}
if _, ok := errors.AsType[usageErr](err); ok {
cli.OsExiter(1) // cliOnUsageError already reported it
if strings.HasPrefix(err.Error(), "flag provided but not defined:") {
// the cli package should already have output the error message, so just exit
cli.OsExiter(1)
return err
}
_, _ = fmt.Fprintf(app.ErrWriter, "Command error: %v\n", err)
+1 -3
View File
@@ -1,14 +1,12 @@
# IDE and code editor configuration
## Table of Contents
- [IDE and code editor configuration](#ide-and-code-editor-configuration)
- [Microsoft Visual Studio Code](#microsoft-visual-studio-code)
## Microsoft Visual Studio Code
Download Microsoft Visual Studio Code at https://code.visualstudio.com/ and follow instructions at https://code.visualstudio.com/docs/languages/go to setup Go extension for it.
Create new directory `.vscode` in Gitea root folder and copy contents of folder [contrib/development/vscode](vscode/) to it. You can now use `Ctrl`+`Shift`+`B` to build gitea executable and `F5` to run it in debug mode.
Create new directory `.vscode` in Gitea root folder and copy contents of folder [contrib/ide/vscode](vscode/) to it. You can now use `Ctrl`+`Shift`+`B` to build gitea executable and `F5` to run it in debug mode.
Supported on Debian, Ubuntu, Red Hat, Fedora, SUSE Linux, MacOS and Microsoft Windows.
+1 -10
View File
@@ -762,12 +762,7 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Git config options
;; The format is `some.configKey = value`.
;; These options will be written into the gitconfig file under "[git] HOME_PATH" when Gitea web starts.
;; ATTENTION:
;; * It only does "set" config, a removed config key from this section won't be removed from git config automatically.
;; * Some config options might affect the behavior of git and fail Gitea's git operation,
;; make sure you know what you are doing before making changes.
;; This section only does "set" config, a removed config key from this section won't be removed from git config automatically. The format is `some.configKey = value`.
;[git.config]
;diff.algorithm = histogram
;core.logAllRefUpdates = true
@@ -3002,10 +2997,6 @@ LEVEL = Info
;; Comma-separated list of workflow directories, the first one to exist
;; in a repo is used to find Actions workflow files
;WORKFLOW_DIRS = .gitea/workflows,.github/workflows
;; Comma-separated list of scoped workflow directories in a source repository, the first one to exist is used.
;; Files here are picked up only when the repo is registered as a scoped-workflow source; in any other repo they neither run repo-level nor scope-level.
;; Must not overlap with WORKFLOW_DIRS. Leave empty so no directory is scanned; no scoped workflows are found or run.
;SCOPED_WORKFLOW_DIRS = .gitea/scoped_workflows
;; Maximum number of attempts a single workflow run can have. Default value is 50.
;MAX_RERUN_ATTEMPTS = 50
-67
View File
@@ -1,67 +0,0 @@
# Setup and requirements
This document lists the tools you need to build Gitea from source and how to get
the code. Once your environment is ready, see [development.md](development.md) for
the build and development workflow, and [testing.md](testing.md) for running tests.
For the contribution workflow and review process, see [CONTRIBUTING.md](../CONTRIBUTING.md).
## Requirements
### Go
[Install Go](https://go.dev/doc/install) and set up your Go environment. The
required version is the one declared in [`go.mod`](../go.mod); installing the same
version your continuous integration uses avoids `gofmt` differences between Go
releases.
> [!NOTE]
> Some `make` tasks build external Go tools on demand (for example `make
> watch-backend`). To use them, the `"$GOPATH"/bin` directory must be on your
> executable `PATH`; otherwise you have to manage those tools yourself.
### Node.js and pnpm
[Install Node.js](https://nodejs.org/en/download/) to build the JavaScript and CSS
files. The minimum supported version is the one declared in
[`package.json`](../package.json) (`engines.node`); the latest LTS is recommended.
Gitea manages frontend dependencies with [pnpm](https://pnpm.io/). The `make`
targets invoke it for you, so installing pnpm manually is only needed if you want
to run `pnpm` commands directly.
### Make
Gitea uses [Make](https://www.gnu.org/software/make/) to drive builds, linting, and
tests. On Windows it can be installed via [MSYS2](https://www.msys2.org/) or
[Chocolatey](https://chocolatey.org/packages/make).
### Python with uv (optional)
Linting the templates, workflow files, and YAML requires Python tooling that Gitea
runs through [uv](https://docs.astral.sh/uv/). After installing uv, `make` creates
the environment automatically (`uv sync`); you only need this if you run
`make lint-templates`, `make lint-yaml`, or `make lint-actions` locally.
### Git LFS
The integration tests require [Git LFS](https://git-lfs.com/) to be installed.
## Getting the source code
Clone the repository:
```bash
git clone https://github.com/go-gitea/gitea
```
To contribute changes, [fork the repository](https://github.com/go-gitea/gitea) on
GitHub and add your fork as a git remote so you can push branches and open pull
requests. See GitHub's [working with forks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks)
documentation for the details.
## Installing dependencies
Most build and test targets install the dependencies they need on their own. To
fetch everything up front, run `make deps` (or the per-group `make deps-frontend`,
`make deps-backend`, `make deps-tools`, `make deps-py`).
-92
View File
@@ -1,92 +0,0 @@
# Prepare build environment
Complete the steps in [build-setup.md](build-setup.md) to prepare your environment for building Gitea from source.
## Choose a branch
By default, the cloned repository is on main branch (the current development branch for next major release, aka: main nightly).
You can switch to a versioned branch (the branch for the next minor stable release, aka: stable nightly )
or a versioned tag (matches the official releases with version numbers)
To test a Pull Request, you can fetch its code by its Pull Request number (take `PR #123456` as example):
```bash
git fetch origin pull/123456/head:pr-123456
```
# Build
Various [make tasks](https://github.com/go-gitea/gitea/blob/main/Makefile)
are provided to keep the build process as simple as possible.
Depending on requirements, the following build tags can be included.
- `bindata`: Build a single monolithic binary, with all assets included. Required for distribution and production build.
- `pam`: Enable support for PAM (Linux Pluggable Authentication Modules).
Can be used to authenticate local users or extend authentication to methods available to PAM.
- `gogit`: (EXPERIMENTAL) Use go-git variants of Git commands.
To include all assets, use the `bindata` tag:
```bash
TAGS="bindata" make build
```
Tag `gogit` is used to try to resolve some Windows-specific performance problems, POSIX systems don't need it.
You can build a Windows binary by:
```bash
GOOS=windows TAGS="bindata gogit" make build
```
## Changing default paths
Gitea will search for a number of things from the _`CustomPath`_.
By default, this is the `custom/` directory in the current working directory when running Gitea.
It will also look for its configuration file _`CustomConf`_ in `$(CustomPath)/conf/app.ini`,
and will use the current working directory as the relative base path _`AppWorkPath`_.
These values, although useful when developing, may conflict with downstream users preferences.
For packagers who need to use paths like `/etc/gitea/app.ini`,
they should define these values at build time for `make build` by environment variable like
`LDFLAGS='-X "module.Var1=Value1" -X "module.Var2=Value2"' TAGS="bindata" make build`.
- _`CustomConf`_: `-X "code.gitea.io/gitea/modules/setting.CustomConf=/etc/gitea/app.ini"`
- _`AppWorkPath`_: `-X "code.gitea.io/gitea/modules/setting.AppWorkPath=/var/lib/gitea"`
- _`CustomPath`_: `-X "code.gitea.io/gitea/modules/setting.CustomPath=/var/lib/gitea/custom"`
- Default PID file location: `-X "code.gitea.io/gitea/cmd.PIDFile=/run/gitea.pid"`
Add as many of the strings with their preceding `-X` to the `LDFLAGS` variable and run `make build`
with the appropriate `TAGS` as above.
Running `gitea help` will allow you to review what the computed settings will be for your `gitea`.
## Cross Build
Gitea use's Golang's toolchain variables for cross-building.
For example, to cross build for Linux ARM64:
```
GOOS=linux GOARCH=arm64 TAGS="bindata" make build
```
### Adding shell autocompletion
Shell completion can be generated directly from binary with:
```sh
gitea completion <shell>
```
Supported values for `<shell>` are `bash`, `fish`, `pwsh` and `zsh`.
Details on how to load the completion for your shell can be found in the completion command help.
## Source Maps
By default, gitea generates reduced source maps for frontend files to conserve space. This can be controlled with the `ENABLE_SOURCEMAP` environment variable:
- `ENABLE_SOURCEMAP=true` generates all source maps, the default for development builds
- `ENABLE_SOURCEMAP=reduced` generates limited source maps, the default for production builds
- `ENABLE_SOURCEMAP=false` generates no source maps
-141
View File
@@ -1,141 +0,0 @@
# Development
This document describes how to build Gitea from source and the day-to-day
development workflow. For prerequisites and how to obtain the code, see
[build-setup.md](build-setup.md). For running tests, see [testing.md](testing.md). For the
contribution workflow and review process, see [CONTRIBUTING.md](../CONTRIBUTING.md).
Area-specific guidelines:
- [Backend development guidelines](guidelines-backend.md)
- [Frontend development guidelines](guidelines-frontend.md)
- [Refactoring guidelines](guidelines-refactoring.md)
## Building
To build Gitea for development, run:
```bash
make build
```
No build tags are required: SQLite support is compiled in by default, which is
enough for local development. The `build` target runs two sub-targets, `frontend`
and `backend`. The `bindata` tag embeds the frontend assets into the binary and is
only needed when packaging a self-contained build, so leave it out during
development.
See `make help` for all available targets, and the workflows in
[`.github/workflows`](https://github.com/go-gitea/gitea/tree/main/.github/workflows)
to see how continuous integration builds and checks Gitea.
## Building continuously
To rebuild automatically when source files change:
```bash
# watch both frontend and backend
make watch
# or watch only the frontend (starts the Vite dev server)
make watch-frontend
# or watch only the backend (Go)
make watch-backend
```
Watching all backend source files may hit the default open-files limit on macOS or
Linux; raise it with `ulimit -n 12288` for the current shell, or in your shell
startup file to make it permanent.
## Formatting, linting and checks
Continuous integration rejects pull requests that fail formatting, linting, or
consistency checks. Format your code first:
```bash
make fmt
```
Then lint:
```bash
# lint everything
make lint
# or only one side
make lint-backend
make lint-frontend
```
Many linters can fix issues automatically with `make lint-fix` (or the scoped
`make lint-backend-fix` / `make lint-frontend-fix`). The combined consistency
checks that CI runs are available as `make checks`.
## Building and adding SVGs
SVG icons are built with `make svg`, which compiles the icon sources into
`public/assets/img/svg`. Custom icons can be added under `web_src/svg`.
## Updating the API
When you create or change API routes, you **must** update the
[Swagger](https://swagger.io/docs/specification/2-0/what-is-swagger/) documentation
using [go-swagger](https://goswagger.io/) comments. See the
[backend development guidelines](guidelines-backend.md) for how API routes,
request/response structs, and swagger definitions fit together.
Regenerate and validate the spec after changing an endpoint, then commit the
updated JSON:
```bash
make generate-swagger
make swagger-validate
```
CI verifies the committed spec is up to date with:
```bash
make swagger-check
```
## Creating new configuration options
When adding configuration options it is not enough to add them to the
`modules/setting` files. Also update
[`custom/conf/app.example.ini`](../custom/conf/app.example.ini), and document them in
the [configuration cheat sheet](https://docs.gitea.com/administration/config-cheat-sheet),
which lives in the [documentation repository](https://gitea.com/gitea/docs).
## Database migrations
If you make breaking changes to a database-persisted struct under `models/`, add a
new migration in `models/migrations/`. See [testing.md](testing.md#migration-tests)
for running the migration tests.
## Testing
For unit, integration, end-to-end, and migration tests, see [testing.md](testing.md).
## IDE configuration
### Visual Studio Code
A `launch.json` and `tasks.json` are provided in
[`contrib/development/vscode`](../contrib/development/vscode). See
[`contrib/development/README.md`](../contrib/development/README.md) for details.
### GoLand
Clicking the `Run Application` arrow on `func main()` in `/main.go` starts a
debuggable Gitea instance.
The `Output Directory` in `Run/Debug Configuration` **must** be set to the Gitea
project directory (the one containing `main.go` and `go.mod`). Otherwise the working
directory is a GoLand temporary directory, which prevents Gitea from loading dynamic
resources (such as templates) in development.
## Submitting your changes
Push your branch and open a pull request. See [CONTRIBUTING.md](../CONTRIBUTING.md)
for the review process and PR requirements. For help, join the `#Develop` channel on
[Discord](https://discord.gg/gitea).
+63
View File
@@ -0,0 +1,63 @@
# Backend development
This document covers backend-specific contribution expectations. For general contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md).
For coding style and architecture, see also the [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend) on the documentation site.
## Dependencies
Go dependencies are managed using [Go Modules](https://go.dev/cmd/go/#hdr-Module_maintenance). \
You can find more details in the [go mod documentation](https://go.dev/ref/mod) and the [Go Modules Wiki](https://github.com/golang/go/wiki/Modules).
Pull requests should only modify `go.mod` and `go.sum` where it is related to your change, be it a bugfix or a new feature. \
Apart from that, these files should only be modified by Pull Requests whose only purpose is to update dependencies.
The `go.mod`, `go.sum` update needs to be justified as part of the PR description,
and must be verified by the reviewers and/or merger to always reference
an existing upstream commit.
## API v1
The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest).
### GitHub API compatibility
Gitea's API should use the same endpoints and fields as the GitHub API as far as possible, unless there are good reasons to deviate. \
If Gitea provides functionality that GitHub does not, a new endpoint can be created. \
If information is provided by Gitea that is not provided by the GitHub API, a new field can be used that doesn't collide with any GitHub fields. \
Updating an existing API should not remove existing fields unless there is a really good reason to do so. \
The same applies to status responses. If you notice a problem, feel free to leave a comment in the code for future refactoring to API v2 (which is currently not planned).
### Adding/Maintaining API routes
All expected results (errors, success, fail messages) must be documented ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L319-L327)). \
All JSON input types must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L76-L91)) \
and referenced in [routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go). \
They can then be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L318). \
All JSON responses must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L36-L68)) \
and referenced in its category in [routers/api/v1/swagger/](routers/api/v1/swagger/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16)) \
They can be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279).
### When to use what HTTP method
In general, HTTP methods are chosen as follows:
- **GET** endpoints return the requested object(s) and status **OK (200)**
- **DELETE** endpoints return the status **No Content (204)** and no content either
- **POST** endpoints are used to **create** new objects (e.g. a User) and return the status **Created (201)** and the created object
- **PUT** endpoints are used to **add/assign** existing Objects (e.g. a user to a team) and return the status **No Content (204)** and no content either
- **PATCH** endpoints are used to **edit/change** an existing object and return the changed object and the status **OK (200)**
### Requirements for API routes
All parameters of endpoints changing/editing an object must be optional (except the ones to identify the object, which are required).
Endpoints returning lists must
- support pagination (`page` & `limit` options in query)
- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444))
### Knowledge
- Partially database table migration must use `SyncWithOptions(IgnoreDrop...)`
- Template variables with "camelCase" or "snake_case" are used for restoring the form values from a submitted form
+17
View File
@@ -0,0 +1,17 @@
# Frontend development
This document covers frontend-specific contribution expectations. For general contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md).
## Dependencies
For the frontend, we use [npm](https://www.npmjs.com/).
The same restrictions apply for frontend dependencies as for [backend dependencies](guideline-backend.md#dependencies), with the exceptions that the files for it are `package.json` and `package-lock.json`, and that new versions must always reference an existing version.
## Design guideline
Depending on your change, please read the
- [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend)
- [frontend development guideline](https://docs.gitea.com/contributing/guidelines-frontend)
- [refactoring guideline](https://docs.gitea.com/contributing/guidelines-refactoring)
-132
View File
@@ -1,132 +0,0 @@
# Backend development guidelines
This document covers backend-specific architecture and contribution expectations.
For the general workflow see [CONTRIBUTING.md](../CONTRIBUTING.md), and for building
and testing see [development.md](development.md) and [testing.md](testing.md).
## Background
The backend is written in Go. Web routing is handled by
[chi](https://github.com/go-chi/chi) and database access goes through the
[XORM](https://xorm.io/) ORM. Understanding how the packages depend on each other is
essential before contributing backend code.
## Package design
### Package layout
The backend is split into top-level packages, each with a focused responsibility:
- `build`: helper scripts used at compile time
- `cmd`: subcommands such as `web`, `serv`, `hooks`, `doctor`, and admin utilities
- `models`: data structures and database operations (XORM); keeps external
dependencies to a minimum
- `models/db`: core database operations
- `models/fixtures`: sample data used by tests
- `models/migrations`: schema migration scripts
- `modules`: standalone functionality with few dependencies
- `modules/setting`: configuration handling
- `modules/git`: interaction with the Git command line
- `routers`: request handlers, split into `api`, `web`, `install`, and `private`
- `services`: business logic that ties routers and models together
- `templates`: Go HTML templates
- `public`: compiled frontend assets
- `tests`: integration and end-to-end test helpers
### Dependency direction
Dependencies only flow in one direction:
```text
cmd → routers → services → models → modules
```
A package on the left may import a package on its right, but never the reverse.
### Naming conventions
- Top-level packages use the plural form: `services`, `models`, `routers`.
- Subpackages use the singular form: `services/user`, `models/repository`.
When packages from different layers share a name, use a snake_case import alias to
disambiguate:
```go
import user_service "gitea.dev/services/user"
```
### Database transactions
Operations that must roll back together should run inside `db.WithTx()` (or
`db.WithTx2()` when a value must be returned), defined in `models/db/context.go`.
Functions that participate in a transaction take a `context.Context` as their first
parameter so the transaction can be propagated.
### XORM gotchas
- Never call `x.Update(exemplar)` without an explicit `WHERE` clause — it updates
every row in the table.
- Partial table migrations must use `SyncWithOptions(IgnoreDrop...)` rather than a
plain `Sync`.
- When inserting rows with preset IDs, MSSQL requires `SET IDENTITY_INSERT` to be
enabled and PostgreSQL requires the sequence to be updated afterwards.
## Dependencies
Go dependencies are managed with [Go Modules](https://go.dev/ref/mod).
Pull requests should only modify `go.mod` and `go.sum` where it relates to the
change at hand, be it a bug fix or a new feature. Otherwise, these files should only
be touched by pull requests whose sole purpose is updating dependencies. Run
`make tidy` after any change to `go.mod`.
Any `go.mod` / `go.sum` update must be justified in the PR description and must be
verified by reviewers and the merger to reference an existing upstream commit.
## API v1
The API is documented with [Swagger](https://gitea.com/api/swagger) and is modelled
on [the GitHub API](https://docs.github.com/en/rest).
### GitHub API compatibility
Gitea's API should use the same endpoints and fields as the GitHub API where
possible, unless there is a good reason to deviate.
- If Gitea offers functionality GitHub does not, a new endpoint may be added.
- If Gitea exposes information the GitHub API does not, a new field may be added as
long as it does not collide with a GitHub field.
- Existing fields should not be removed unless there is a strong reason; the same
applies to status responses.
If you notice a problem that would require a breaking change, leave a comment in the
code for a future refactor to API v2 (which is currently not planned) rather than
breaking v1.
### Adding and maintaining API routes
- All possible results (errors, success, and failure messages) must be documented in
the swagger comments on the route.
- Every JSON request body must be defined as a struct in `modules/structs/` and
registered in [`routers/api/v1/swagger/options.go`](../routers/api/v1/swagger/options.go).
- Every JSON response must be defined as a struct in `modules/structs/` and
registered with its category under [`routers/api/v1/swagger/`](../routers/api/v1/swagger).
### HTTP methods and status codes
In general, choose HTTP methods as follows:
- **GET** returns the requested object(s) with status **200 OK**.
- **POST** creates a new object (e.g. a user) and returns **201 Created** with the
created object.
- **PUT** adds or assigns an existing object (e.g. a user to a team) and returns
**204 No Content** with no body.
- **PATCH** edits an existing object and returns the changed object with **200 OK**.
- **DELETE** removes an object and returns **204 No Content** with no body.
### Requirements for API routes
- All parameters of endpoints that edit an object must be optional, except those
needed to identify the object, which are required.
- Endpoints returning lists must support pagination (`page` and `limit` query
options) and set the `X-Total-Count` header via `ctx.SetTotalCountHeader(...)`.
-98
View File
@@ -1,98 +0,0 @@
# Frontend development guidelines
This document covers frontend-specific architecture and contribution expectations.
For the general workflow see [CONTRIBUTING.md](../CONTRIBUTING.md), and for building
and testing see [development.md](development.md) and [testing.md](testing.md).
## Background
The frontend uses [Vue 3](https://vuejs.org/), [Fomantic-UI](https://fomantic-ui.com/) (built on jQuery)
and [Tailwind CSS](https://tailwindcss.com/). Pages are rendered with Go HTML templates.
Source files live in:
- `web_src/css/`: CSS styles
- `web_src/js/`: JavaScript and TypeScript
- `web_src/js/components/`: Vue components
- `web_src/js/features/`: feature modules wired up at page load
- `templates/`: Go HTML templates
## Dependencies
Frontend dependencies are managed with [pnpm](https://pnpm.io/). The same rules as
for [backend dependencies](guidelines-backend.md#dependencies) apply, except the
relevant files are `package.json` and `pnpm-lock.yaml`, and new versions must always
reference an existing published version.
## Framework usage
Mixing frameworks arbitrarily makes code hard to maintain. Recommended combinations:
- Vue3
- Vanilla JavaScript
- Fomantic-UI (jQuery), deprecated, we vendored a specific version with a lot of changes.
Avoid combinations such as Vue with Fomantic-UI.
Vue components may reuse Fomantic-UI CSS classes for visual consistency.
Use Go templates for simple or SEO-relevant pages and Vue for complex, interactive pages.
Gitea uses Vue 3 **without** JSX to keep HTML and JavaScript separate.
> [!NOTE]
> Fomantic-UI is not an accessibility-friendly framework. Gitea patches some ARIA
> behavior, but accessibility work is ongoing — prefer semantic HTML and test
> keyboard/screen-reader behavior where you can.
## Gitea-specific conventions
- Keep features in their own files or directories.
- Use kebab-case for HTML `id`s and classes, ideally with 2-3 feature keywords.
- Prefix classes to avoid short-name conflicts between different frameworks.
- Create a new class name when overriding framework styles instead of editing the framework's own classes,
or fix the framework's source to fix all cases.
- Prefer semantic elements such as `<button>` over generic `<div>`s.
- Avoid `!important`; when it is unavoidable, document why.
- Prefix custom DOM events with `ce-`.
## CSS
Prefer Tailwind utility classes with the `tw-` prefix, and the `flex-*` layout
helpers over per-child margins. Gitea also ships a small set of custom helpers:
`gt-` for general helpers and `g-` for framework-level helpers (see
`web_src/css/helpers.css`); use these only when a Tailwind utility does not exist.
Write class attributes as a single readable unit in templates:
```html
<div class="flex-text-inline {{if .IsFoo}}tw-hidden{{end}}"></div>
```
## TypeScript
- Use `import type` for type-only imports.
- Prefer `@ts-expect-error` over `@ts-ignore`.
- Use the `!` non-null assertion (rather than `?.`/`??`) when a value is known to always exist.
- Only mark a function `async` when it actually uses `await` or returns a `Promise`.
Avoid async event listeners; if unavoidable, call `e.preventDefault()` before the
first `await`. For a deliberately un-awaited call, assign it: `const _promise = asyncFoo()`.
## Data fetching
Use the `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` wrappers from
[`web_src/js/modules/fetch.ts`](../web_src/js/modules/fetch.ts).
## DOM attributes
Avoid `node.dataset` because of its camel-casing behavior; use `node.getAttribute`
in new code. Never bind user-provided data directly onto DOM nodes.
## Showing and hiding elements
- In Vue, use `v-if` and `v-show`.
- In Go templates and plain JavaScript, use the `.tw-hidden` class together with the
`showElem()`, `hideElem()`, and `toggleElem()` helpers from
[`web_src/js/utils/dom.ts`](../web_src/js/utils/dom.ts).
## UI component gallery
When running Gitea in development mode, standardized UI components are available at
`/devtest` (for example `http://localhost:3000/devtest`). These pages are also used
by the e2e tests.
-38
View File
@@ -1,38 +0,0 @@
# Refactoring guidelines
This document covers expectations for refactoring work. For the general workflow see
[CONTRIBUTING.md](../CONTRIBUTING.md).
## Background
Gitea is a large, long-lived project. Over time the codebase has accumulated
outdated mechanisms, mixed frameworks, and legacy code that can cause bugs or slow
down new features. Refactoring keeps the codebase maintainable, but it needs to be
done carefully so it improves things without introducing regressions.
## Writing a refactoring PR
- Be forward-looking: address the root cause, not just the immediate symptom.
- Aim to reduce ambiguity and conflicts and to improve maintainability.
- Explain the rationale in the PR description: why the refactor is necessary, how it
resolves the legacy problem, and its advantages and disadvantages.
- Keep the scope tight: preserve existing behavior where feasible and avoid bundling
unrelated changes.
- Break large refactors into intermediate steps across multiple PRs so each one is
easy to review.
- Include tests that verify the behavior stays correct.
- Prefer scheduling non-bugfix refactoring early in a milestone, so any issues
surface well before a release.
- If there is disagreement about a refactor, escalate to the Technical Oversight
Committee (TOC) for a decision.
## Reviewing and merging
- Keep refactoring PRs short-lived (typically no more than 7 days) with quick review
cycles, and merge them promptly so they do not block on unrelated work.
- A non-author core member may approve and merge a refactoring PR after 7 days if the
TOC has raised no objection.
- Accept imperfect intermediate implementations as long as the final result improves
the codebase.
- A temporary regression caused by a necessary refactor is acceptable if it is fixed
promptly afterwards.
-151
View File
@@ -1,151 +0,0 @@
# Testing
Gitea has four kinds of automated tests: backend unit tests, integration tests,
end-to-end (e2e) tests, and migration tests. Local runs default to SQLite, so no
extra services are required to get started.
For prerequisites see [build-setup.md](build-setup.md); for the build workflow see
[development.md](development.md).
## Unit tests
Backend unit tests live in `*_test.go` files next to the code they cover. Set
`GITEA_TEST_LOG_SQL=1` to log all SQL statements executed during the tests.
```bash
make test-backend
```
To run a single backend test, use `go test` directly or the `#` selector:
```bash
go test -run '^TestName$' ./modulepath/
make test-backend#TestName
```
Frontend unit tests run with [Vitest](https://vitest.dev/):
```bash
make test-frontend
# single file:
pnpm exec vitest <path-filter>
```
## Integration tests
Integration tests exercise Gitea against a real database. They live in
`tests/integration/` and require [Git LFS](https://git-lfs.com/) to be installed.
The database is selected with `GITEA_TEST_DATABASE`; an empty value defaults to
SQLite, which needs no external service:
```bash
make test-integration
```
Run a single integration test with the `#` selector:
```bash
make test-integration#TestName
```
If you hit errors such as a mismatched database version or SSH push failures, try a
clean rebuild first:
```bash
make clean build
```
### Running against other databases
Set `GITEA_TEST_DATABASE` together with the matching `TEST_*` connection variables.
The commands below start a throwaway database container (press `Ctrl-C` to stop and
remove it) and then run the tests against it.
#### MySQL
```bash
docker run -e "MYSQL_DATABASE=test" -e "MYSQL_ALLOW_EMPTY_PASSWORD=yes" -p 3306:3306 --rm --name mysql mysql:latest
```
```bash
GITEA_TEST_DATABASE=mysql TEST_MYSQL_HOST=localhost:3306 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=root TEST_MYSQL_PASSWORD='' make test-integration
```
#### PostgreSQL
PostgreSQL tests also use a MinIO container for object storage:
```bash
docker run -e "POSTGRES_DB=test" -e "POSTGRES_USER=postgres" -e "POSTGRES_PASSWORD=postgres" -p 5432:5432 --rm --name pgsql postgres:latest
docker run --rm -p 9000:9000 -e MINIO_ROOT_USER=123456 -e MINIO_ROOT_PASSWORD=12345678 --name minio bitnamilegacy/minio:2023.8.31
```
```bash
GITEA_TEST_DATABASE=pgsql TEST_MINIO_ENDPOINT=localhost:9000 TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=postgres TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-integration
```
#### MSSQL
```bash
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest
```
```bash
GITEA_TEST_DATABASE=mssql TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-integration
```
### Running the database test workflow with Gitea Runner
The CI database test jobs can be run locally with
[Gitea Runner](https://gitea.com/gitea/runner). Running every job is
resource-intensive and not recommended:
```bash
gitea-runner exec -W ./.github/workflows/pull-db-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest
```
List the available job names, then run a single one:
```bash
gitea-runner exec -W ./.github/workflows/pull-db-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest -l
gitea-runner exec -W ./.github/workflows/pull-db-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest -j <job_name>
```
## End-to-end tests
End-to-end tests drive a running Gitea instance with [Playwright](https://playwright.dev/):
```bash
make test-e2e
```
To run a single e2e test file, pass it via `GITEA_TEST_E2E_FLAGS`:
```bash
GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e
```
Useful environment variables:
| Variable | Description |
| :--- | :--- |
| `GITEA_TEST_E2E_DEBUG` | When set, show the Gitea server output. |
| `GITEA_TEST_E2E_FLAGS` | Additional flags passed to Playwright, e.g. `--ui`. |
| `GITEA_TEST_E2E_TIMEOUT_FACTOR` | Timeout multiplier (default: 4 on CI, 1 locally). |
## Migration tests
If you change a database-persisted struct under `models/` you will usually need a
new migration in `models/migrations/`. Run the migration tests with:
```bash
make test-migration
```
## Continuous integration
CI runs the unit tests, runs the integration tests against every supported database,
and tests migration from several recent Gitea versions. Please submit your pull
request with additional unit and integration tests as appropriate. Prefer unit tests
when the logic can be tested in isolation, and keep local integration and e2e tests
fast (aim for sub-2s runtime).
+51 -186
View File
@@ -1,6 +1,7 @@
import arrayFunc from 'eslint-plugin-array-func';
import comments from '@eslint-community/eslint-plugin-eslint-comments';
import deMorgan from 'eslint-plugin-de-morgan';
import github from 'eslint-plugin-github';
import globals from 'globals';
import importPlugin from 'eslint-plugin-import-x';
import playwright from 'eslint-plugin-playwright';
@@ -71,6 +72,7 @@ export default defineConfig([
regexp,
sonarjs,
unicorn,
github,
wc,
},
settings: {
@@ -84,6 +86,7 @@ export default defineConfig([
'@eslint-community/eslint-comments/no-duplicate-disable': [2],
'@eslint-community/eslint-comments/no-restricted-disable': [0],
'@eslint-community/eslint-comments/no-unlimited-disable': [2],
'@eslint-community/eslint-comments/no-unused-disable': [2],
'@eslint-community/eslint-comments/no-unused-enable': [2],
'@eslint-community/eslint-comments/no-use': [0],
'@eslint-community/eslint-comments/require-description': [0],
@@ -190,6 +193,7 @@ export default defineConfig([
'@typescript-eslint/no-duplicate-type-constituents': [2, {ignoreUnions: true}],
'@typescript-eslint/no-dynamic-delete': [0],
'@typescript-eslint/no-empty-function': [0],
'@typescript-eslint/no-empty-interface': [0],
'@typescript-eslint/no-empty-object-type': [2],
'@typescript-eslint/no-explicit-any': [0],
'@typescript-eslint/no-extra-non-null-assertion': [2],
@@ -202,6 +206,7 @@ export default defineConfig([
'@typescript-eslint/no-invalid-this': [0],
'@typescript-eslint/no-invalid-void-type': [0],
'@typescript-eslint/no-loop-func': [0],
'@typescript-eslint/no-loss-of-precision': [0],
'@typescript-eslint/no-magic-numbers': [0],
'@typescript-eslint/no-meaningless-void-operator': [0],
'@typescript-eslint/no-misused-new': [2],
@@ -230,7 +235,7 @@ export default defineConfig([
'@typescript-eslint/no-unsafe-assignment': [0],
'@typescript-eslint/no-unsafe-call': [0],
'@typescript-eslint/no-unsafe-declaration-merging': [2],
'@typescript-eslint/no-unsafe-enum-comparison': [0],
'@typescript-eslint/no-unsafe-enum-comparison': [2],
'@typescript-eslint/no-unsafe-function-type': [2],
'@typescript-eslint/no-unsafe-member-access': [0],
'@typescript-eslint/no-unsafe-return': [0],
@@ -274,6 +279,7 @@ export default defineConfig([
'@typescript-eslint/strict-void-return': [0],
'@typescript-eslint/switch-exhaustiveness-check': [0],
'@typescript-eslint/triple-slash-reference': [2],
'@typescript-eslint/typedef': [0],
'@typescript-eslint/unbound-method': [0], // too many false-positives
'@typescript-eslint/unified-signatures': [2],
'accessor-pairs': [2],
@@ -306,9 +312,32 @@ export default defineConfig([
'func-names': [0],
'func-style': [0],
'getter-return': [2],
'github/a11y-aria-label-is-well-formatted': [0],
'github/a11y-no-title-attribute': [0],
'github/a11y-no-visually-hidden-interactive-element': [0],
'github/a11y-role-supports-aria-props': [0],
'github/a11y-svg-has-accessible-name': [0],
'github/array-foreach': [0],
'github/async-currenttarget': [2],
'github/async-preventdefault': [0], // https://github.com/github/eslint-plugin-github/issues/599
'github/authenticity-token': [0],
'github/get-attribute': [0],
'github/js-class-name': [0],
'github/no-blur': [0],
'github/no-d-none': [0],
'github/no-dataset': [2],
'github/no-dynamic-script-tag': [2],
'github/no-implicit-buggy-globals': [2],
'github/no-inner-html': [0],
'github/no-innerText': [2],
'github/no-then': [2],
'github/no-useless-passive': [2],
'github/prefer-observers': [0],
'github/require-passive-events': [2],
'gitea/unescaped-html-literal': [2],
'grouped-accessor-pairs': [2],
'guard-for-in': [0],
'id-denylist': [0],
'id-blacklist': [0],
'id-length': [0],
'id-match': [0],
'import-x/consistent-type-specifier-style': [0],
@@ -355,6 +384,7 @@ export default defineConfig([
'import-x/prefer-default-export': [0],
'import-x/unambiguous': [0],
'init-declarations': [0],
'line-comment-position': [0],
'logical-assignment-operators': [0],
'max-classes-per-file': [0],
'max-depth': [0],
@@ -363,12 +393,14 @@ export default defineConfig([
'max-nested-callbacks': [0],
'max-params': [0],
'max-statements': [0],
'multiline-comment-style': [0],
'new-cap': [0],
'no-alert': [0],
'no-array-constructor': [0], // handled by @typescript-eslint/no-array-constructor
'no-async-promise-executor': [0],
'no-await-in-loop': [0],
'no-bitwise': [0],
'no-buffer-constructor': [0],
'no-caller': [2],
'no-case-declarations': [2],
'no-class-assign': [2],
@@ -525,11 +557,12 @@ export default defineConfig([
'no-nested-ternary': [0],
'no-new-func': [0], // handled by @typescript-eslint/no-implied-eval
'no-new-native-nonconstructor': [2],
'no-new-object': [2],
'no-new-symbol': [0], // handled by no-new-native-nonconstructor
'no-new-wrappers': [2],
'no-new': [0],
'no-nonoctal-decimal-escape': [2],
'no-obj-calls': [2],
'no-object-constructor': [2],
'no-octal-escape': [2],
'no-octal': [2],
'no-param-reassign': [0],
@@ -589,8 +622,10 @@ export default defineConfig([
'no-warning-comments': [0],
'no-with': [0], // handled by no-restricted-syntax
'object-shorthand': [2, 'always'],
'one-var-declaration-per-line': [0],
'one-var': [0],
'operator-assignment': [2, 'always'],
'operator-linebreak': [0], // handled by @stylistic/operator-linebreak
'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}],
'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}],
'prefer-destructuring': [0],
@@ -726,309 +761,139 @@ export default defineConfig([
'strict': [0],
'symbol-description': [2],
'unicode-bom': [2, 'never'],
'unicorn/better-dom-traversing': [2],
'unicorn/better-regex': [0],
'unicorn/catch-error-name': [0],
'unicorn/class-reference-in-static-methods': [2],
'unicorn/comment-content': [0],
'unicorn/consistent-assert': [0],
'unicorn/consistent-boolean-name': [0],
'unicorn/consistent-class-member-order': [0],
'unicorn/consistent-compound-words': [0], // too opinionated
'unicorn/consistent-conditional-object-spread': [2],
'unicorn/consistent-date-clone': [2],
'unicorn/consistent-destructuring': [2],
'unicorn/consistent-empty-array-spread': [2],
'unicorn/consistent-existence-index-check': [0],
'unicorn/consistent-export-decorator-position': [2],
'unicorn/consistent-function-scoping': [0],
'unicorn/consistent-function-style': [2],
'unicorn/consistent-json-file-read': [2],
'unicorn/consistent-optional-chaining': [2],
'unicorn/consistent-template-literal-escape': [2],
'unicorn/consistent-existence-index-check': [0],
'unicorn/consistent-function-scoping': [0],
'unicorn/custom-error-definition': [0],
'unicorn/default-export-style': [2],
'unicorn/dom-node-dataset': [2, {preferAttributes: true}],
'unicorn/empty-brace-spaces': [2],
'unicorn/error-message': [0],
'unicorn/escape-case': [0],
'unicorn/expiring-todo-comments': [0],
'unicorn/explicit-length-check': [0],
'unicorn/explicit-timer-delay': [2],
'unicorn/filename-case': [0],
'unicorn/id-match': [2],
'unicorn/import-index': [0],
'unicorn/import-style': [0],
'unicorn/isolated-functions': [2, {functions: []}],
'unicorn/logical-assignment-operators': [0],
'unicorn/max-nested-calls': [0],
'unicorn/name-replacements': [0],
'unicorn/new-for-builtins': [2],
'unicorn/no-abusive-eslint-disable': [0],
'unicorn/no-accessor-recursion': [2],
'unicorn/no-accidental-bitwise-operator': [2],
'unicorn/no-anonymous-default-export': [0],
'unicorn/no-array-callback-reference': [0],
'unicorn/no-array-concat-in-loop': [2],
'unicorn/no-array-fill-with-reference-type': [2],
'unicorn/no-array-from-fill': [2],
'unicorn/no-array-front-mutation': [0],
'unicorn/no-array-for-each': [2],
'unicorn/no-array-method-this-argument': [2],
'unicorn/no-array-push-push': [2],
'unicorn/no-array-reduce': [2],
'unicorn/no-array-reverse': [0],
'unicorn/no-array-sort': [0],
'unicorn/no-array-sort-for-min-max': [2],
'unicorn/no-array-splice': [0],
'unicorn/no-asterisk-prefix-in-documentation-comments': [0],
'unicorn/no-await-expression-member': [0],
'unicorn/no-await-in-promise-methods': [2],
'unicorn/no-blob-to-file': [2],
'unicorn/no-boolean-sort-comparator': [2],
'unicorn/no-break-in-nested-loop': [0],
'unicorn/no-canvas-to-image': [2],
'unicorn/no-chained-comparison': [2],
'unicorn/no-collection-bracket-access': [2],
'unicorn/no-computed-property-existence-check': [0],
'unicorn/no-confusing-array-splice': [2],
'unicorn/no-confusing-array-with': [2],
'unicorn/no-console-spaces': [0],
'unicorn/no-constant-zero-expression': [2],
'unicorn/no-declarations-before-early-exit': [0],
'unicorn/no-document-cookie': [2],
'unicorn/no-double-comparison': [2],
'unicorn/no-duplicate-if-branches': [2],
'unicorn/no-duplicate-logical-operands': [2],
'unicorn/no-duplicate-loops': [0],
'unicorn/no-duplicate-set-values': [2],
'unicorn/no-empty-file': [2],
'unicorn/no-error-property-assignment': [2],
'unicorn/no-exports-in-scripts': [2],
'unicorn/no-for-each': [2],
'unicorn/no-for-loop': [0],
'unicorn/no-global-object-property-assignment': [0],
'unicorn/no-hex-escape': [0],
'unicorn/no-immediate-mutation': [0],
'unicorn/no-impossible-length-comparison': [2],
'unicorn/no-incorrect-query-selector': [2],
'unicorn/no-incorrect-template-string-interpolation': [0],
'unicorn/no-instanceof-builtins': [2],
'unicorn/no-invalid-argument-count': [0],
'unicorn/no-invalid-character-comparison': [2],
'unicorn/no-instanceof-array': [0],
'unicorn/no-invalid-fetch-options': [2],
'unicorn/no-invalid-file-input-accept': [2],
'unicorn/no-invalid-remove-event-listener': [2],
'unicorn/no-keyword-prefix': [0],
'unicorn/no-late-current-target-access': [2],
'unicorn/no-length-as-slice-end': [2],
'unicorn/no-lonely-if': [2],
'unicorn/no-loop-iterable-mutation': [2],
'unicorn/no-magic-array-flat-depth': [0],
'unicorn/no-manually-wrapped-comments': [0], // too opinionated
'unicorn/no-mismatched-map-key': [2],
'unicorn/no-misrefactored-assignment': [2],
'unicorn/no-named-default': [2],
'unicorn/no-negated-array-predicate': [2],
'unicorn/no-negated-comparison': [2],
'unicorn/no-negated-condition': [0],
'unicorn/no-negation-in-equality-check': [2],
'unicorn/no-nested-ternary': [0],
'unicorn/no-new-array': [0],
'unicorn/no-new-buffer': [0],
'unicorn/no-non-function-verb-prefix': [0],
'unicorn/no-nonstandard-builtin-properties': [2],
'unicorn/no-null': [0],
'unicorn/no-object-as-default-parameter': [0],
'unicorn/no-object-methods-with-collections': [2],
'unicorn/no-optional-chaining-on-undeclared-variable': [2],
'unicorn/no-process-exit': [0],
'unicorn/no-redundant-comparison': [2],
'unicorn/no-return-array-push': [2],
'unicorn/no-selector-as-dom-name': [2],
'unicorn/no-single-promise-in-promise-methods': [2],
'unicorn/no-static-only-class': [2],
'unicorn/no-subtraction-comparison': [2],
'unicorn/no-thenable': [2],
'unicorn/no-this-assignment': [2],
'unicorn/no-this-outside-of-class': [0], // gitea uses `this` in non-class functions
'unicorn/no-top-level-assignment-in-function': [0],
'unicorn/no-top-level-side-effects': [0],
'unicorn/no-typeof-undefined': [2],
'unicorn/no-uncalled-method': [2],
'unicorn/no-undeclared-class-members': [2],
'unicorn/no-unnecessary-array-flat-depth': [2],
'unicorn/no-unnecessary-array-splice-count': [2],
'unicorn/no-unnecessary-await': [2],
'unicorn/no-unnecessary-boolean-comparison': [2],
'unicorn/no-unnecessary-global-this': [0],
'unicorn/no-unnecessary-nested-ternary': [2],
'unicorn/no-unnecessary-polyfills': [2],
'unicorn/no-unnecessary-slice-end': [2],
'unicorn/no-unnecessary-splice': [2],
'unicorn/no-unreadable-array-destructuring': [0],
'unicorn/no-unreadable-for-of-expression': [0],
'unicorn/no-unreadable-iife': [0],
'unicorn/no-unreadable-new-expression': [0],
'unicorn/no-unreadable-object-destructuring': [0],
'unicorn/no-unsafe-buffer-conversion': [2],
'unicorn/no-unsafe-dom-html': [0],
'unicorn/no-unsafe-property-key': [0],
'unicorn/no-unsafe-string-replacement': [0],
'unicorn/no-unused-array-method-return': [2],
'unicorn/no-unused-properties': [2],
'unicorn/no-useless-boolean-cast': [2],
'unicorn/no-useless-coercion': [2],
'unicorn/no-useless-collection-argument': [2],
'unicorn/no-useless-compound-assignment': [2],
'unicorn/no-useless-concat': [2],
'unicorn/no-useless-continue': [2],
'unicorn/no-useless-delete-check': [2],
'unicorn/no-useless-else': [0],
'unicorn/no-useless-error-capture-stack-trace': [2],
'unicorn/no-useless-fallback-in-spread': [2],
'unicorn/no-useless-iterator-to-array': [2],
'unicorn/no-useless-length-check': [2],
'unicorn/no-useless-logical-operand': [2],
'unicorn/no-useless-override': [2],
'unicorn/no-useless-promise-resolve-reject': [2],
'unicorn/no-useless-recursion': [0],
'unicorn/no-useless-spread': [2],
'unicorn/no-useless-switch-case': [2],
'unicorn/no-useless-template-literals': [2],
'unicorn/no-useless-undefined': [0],
'unicorn/no-xor-as-exponentiation': [2],
'unicorn/no-zero-fractions': [2],
'unicorn/number-literal-case': [0],
'unicorn/numeric-separators-style': [0],
'unicorn/operator-assignment': [2],
'unicorn/prefer-add-event-listener': [2],
'unicorn/prefer-add-event-listener-options': [2],
'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find
'unicorn/prefer-array-flat': [2],
'unicorn/prefer-array-flat-map': [2],
'unicorn/prefer-array-from-async': [2],
'unicorn/prefer-array-from-map': [2],
'unicorn/prefer-array-index-of': [2],
'unicorn/prefer-array-iterable-methods': [2],
'unicorn/prefer-array-last-methods': [2],
'unicorn/prefer-array-slice': [2],
'unicorn/prefer-array-some': [2],
'unicorn/prefer-at': [0],
'unicorn/prefer-await': [2],
'unicorn/prefer-bigint-literals': [2],
'unicorn/prefer-blob-reading-methods': [2],
'unicorn/prefer-boolean-return': [2],
'unicorn/prefer-class-fields': [2],
'unicorn/prefer-classlist-toggle': [2],
'unicorn/prefer-code-point': [0],
'unicorn/prefer-continue': [0],
'unicorn/prefer-date-now': [2],
'unicorn/prefer-default-parameters': [0],
'unicorn/prefer-direct-iteration': [2],
'unicorn/prefer-dispose': [2],
'unicorn/prefer-dom-node-append': [2],
'unicorn/prefer-dom-node-html-methods': [0],
'unicorn/prefer-dom-node-dataset': [0],
'unicorn/prefer-dom-node-remove': [2],
'unicorn/prefer-dom-node-text-content': [2],
'unicorn/prefer-early-return': [0],
'unicorn/prefer-else-if': [2],
'unicorn/prefer-event-target': [2],
'unicorn/prefer-export-from': [0],
'unicorn/prefer-flat-math-min-max': [2],
'unicorn/prefer-get-or-insert-computed': [2],
'unicorn/prefer-global-number-constants': [2],
'unicorn/prefer-global-this': [0],
'unicorn/prefer-has-check': [2],
'unicorn/prefer-hoisting-branch-code': [2],
'unicorn/prefer-https': [0], // false-positives on namespace and schema URIs
'unicorn/prefer-identifier-import-export-specifiers': [2],
'unicorn/prefer-import-meta-properties': [2],
'unicorn/prefer-includes': [0], // handled by @typescript-eslint/prefer-includes
'unicorn/prefer-includes-over-repeated-comparisons': [0], // too opinionated
'unicorn/prefer-iterable-in-constructor': [2],
'unicorn/prefer-iterator-concat': [0], // too opinionated
'unicorn/prefer-iterator-to-array': [2],
'unicorn/prefer-iterator-to-array-at-end': [2],
'unicorn/prefer-json-parse-buffer': [0],
'unicorn/prefer-keyboard-event-key': [2],
'unicorn/prefer-location-assign': [2],
'unicorn/prefer-logical-operator-over-ternary': [2],
'unicorn/prefer-map-from-entries': [0],
'unicorn/prefer-math-abs': [2],
'unicorn/prefer-math-constants': [2],
'unicorn/prefer-math-min-max': [2],
'unicorn/prefer-math-trunc': [2],
'unicorn/prefer-minimal-ternary': [0],
'unicorn/prefer-modern-dom-apis': [0],
'unicorn/prefer-modern-math-apis': [2],
'unicorn/prefer-module': [2],
'unicorn/prefer-native-coercion-functions': [2],
'unicorn/prefer-negative-index': [2],
'unicorn/prefer-node-protocol': [2],
'unicorn/prefer-number-coercion': [0],
'unicorn/prefer-number-is-safe-integer': [0],
'unicorn/prefer-number-properties': [0],
'unicorn/prefer-object-define-properties': [2],
'unicorn/prefer-object-destructuring-defaults': [2],
'unicorn/prefer-object-from-entries': [2],
'unicorn/prefer-object-iterable-methods': [2],
'unicorn/prefer-object-has-own': [0],
'unicorn/prefer-optional-catch-binding': [2],
'unicorn/prefer-path2d': [2],
'unicorn/prefer-private-class-fields': [0],
'unicorn/prefer-promise-with-resolvers': [2],
'unicorn/prefer-prototype-methods': [0],
'unicorn/prefer-query-selector': [2],
'unicorn/prefer-queue-microtask': [2],
'unicorn/prefer-reflect-apply': [0],
'unicorn/prefer-regexp-escape': [0],
'unicorn/prefer-regexp-test': [2],
'unicorn/prefer-response-static-json': [2],
'unicorn/prefer-scoped-selector': [0],
'unicorn/prefer-set-has': [0],
'unicorn/prefer-set-size': [2],
'unicorn/prefer-short-arrow-method': [2],
'unicorn/prefer-simple-condition-first': [0],
'unicorn/prefer-simple-sort-comparator': [2],
'unicorn/prefer-single-array-predicate': [2],
'unicorn/prefer-single-call': [2],
'unicorn/prefer-single-object-destructuring': [2],
'unicorn/prefer-single-replace': [2],
'unicorn/prefer-smaller-scope': [2],
'unicorn/prefer-split-limit': [0], // too opinionated
'unicorn/prefer-spread': [0],
'unicorn/prefer-string-match-all': [2],
'unicorn/prefer-string-pad-start-end': [2],
'unicorn/prefer-string-raw': [0],
'unicorn/prefer-string-repeat': [2],
'unicorn/prefer-string-replace-all': [0],
'unicorn/prefer-string-slice': [0],
'unicorn/prefer-string-starts-ends-with': [0], // handled by @typescript-eslint/prefer-string-starts-ends-with
'unicorn/prefer-string-trim-start-end': [2],
'unicorn/prefer-structured-clone': [2],
'unicorn/prefer-switch': [0],
'unicorn/prefer-temporal': [0],
'unicorn/prefer-ternary': [0],
'unicorn/prefer-top-level-await': [0],
'unicorn/prefer-type-error': [0],
'unicorn/prefer-type-literal-last': [0],
'unicorn/prefer-uint8array-base64': [0],
'unicorn/prefer-unary-minus': [2],
'unicorn/prefer-unicode-code-point-escapes': [0],
'unicorn/prefer-url-can-parse': [2],
'unicorn/prefer-url-href': [2],
'unicorn/prefer-while-loop-condition': [2],
'unicorn/prevent-abbreviations': [0],
'unicorn/relative-url-style': [2],
'unicorn/require-array-join-separator': [2],
'unicorn/require-array-sort-compare': [0],
'unicorn/require-css-escape': [2],
'unicorn/require-module-attributes': [2],
'unicorn/require-module-specifiers': [0],
'unicorn/require-number-to-fixed-digits-argument': [2],
'unicorn/require-passive-events': [2],
'unicorn/require-post-message-target-origin': [0],
'unicorn/require-proxy-trap-boolean-return': [2],
'unicorn/string-content': [0],
'unicorn/switch-case-braces': [0],
'unicorn/switch-case-break-position': [2],
'unicorn/template-indent': [2],
'unicorn/text-encoding-identifier-case': [0],
'unicorn/throw-new-error': [2],
'unicorn/try-complexity': [0],
'use-isnan': [2],
'valid-typeof': [2, {requireStringLiterals: true}],
'vars-on-top': [0],
@@ -1091,7 +956,6 @@ export default defineConfig([
languageOptions: {globals: globals.vitest},
rules: {
'gitea/unescaped-html-literal': [0],
'unicorn/no-error-property-assignment': [0],
'vitest/consistent-test-filename': [0],
'vitest/consistent-test-it': [0],
'vitest/expect-expect': [0],
@@ -1103,6 +967,7 @@ export default defineConfig([
'vitest/no-conditional-in-test': [0],
'vitest/no-conditional-tests': [0],
'vitest/no-disabled-tests': [0],
'vitest/no-done-callback': [0],
'vitest/no-duplicate-hooks': [0],
'vitest/no-focused-tests': [2],
'vitest/no-hooks': [0],
+10 -10
View File
@@ -23,17 +23,17 @@ require (
github.com/ProtonMail/go-crypto v1.4.1
github.com/PuerkitoBio/goquery v1.12.0
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0
github.com/alecthomas/chroma/v2 v2.27.0
github.com/alecthomas/chroma/v2 v2.26.1
github.com/aws/aws-sdk-go-v2/credentials v1.19.24
github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.6.0
github.com/bohde/codel v0.2.0
github.com/buildkite/terminal-to-html/v3 v3.16.8
github.com/caddyserver/certmagic v0.25.4
github.com/caddyserver/certmagic v0.25.3
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635
github.com/chi-middleware/proxy v1.1.1
github.com/dlclark/regexp2/v2 v2.2.2
github.com/dlclark/regexp2/v2 v2.2.1
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
github.com/dustin/go-humanize v1.0.1
github.com/editorconfig/editorconfig-core-go/v2 v2.6.4
@@ -73,7 +73,7 @@ require (
github.com/lib/pq v1.12.3
github.com/markbates/goth v1.82.0
github.com/mattn/go-isatty v0.0.22
github.com/mattn/go-sqlite3 v1.14.47
github.com/mattn/go-sqlite3 v1.14.45
github.com/meilisearch/meilisearch-go v0.36.3
github.com/mholt/archives v0.1.5
github.com/microcosm-cc/bluemonday v1.0.27
@@ -86,7 +86,7 @@ require (
github.com/pquerna/otp v1.5.0
github.com/prometheus/client_golang v1.23.2
github.com/quasoft/websspi v1.1.2
github.com/redis/go-redis/v9 v9.21.0
github.com/redis/go-redis/v9 v9.20.0
github.com/robfig/cron/v3 v3.0.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/sassoftware/go-rpmutils v0.4.0
@@ -96,15 +96,15 @@ require (
github.com/tstranex/u2f v1.0.0
github.com/ulikunitz/xz v0.5.15
github.com/urfave/cli-docs/v3 v3.1.0
github.com/urfave/cli/v3 v3.10.0
github.com/urfave/cli/v3 v3.9.1
github.com/wneessen/go-mail v0.7.3
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.8.2
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0
gitlab.com/gitlab-org/api/client-go/v2 v2.38.0
go.yaml.in/yaml/v4 v4.0.0-rc.5
golang.org/x/crypto v0.53.0
golang.org/x/image v0.43.0
golang.org/x/image v0.42.0
golang.org/x/mod v0.37.0
golang.org/x/net v0.56.0
golang.org/x/oauth2 v0.36.0
@@ -114,7 +114,7 @@ require (
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.11
gopkg.in/ini.v1 v1.67.3
modernc.org/sqlite v1.53.0
modernc.org/sqlite v1.52.0
mvdan.cc/xurls/v2 v2.6.0
strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab
xorm.io/builder v0.3.13
@@ -273,7 +273,7 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.73.4 // indirect
modernc.org/libc v1.73.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
+20 -20
View File
@@ -76,8 +76,8 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0/go.mod h1:I28hc9eaiqK
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@@ -187,8 +187,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/buildkite/terminal-to-html/v3 v3.16.8 h1:QN/daUob6cmK8GcdKnwn9+YTlPr1vNj+oeAIiJK6fPc=
github.com/buildkite/terminal-to-html/v3 v3.16.8/go.mod h1:+k1KVKROZocrTLsEQ9PEf9A+8+X8uaVV5iO1ZIOwKYM=
github.com/caddyserver/certmagic v0.25.4 h1:8eIXh0HC3MsGnNo8One+BCxMGTbe5zb/oz+2KsxBFQg=
github.com/caddyserver/certmagic v0.25.4/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A=
github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
@@ -240,8 +240,8 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.2.2 h1:MYWvNYw8okuqNhwTYO587EZMiDruVa2vhV6fsGpfya0=
github.com/dlclark/regexp2/v2 v2.2.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@@ -514,8 +514,8 @@ github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/a
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4=
github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/meilisearch/meilisearch-go v0.36.3 h1:Yx1aTY5jDgtbStPVkhJTDoLnZTy5sejQSPyjfNMy6e4=
github.com/meilisearch/meilisearch-go v0.36.3/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
@@ -618,8 +618,8 @@ github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4Ul
github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw=
github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk=
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0=
github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/redis/rueidis v1.0.71 h1:pODtnAR5GAB7j4ekhldZ29HKOxe4Hph0GTDGk1ayEQY=
github.com/redis/rueidis v1.0.71/go.mod h1:lfdcZzJ1oKGKL37vh9fO3ymwt+0TdjkkUCJxbgpmcgQ=
github.com/redis/rueidis/rueidiscompat v1.0.71 h1:wNZ//kEjMZgBM0KCk7ncOX8KmAgROU2kDdDNpwheG4w=
@@ -709,8 +709,8 @@ github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
github.com/urfave/cli/v3 v3.10.0 h1:0aU8yOObVDMkM13Cj4G+zb4P0PdeJMec65f81Ak1ioM=
github.com/urfave/cli/v3 v3.10.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.9.1 h1:OLU13atWZ0M+a4xmyBuBNOLZsSRYXyPeMeNjOvgYP54=
github.com/urfave/cli/v3 v3.9.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wneessen/go-mail v0.7.3 h1:g3DravXC5SMlVdboFrQA8Jx95A8sOzoBeS5F+vzNRK0=
github.com/wneessen/go-mail v0.7.3/go.mod h1:QGhBX0yNbc1J+Mkjcu7z2rpj4B4l+BmDY8gYznPC9sk=
@@ -740,8 +740,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0 h1:Bq5YIYgUJVbt4Hbh7ibBwNR4SNEafsyDVhIXl7dXDdg=
gitlab.com/gitlab-org/api/client-go/v2 v2.42.0/go.mod h1:SKUbKSS59KPt6WeGNJoYF8HDaf/rFMUSITlftj/HkLg=
gitlab.com/gitlab-org/api/client-go/v2 v2.38.0 h1:gZSMTTnLcUeY5mH4z3G6GEzbaBTOCUfBCAJXMRyuzEM=
gitlab.com/gitlab-org/api/client-go/v2 v2.38.0/go.mod h1:SKUbKSS59KPt6WeGNJoYF8HDaf/rFMUSITlftj/HkLg=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
@@ -780,8 +780,8 @@ golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -940,8 +940,8 @@ modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
modernc.org/libc v1.73.0 h1:Y/KmTxbIN5T3x+NFjYOzV/+Ha7wKClfIecmTCTuYlqQ=
modernc.org/libc v1.73.0/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -950,8 +950,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+10 -21
View File
@@ -7,7 +7,6 @@ import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"time"
@@ -22,8 +21,6 @@ import (
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
webhook_module "gitea.dev/modules/webhook"
"xorm.io/builder"
)
// ActionRun represents a run of a workflow file
@@ -51,13 +48,6 @@ type ActionRun struct {
Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed
RawConcurrency string // raw concurrency
// WorkflowRepoID/WorkflowCommitSHA record the (repo, commit) the run's workflow file content came from.
// Always filled (repo-level run = the repo itself; scoped run = the source repo).
WorkflowRepoID int64 `xorm:"NOT NULL DEFAULT 0"`
WorkflowCommitSHA string `xorm:"VARCHAR(64) NOT NULL DEFAULT ''"`
IsScopedRun bool `xorm:"NOT NULL DEFAULT false"` // IsScopedRun explicitly classifies scoped runs.
// Started and Stopped are identical to the latest attempt after ActionRunAttempt was introduced.
// When a rerun creates a new latest attempt, they are reset until the new attempt starts and stops.
Started timeutil.TimeStamp
@@ -96,11 +86,7 @@ func (run *ActionRun) WorkflowLink() string {
if run.Repo == nil {
return ""
}
// A scoped run's workflow is disambiguated by its source repo, so carry scoped_workflow_source_repo_id back to the run list
if run.IsScopedRun {
return fmt.Sprintf("%s/actions/?workflow=%s&scoped_workflow_source_repo_id=%d", run.Repo.Link(), url.QueryEscape(run.WorkflowID), run.WorkflowRepoID)
}
return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), url.QueryEscape(run.WorkflowID))
return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), run.WorkflowID)
}
// RefLink return the url of run's ref
@@ -278,7 +264,11 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er
}
func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionRun, error) {
run, has, err := db.Get[ActionRun](ctx, builder.Eq{"repo_id": repoID, "`index`": runIndex})
run := &ActionRun{
RepoID: repoID,
Index: runIndex,
}
has, err := db.GetEngine(ctx).Get(run)
if err != nil {
return nil, err
} else if !has {
@@ -289,7 +279,9 @@ func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionR
}
func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) {
run := &ActionRun{}
run := &ActionRun{
RepoID: repoID,
}
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).Desc("index").Get(run)
if err != nil {
return nil, err
@@ -303,10 +295,7 @@ func GetWorkflowLatestRun(ctx context.Context, repoID int64, workflowFile, branc
var run ActionRun
q := db.GetEngine(ctx).Where("repo_id=?", repoID).
And("ref = ?", branch).
And("workflow_id = ?", workflowFile).
// TODO: the badge only reflects the repo's own (repo-level) runs; a same-named scoped run must not leak in.
// Support a scoped-workflow badge later by making this source-aware.
And("is_scoped_run = ?", false)
And("workflow_id = ?", workflowFile)
if event != "" {
q.And("event = ?", event)
}
+10 -48
View File
@@ -108,10 +108,6 @@ type ActionRunJob struct {
// ParentJobID scopes `Needs` resolution: name lookups happen only among rows sharing the same ParentJobID. 0 for top-level rows.
ParentJobID int64 `xorm:"index NOT NULL DEFAULT 0"`
// ContinueOnError mirrors the job-level continue-on-error field from the workflow YAML.
// When true, a failure of this job does not fail the overall workflow run.
ContinueOnError bool `xorm:"NOT NULL DEFAULT FALSE"`
Started timeutil.TimeStamp
Stopped timeutil.TimeStamp
Created timeutil.TimeStamp `xorm:"created"`
@@ -361,14 +357,6 @@ func CollectAllDescendantJobs(parent *ActionRunJob, allJobs []*ActionRunJob) []*
return out
}
// hasWaitingJobsToPick reports whether any waiting, unclaimed, non-reusable job
// remains in the repo, i.e. work that an idle runner could still pick up.
func hasWaitingJobsToPick(ctx context.Context, repoID int64) (bool, error) {
return db.GetEngine(ctx).
Where("repo_id = ? AND task_id = ? AND status = ? AND is_reusable_caller = ?", repoID, 0, StatusWaiting, false).
Exist(&ActionRunJob{})
}
func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, cols ...string) (int64, error) {
e := db.GetEngine(ctx)
@@ -393,41 +381,18 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
return affected, nil
}
if job.RunID == 0 {
var err error
if job, err = GetRunJobByRepoAndID(ctx, job.RepoID, job.ID); err != nil {
// Reusable workflow caller jobs are never picked up by runners, so they don't need a task-version bump.
if statusUpdated && job.Status.IsWaiting() && !job.IsReusableCaller {
// if the status of job changes to waiting again, increase tasks version.
if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
return 0, err
}
}
// Reusable workflow caller jobs are never picked up by runners, so they don't need a task-version bump.
if statusUpdated && !job.IsReusableCaller {
switch {
case job.Status.IsWaiting():
// A job returning to the waiting queue is work a runner can pick up, so bump the
// version to wake idle runners whose tasksVersion already equals latestVersion.
if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
return 0, err
}
case job.Status.IsDone():
// When a job finishes, bump the version so that idle runners — whose
// tasksVersion already equals the current latestVersion — learn that
// remaining waiting jobs are still available and attempt PickTask again.
// Without this bump, runners that completed their tasks would see
// tasksVersion==latestVersion and skip PickTask, leaving the other jobs
// permanently unassigned until the version changes for another reason.
// Only bump when waiting work actually remains for this repo, otherwise
// every job completion would needlessly bump the global version and wake
// every idle runner instance-wide for nothing.
hasWaiting, err := hasWaitingJobsToPick(ctx, job.RepoID)
if err != nil {
return 0, err
}
if hasWaiting {
if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
return 0, err
}
}
if job.RunID == 0 {
var err error
if job, err = GetRunJobByRepoAndID(ctx, job.RepoID, job.ID); err != nil {
return 0, err
}
}
@@ -535,12 +500,9 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
allSkipped := len(jobs) != 0
var hasFailure, hasCancelled, hasCancelling, hasWaiting, hasRunning, hasBlocked bool
for _, job := range jobs {
// A failed job with continue-on-error:true does not fail the workflow run.
// It counts as a "continued failure" and is treated like success for aggregation.
isContinuedFailure := job.ContinueOnError && job.Status == StatusFailure
allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped || isContinuedFailure)
allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped)
allSkipped = allSkipped && job.Status == StatusSkipped
hasFailure = hasFailure || (job.Status == StatusFailure && !job.ContinueOnError)
hasFailure = hasFailure || job.Status == StatusFailure
hasCancelled = hasCancelled || job.Status == StatusCancelled
hasCancelling = hasCancelling || job.Status == StatusCancelling
hasWaiting = hasWaiting || job.Status == StatusWaiting
+2 -31
View File
@@ -10,7 +10,6 @@ import (
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/optional"
"gitea.dev/modules/translation"
webhook_module "gitea.dev/modules/webhook"
@@ -62,9 +61,7 @@ type FindRunOptions struct {
RepoID int64
OwnerID int64
WorkflowID string
WorkflowRepoID int64 // source-aware filter: the repo a run's workflow content came from (0 = any)
IsScopedRun optional.Option[bool] // is the run from a scoped workflow
Ref string // the commit/tag/… that caused this workflow
Ref string // the commit/tag/… that caused this workflow
TriggerUserID int64
TriggerEvent webhook_module.HookEventType
Status []Status
@@ -80,12 +77,6 @@ func (opts FindRunOptions) ToConds() builder.Cond {
if opts.WorkflowID != "" {
cond = cond.And(builder.Eq{"`action_run`.workflow_id": opts.WorkflowID})
}
if opts.WorkflowRepoID > 0 {
cond = cond.And(builder.Eq{"`action_run`.workflow_repo_id": opts.WorkflowRepoID})
}
if opts.IsScopedRun.Has() {
cond = cond.And(builder.Eq{"`action_run`.is_scoped_run": opts.IsScopedRun.Value()})
}
if opts.TriggerUserID > 0 {
cond = cond.And(builder.Eq{"`action_run`.trigger_user_id": opts.TriggerUserID})
}
@@ -115,15 +106,6 @@ func (opts FindRunOptions) ToJoins() []db.JoinFunc {
}
func (opts FindRunOptions) ToOrders() string {
// When scoped to a repo, sort by `index`: it reuses the unique
// `repo_index` (repo_id, index) index, so the query seeks repo_id and
// walks index descending instead of filesorting all matching rows.
// Within a repo `index` is co-monotonic with `id`, so the order is the same.
if opts.RepoID > 0 {
return "`action_run`.`index` DESC"
}
// `index` is scoped per repo, so it is meaningless across repos. With no
// RepoID, sort by the global, PK-indexed `id` for a deterministic order.
return "`action_run`.`id` DESC"
}
@@ -165,20 +147,9 @@ func GetRunBranches(ctx context.Context, repoID int64) ([]string, error) {
// GetRunWorkflowIDs returns all distinct WorkflowIDs that have at least
// one ActionRun in the given repo.
func GetRunWorkflowIDs(ctx context.Context, repoID int64) ([]string, error) {
return getRunWorkflowIDs(ctx, repoID, builder.NewCond())
}
// GetRepoRunWorkflowIDs returns all distinct WorkflowIDs that have at least
// one repo-level ActionRun in the given repo.
func GetRepoRunWorkflowIDs(ctx context.Context, repoID int64) ([]string, error) {
return getRunWorkflowIDs(ctx, repoID, builder.Eq{"is_scoped_run": false})
}
func getRunWorkflowIDs(ctx context.Context, repoID int64, extraCond builder.Cond) ([]string, error) {
ids := make([]string, 0, 10)
cond := builder.Eq{"repo_id": repoID}
return ids, db.GetEngine(ctx).Table("action_run").
Where(cond.And(extraCond)).
Where(builder.Eq{"repo_id": repoID}).
Distinct("workflow_id").
Cols("workflow_id").
Asc("workflow_id").
-125
View File
@@ -6,13 +6,10 @@ package actions
import (
"testing"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/modules/optional"
"gitea.dev/modules/translation"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetRunWorkflowIDs(t *testing.T) {
@@ -27,46 +24,6 @@ func TestGetRunWorkflowIDs(t *testing.T) {
assert.Empty(t, ids)
}
func TestGetRepoRunWorkflowIDs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const (
repoID = int64(4)
repoWorkflowID = "repo-orphan.yaml"
scopedWorkflowID = "scoped-only.yaml"
sharedWorkflowID = "shared-name.yaml"
scopedWorkflowRepo = int64(111)
)
for _, spec := range []struct {
id int64
workflowID string
workflowRepoID int64
isScopedRun bool
}{
{99811, repoWorkflowID, repoID, false},
{99812, scopedWorkflowID, scopedWorkflowRepo, true},
{99813, sharedWorkflowID, repoID, false},
{99814, sharedWorkflowID, scopedWorkflowRepo, true},
} {
require.NoError(t, db.Insert(t.Context(), &ActionRun{
ID: spec.id,
Index: spec.id,
RepoID: repoID,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: spec.workflowID,
WorkflowRepoID: spec.workflowRepoID,
IsScopedRun: spec.isScopedRun,
}))
}
ids, err := GetRepoRunWorkflowIDs(t.Context(), repoID)
require.NoError(t, err)
assert.Contains(t, ids, repoWorkflowID)
assert.Contains(t, ids, sharedWorkflowID)
assert.NotContains(t, ids, scopedWorkflowID)
}
func TestGetStatusInfoList(t *testing.T) {
statusInfoList := GetStatusInfoList(t.Context(), translation.MockLocale{})
@@ -78,85 +35,3 @@ func TestGetStatusInfoList(t *testing.T) {
{Status: int(StatusCancelling), StatusName: StatusCancelling.String(), DisplayedStatus: "actions.status.cancelling"},
}, statusInfoList)
}
// TestFindRunOptions_WorkflowRepoID: two runs share the bare WorkflowID but come from different content-source repos;
// the source-aware WorkflowRepoID filter must separate them.
func TestFindRunOptions_WorkflowRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const (
repoID = int64(4)
sourceA = int64(111)
sourceB = int64(222)
workflowID = "u3-shared.yaml"
)
for _, spec := range []struct{ id, workflowRepoID int64 }{
{99801, sourceA},
{99802, sourceB},
} {
require.NoError(t, db.Insert(t.Context(), &ActionRun{
ID: spec.id,
Index: spec.id,
RepoID: repoID,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: workflowID,
WorkflowRepoID: spec.workflowRepoID,
IsScopedRun: true,
}))
}
// no source filter -> both
all, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID})
require.NoError(t, err)
assert.Len(t, all, 2)
// filter by source A -> only the run whose content came from A
onlyA, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, WorkflowRepoID: sourceA})
require.NoError(t, err)
require.Len(t, onlyA, 1)
assert.EqualValues(t, 99801, onlyA[0].ID)
// filter by source B -> only the run whose content came from B
onlyB, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, WorkflowRepoID: sourceB})
require.NoError(t, err)
require.Len(t, onlyB, 1)
assert.EqualValues(t, 99802, onlyB[0].ID)
}
func TestFindRunOptions_IsScopedRun(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const (
repoID = int64(4)
workflowID = "scoped-flag.yaml"
)
for _, spec := range []struct {
id int64
scoped bool
}{
{99821, false},
{99822, true},
} {
require.NoError(t, db.Insert(t.Context(), &ActionRun{
ID: spec.id,
Index: spec.id,
RepoID: repoID,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: workflowID,
WorkflowRepoID: repoID,
IsScopedRun: spec.scoped,
}))
}
repoLevel, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, IsScopedRun: optional.Some(false)})
require.NoError(t, err)
require.Len(t, repoLevel, 1)
assert.EqualValues(t, 99821, repoLevel[0].ID)
scoped, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, IsScopedRun: optional.Some(true)})
require.NoError(t, err)
require.Len(t, scoped, 1)
assert.EqualValues(t, 99822, scoped[0].ID)
}
-55
View File
@@ -13,7 +13,6 @@ import (
"gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUpdateRepoRunsNumbers(t *testing.T) {
@@ -45,57 +44,3 @@ func TestActionRun_Duration_NonNegative(t *testing.T) {
}
assert.Equal(t, time.Duration(0), run.Duration())
}
func TestActionRun_WorkflowLink(t *testing.T) {
repo := &repo_model.Repository{OwnerName: "org", Name: "consumer"}
// a repo-level run links by file name only
repoLevel := &ActionRun{Repo: repo, WorkflowID: "ci.yaml", WorkflowRepoID: repo.ID}
assert.Equal(t, repo.Link()+"/actions/?workflow=ci.yaml", repoLevel.WorkflowLink())
// a scoped run carries its source repo id back, so the list stays filtered to that source
scoped := &ActionRun{Repo: repo, WorkflowID: "ci.yaml", WorkflowRepoID: 42, IsScopedRun: true}
assert.Equal(t, repo.Link()+"/actions/?workflow=ci.yaml&scoped_workflow_source_repo_id=42", scoped.WorkflowLink())
}
func TestGetWorkflowLatestRun_RepoLevelOnly(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const (
repoID = int64(4)
workflowID = "badge-source-aware.yaml"
ref = "refs/heads/main"
)
require.NoError(t, db.Insert(t.Context(), &ActionRun{
ID: 99811,
Index: 99811,
RepoID: repoID,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: workflowID,
Ref: ref,
Event: "push",
Status: StatusSuccess,
WorkflowRepoID: repoID,
WorkflowCommitSHA: "repo-level-sha",
}))
require.NoError(t, db.Insert(t.Context(), &ActionRun{
ID: 99812,
Index: 99812,
RepoID: repoID,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: workflowID,
Ref: ref,
Event: "push",
Status: StatusFailure,
WorkflowRepoID: 111,
WorkflowCommitSHA: "scoped-sha",
IsScopedRun: true,
}))
run, err := GetWorkflowLatestRun(t.Context(), repoID, workflowID, ref, "push")
require.NoError(t, err)
assert.EqualValues(t, 99811, run.ID)
assert.False(t, run.IsScopedRun)
}
-179
View File
@@ -1,179 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"fmt"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ActionScopedWorkflowSource registers a repository as a source of scoped workflows, either for an owner (user/org) or for the whole instance.
type ActionScopedWorkflowSource struct {
ID int64 `xorm:"pk autoincr"`
// OwnerID is the scope the source applies to: a user/org ID (applies to that owner's repos), or 0 for instance-level (applies to every repo).
OwnerID int64 `xorm:"UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
// SourceRepoID is the source repository providing the workflow files; always non-zero.
SourceRepoID int64 `xorm:"INDEX UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
// WorkflowConfigs maps a workflow ID (entry name) to its merge-gate config.
WorkflowConfigs map[string]*ScopedWorkflowConfig `xorm:"JSON TEXT 'workflow_configs'"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
// ScopedWorkflowConfig is one scoped workflow's config within a source registration.
type ScopedWorkflowConfig struct {
Required bool `json:"required"`
Patterns []string `json:"patterns"` // the status-check patterns that must be present and pass, only effective when Required is true
}
func init() {
db.RegisterModel(new(ActionScopedWorkflowSource))
}
// IsWorkflowRequired reports whether the given workflow ID (entry name) is marked required in this source.
func (s *ActionScopedWorkflowSource) IsWorkflowRequired(workflowID string) bool {
c, ok := s.WorkflowConfigs[workflowID]
return ok && c.Required
}
type FindScopedWorkflowSourceOpts struct {
db.ListOptions
OwnerIDs []int64
SourceRepoID int64
}
func (opts FindScopedWorkflowSourceOpts) ToConds() builder.Cond {
cond := builder.NewCond()
if len(opts.OwnerIDs) > 0 {
cond = cond.And(builder.In("owner_id", opts.OwnerIDs))
}
if opts.SourceRepoID != 0 {
cond = cond.And(builder.Eq{"source_repo_id": opts.SourceRepoID})
}
return cond
}
// GetEffectiveScopedWorkflowSources returns the scoped-workflow sources effective for a repo owned by repoOwnerID:
// the owner's own sources plus instance-level (owner_id=0) sources.
func GetEffectiveScopedWorkflowSources(ctx context.Context, repoOwnerID int64) ([]*ActionScopedWorkflowSource, error) {
owners := []int64{0}
if repoOwnerID != 0 {
owners = append(owners, repoOwnerID)
}
return db.Find[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: owners})
}
// IsScopedWorkflowSourceEffective reports whether sourceRepoID is a scoped-workflow source effective for a repo owned by repoOwnerID.
func IsScopedWorkflowSourceEffective(ctx context.Context, repoOwnerID, sourceRepoID int64) (bool, error) {
owners := []int64{0}
if repoOwnerID != 0 {
owners = append(owners, repoOwnerID)
}
return db.Exist[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: owners, SourceRepoID: sourceRepoID}.ToConds())
}
// IsWorkflowRequiredInSources reports whether workflowID from sourceRepoID is required by any of the given sources.
func IsWorkflowRequiredInSources(sources []*ActionScopedWorkflowSource, sourceRepoID int64, workflowID string) bool {
for _, s := range sources {
if s.SourceRepoID == sourceRepoID && s.IsWorkflowRequired(workflowID) {
return true
}
}
return false
}
// ScopedStatusContextPrefix returns the source-repo prefix that makes a scoped run's commit-status context distinct from same-named workflows.
func ScopedStatusContextPrefix(ctx context.Context, sourceRepoID int64) string {
if sourceRepo, err := repo_model.GetRepositoryByID(ctx, sourceRepoID); err == nil {
return sourceRepo.FullName()
}
return fmt.Sprintf("scoped:%d", sourceRepoID)
}
// IsScopedWorkflowRequired reports whether workflowID from sourceRepoID is required for a repo owned by consumerOwnerID.
func IsScopedWorkflowRequired(ctx context.Context, consumerOwnerID, sourceRepoID int64, workflowID string) (bool, error) {
sources, err := GetEffectiveScopedWorkflowSources(ctx, consumerOwnerID)
if err != nil {
return false, err
}
return IsWorkflowRequiredInSources(sources, sourceRepoID, workflowID), nil
}
// IsScopedWorkflowOptedOutloads the consumer's effective sources then calls ScopedWorkflowOptedOut
func IsScopedWorkflowOptedOut(ctx context.Context, cfg *repo_model.ActionsConfig, consumerOwnerID, sourceRepoID int64, workflowID string) (bool, error) {
if !cfg.IsScopedWorkflowDisabled(sourceRepoID, workflowID) {
return false, nil
}
sources, err := GetEffectiveScopedWorkflowSources(ctx, consumerOwnerID)
if err != nil {
return false, err
}
return ScopedWorkflowOptedOut(cfg, sources, sourceRepoID, workflowID), nil
}
// ScopedWorkflowOptedOut reports whether a consumer's opt-out of (sourceRepoID, workflowID) is in effect.
func ScopedWorkflowOptedOut(cfg *repo_model.ActionsConfig, sources []*ActionScopedWorkflowSource, sourceRepoID int64, workflowID string) bool {
return !IsWorkflowRequiredInSources(sources, sourceRepoID, workflowID) && cfg.IsScopedWorkflowDisabled(sourceRepoID, workflowID)
}
// GetScopedWorkflowSourcesByOwner returns the sources an owner (user/org, or 0 for instance) registered.
func GetScopedWorkflowSourcesByOwner(ctx context.Context, ownerID int64) ([]*ActionScopedWorkflowSource, error) {
return db.Find[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: []int64{ownerID}})
}
// GetScopedWorkflowSource returns the (owner, repo) source registration or a NotExist error.
func GetScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) (*ActionScopedWorkflowSource, error) {
src := &ActionScopedWorkflowSource{}
has, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Get(src)
if err != nil {
return nil, err
}
if !has {
return nil, util.NewNotExistErrorf("scoped workflow source (owner %d, repo %d) does not exist", ownerID, repoID)
}
return src, nil
}
// AddScopedWorkflowSource registers repoID as a source for ownerID (no-op if already registered).
func AddScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) error {
exists, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Exist(new(ActionScopedWorkflowSource))
if err != nil {
return err
}
if exists {
return nil
}
if err := db.Insert(ctx, &ActionScopedWorkflowSource{OwnerID: ownerID, SourceRepoID: repoID}); err != nil {
// Re-check and treat an already-present row as the intended no-op.
if exists, existErr := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Exist(new(ActionScopedWorkflowSource)); existErr == nil && exists {
return nil
}
return err
}
return nil
}
// SetScopedWorkflowSourceConfigs replaces the per-workflow merge-gate configs (workflow ID -> config).
func SetScopedWorkflowSourceConfigs(ctx context.Context, ownerID, repoID int64, configs map[string]*ScopedWorkflowConfig) error {
_, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).
Cols("workflow_configs").
Update(&ActionScopedWorkflowSource{WorkflowConfigs: configs})
return err
}
// RemoveScopedWorkflowSource removes the (owner, repo) source registration.
func RemoveScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) error {
_, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Delete(new(ActionScopedWorkflowSource))
return err
}
-139
View File
@@ -1,139 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestScopedWorkflowSource_IsWorkflowRequired(t *testing.T) {
src := &ActionScopedWorkflowSource{WorkflowConfigs: map[string]*ScopedWorkflowConfig{
"a.yml": {Required: true, Patterns: []string{"p"}},
"b.yml": {Required: true, Patterns: []string{"p"}},
"c.yml": {Required: false, Patterns: []string{"p"}}, // patterns kept as history, not required
}}
assert.True(t, src.IsWorkflowRequired("a.yml"))
assert.True(t, src.IsWorkflowRequired("b.yml"))
assert.False(t, src.IsWorkflowRequired("c.yml"), "config kept as history but not required")
assert.False(t, src.IsWorkflowRequired("d.yml"))
empty := &ActionScopedWorkflowSource{}
assert.False(t, empty.IsWorkflowRequired("a.yml"))
}
func TestIsWorkflowRequiredInSources(t *testing.T) {
// repo 100 registered twice (org optional + instance required).
sources := []*ActionScopedWorkflowSource{
{OwnerID: 2, SourceRepoID: 100, WorkflowConfigs: nil},
{OwnerID: 0, SourceRepoID: 100, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"a.yml": {Required: true, Patterns: []string{"p"}}}},
{OwnerID: 0, SourceRepoID: 200, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"b.yml": {Required: true, Patterns: []string{"p"}}}},
}
assert.True(t, IsWorkflowRequiredInSources(sources, 100, "a.yml"), "required at instance level wins over org optional")
assert.False(t, IsWorkflowRequiredInSources(sources, 100, "z.yml"))
assert.False(t, IsWorkflowRequiredInSources(sources, 200, "a.yml"), "a.yml is required for repo 100, not repo 200")
assert.True(t, IsWorkflowRequiredInSources(sources, 200, "b.yml"))
assert.False(t, IsWorkflowRequiredInSources(sources, 999, "a.yml"), "unknown source repo")
}
func TestGetEffectiveScopedWorkflowSources(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
rows := []*ActionScopedWorkflowSource{
{OwnerID: 2, SourceRepoID: 100, WorkflowConfigs: nil}, // org 2 registers repo 100 (optional)
{OwnerID: 0, SourceRepoID: 100, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"a.yml": {Required: true, Patterns: []string{"p"}}}}, // instance also registers repo 100 (required)
{OwnerID: 0, SourceRepoID: 200, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"b.yml": {Required: true, Patterns: []string{"p"}}}}, // instance source 200
{OwnerID: 3, SourceRepoID: 300, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"c.yml": {Required: true, Patterns: []string{"p"}}}}, // a different owner's source
}
for _, r := range rows {
require.NoError(t, db.Insert(ctx, r))
}
// owner 2 sees its own sources plus instance-level ones, but not owner 3's.
owner2, err := GetEffectiveScopedWorkflowSources(ctx, 2)
require.NoError(t, err)
assert.Len(t, owner2, 3)
required, err := IsScopedWorkflowRequired(ctx, 2, 100, "a.yml")
require.NoError(t, err)
assert.True(t, required, "instance marks a.yml required → required for owner 2 even though org left it optional")
required, err = IsScopedWorkflowRequired(ctx, 2, 100, "x.yml")
require.NoError(t, err)
assert.False(t, required)
required, err = IsScopedWorkflowRequired(ctx, 2, 200, "b.yml")
require.NoError(t, err)
assert.True(t, required)
// owner 3's source must not be effective for owner 2.
required, err = IsScopedWorkflowRequired(ctx, 2, 300, "c.yml")
require.NoError(t, err)
assert.False(t, required)
// IsScopedWorkflowSourceEffective: owner-level and instance-level sources are effective; another owner's is not.
effective, err := IsScopedWorkflowSourceEffective(ctx, 2, 100)
require.NoError(t, err)
assert.True(t, effective, "owner 2's own source")
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 200)
require.NoError(t, err)
assert.True(t, effective, "instance-level source is effective for any owner")
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 300)
require.NoError(t, err)
assert.False(t, effective, "owner 3's source is not effective for owner 2")
effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 999)
require.NoError(t, err)
assert.False(t, effective, "unknown source repo")
effective, err = IsScopedWorkflowSourceEffective(ctx, 3, 300)
require.NoError(t, err)
assert.True(t, effective, "owner 3's own source is effective for owner 3")
}
func TestScopedWorkflowSourceCRUD(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
// add is idempotent
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 10))
require.NoError(t, AddScopedWorkflowSource(ctx, 5, 10))
sources, err := GetScopedWorkflowSourcesByOwner(ctx, 5)
require.NoError(t, err)
assert.Len(t, sources, 1)
// set the per-workflow configs (entry name -> {required, patterns}); a.yml required, b.yml kept as history (not required)
configs := map[string]*ScopedWorkflowConfig{
"a.yml": {Required: true, Patterns: []string{"src: a.yml / *"}},
"b.yml": {Required: false, Patterns: []string{"src: b.yml / build (push)"}},
}
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 10, configs))
src, err := GetScopedWorkflowSource(ctx, 5, 10)
require.NoError(t, err)
assert.Equal(t, configs, src.WorkflowConfigs)
// clearing the configs works
require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 10, nil))
src, err = GetScopedWorkflowSource(ctx, 5, 10)
require.NoError(t, err)
assert.Empty(t, src.WorkflowConfigs)
// remove
require.NoError(t, RemoveScopedWorkflowSource(ctx, 5, 10))
_, err = GetScopedWorkflowSource(ctx, 5, 10)
assert.ErrorIs(t, err, util.ErrNotExist)
sources, err = GetScopedWorkflowSourcesByOwner(ctx, 5)
require.NoError(t, err)
assert.Empty(t, sources)
}
-54
View File
@@ -48,57 +48,3 @@ func TestStatusFromResult(t *testing.T) {
assert.Equal(t, tt.want, StatusFromResult(tt.result), "result=%s", tt.result)
}
}
func newJob(status Status, continueOnError bool) *ActionRunJob {
return &ActionRunJob{Status: status, ContinueOnError: continueOnError}
}
func TestAggregateJobStatusContinueOnError(t *testing.T) {
cases := []struct {
name string
jobs []*ActionRunJob
want Status
}{
{
name: "all success",
jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusSuccess, false)},
want: StatusSuccess,
},
{
name: "one failure without continue-on-error",
jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusFailure, false)},
want: StatusFailure,
},
{
name: "one failure with continue-on-error",
jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusFailure, true)},
want: StatusSuccess,
},
{
name: "only continued-failure",
jobs: []*ActionRunJob{newJob(StatusFailure, true)},
want: StatusSuccess,
},
{
name: "continued-failure plus real failure",
jobs: []*ActionRunJob{newJob(StatusFailure, true), newJob(StatusFailure, false)},
want: StatusFailure,
},
{
name: "all skipped",
jobs: []*ActionRunJob{newJob(StatusSkipped, false), newJob(StatusSkipped, false)},
want: StatusSkipped,
},
{
name: "continued-failure plus skipped counts as success",
jobs: []*ActionRunJob{newJob(StatusFailure, true), newJob(StatusSkipped, false)},
want: StatusSuccess,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, AggregateJobStatus(tt.jobs))
})
}
}
+68 -134
View File
@@ -227,18 +227,13 @@ func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) {
return util.EllipsisDisplayString(name, limit) // database column has a length limit
}
// errJobAlreadyClaimed is a sentinel used inside claimJobForRunner to signal that
// another runner won the optimistic-lock race; it is never returned to callers.
var errJobAlreadyClaimed = errors.New("job already claimed by another runner")
// CreateTaskForRunner finds a waiting job that matches the runner's labels and
// atomically claims it. It iterates through all matching jobs so that a
// concurrent claim by another runner (which would lose the optimistic lock on
// job #1) does not leave the remaining jobs permanently unassigned.
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
if db.InTransaction(ctx) {
return nil, false, errors.New("CreateTaskForRunner must not be called within a database transaction")
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return nil, false, err
}
defer committer.Close()
e := db.GetEngine(ctx)
jobCond := builder.NewCond()
@@ -259,144 +254,83 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
}
// TODO: a more efficient way to filter labels
var job *ActionRunJob
log.Trace("runner labels: %v", runner.AgentLabels)
for _, v := range jobs {
if !runner.CanMatchLabels(v.RunsOn) {
continue
if runner.CanMatchLabels(v.RunsOn) {
job = v
break
}
task, ok, err := claimJobForRunner(ctx, runner, v)
if err != nil {
return nil, false, err
}
if ok {
return task, true, nil
}
// Another runner claimed this job concurrently; try the next one.
}
return nil, false, nil
}
// claimJobForRunner attempts to atomically claim job for runner inside its own
// transaction. Returns (task, true, nil) on success, or (nil, false, nil) when
// another runner wins the optimistic-lock race (the caller should try the next
// candidate job).
func claimJobForRunner(ctx context.Context, runner *ActionRunner, job *ActionRunJob) (*ActionTask, bool, error) {
var resultTask *ActionTask
err := db.WithTx(ctx, func(ctx context.Context) error {
e := db.GetEngine(ctx)
if err := job.LoadAttributes(ctx); err != nil {
return err
}
now := timeutil.TimeStampNow()
job.Started = now
job.Status = StatusRunning
task := &ActionTask{
JobID: job.ID,
Attempt: job.Attempt,
RunnerID: runner.ID,
Started: now,
Status: StatusRunning,
RepoID: job.RepoID,
OwnerID: job.OwnerID,
CommitSHA: job.CommitSHA,
IsForkPullRequest: job.IsForkPullRequest,
}
task.GenerateAndFillToken()
workflowJob, err := job.ParseJob()
if err != nil {
return fmt.Errorf("load job %d: %w", job.ID, err)
}
if _, err := e.Insert(task); err != nil {
return err
}
task.LogFilename = logFileName(job.Run.Repo.FullName(), task.ID)
if err := UpdateTask(ctx, task, "log_filename"); err != nil {
return err
}
if len(workflowJob.Steps) > 0 {
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
for i, v := range workflowJob.Steps {
steps[i] = &ActionTaskStep{
Name: makeTaskStepDisplayName(v, 255),
TaskID: task.ID,
Index: int64(i),
RepoID: task.RepoID,
Status: StatusWaiting,
}
}
if _, err := e.Insert(steps); err != nil {
return err
}
task.Steps = steps
}
job.TaskID = task.ID
n, err := UpdateRunJob(ctx, job, builder.And(builder.Eq{"task_id": 0}, builder.Eq{"status": StatusWaiting}))
if err != nil {
return err
}
if n != 1 {
// Another runner claimed this job between our scan and this update;
// signal the outer loop to move on without treating this as an error.
return errJobAlreadyClaimed
}
task.Job = job
resultTask = task
return nil
})
if errors.Is(err, errJobAlreadyClaimed) {
if job == nil {
return nil, false, nil
}
if err != nil {
if err := job.LoadAttributes(ctx); err != nil {
return nil, false, err
}
return resultTask, true, nil
}
// ReleaseTaskForRunner reverts a freshly-claimed but undelivered task: it deletes
// the task together with its steps and returns the job to the waiting queue. It is
// used when assembling the runner response fails after the job was already claimed,
// so the job is not stranded in running state with no runner ever executing it.
func ReleaseTaskForRunner(ctx context.Context, task *ActionTask) error {
return db.WithTx(ctx, func(ctx context.Context) error {
e := db.GetEngine(ctx)
now := timeutil.TimeStampNow()
job.Started = now
job.Status = StatusRunning
job, err := GetRunJobByRepoAndID(ctx, task.RepoID, task.JobID)
if err != nil {
return err
}
task := &ActionTask{
JobID: job.ID,
Attempt: job.Attempt,
RunnerID: runner.ID,
Started: now,
Status: StatusRunning,
RepoID: job.RepoID,
OwnerID: job.OwnerID,
CommitSHA: job.CommitSHA,
IsForkPullRequest: job.IsForkPullRequest,
}
task.GenerateAndFillToken()
job.Status = StatusWaiting
job.Started = 0
job.TaskID = 0
// Guard on task_id and status so we only release while the job still
// references this task and has not progressed past running.
n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": task.ID, "status": StatusRunning}, "status", "started", "task_id")
if err != nil {
return err
}
if n != 1 {
return fmt.Errorf("release task %d: job %d no longer references it", task.ID, task.JobID)
}
workflowJob, err := job.ParseJob()
if err != nil {
return nil, false, fmt.Errorf("load job %d: %w", job.ID, err)
}
if _, err := e.Delete(&ActionTaskStep{TaskID: task.ID}); err != nil {
return err
if _, err := e.Insert(task); err != nil {
return nil, false, err
}
task.LogFilename = logFileName(job.Run.Repo.FullName(), task.ID)
if err := UpdateTask(ctx, task, "log_filename"); err != nil {
return nil, false, err
}
if len(workflowJob.Steps) > 0 {
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
for i, v := range workflowJob.Steps {
steps[i] = &ActionTaskStep{
Name: makeTaskStepDisplayName(v, 255),
TaskID: task.ID,
Index: int64(i),
RepoID: task.RepoID,
Status: StatusWaiting,
}
}
if _, err := e.ID(task.ID).Delete(&ActionTask{}); err != nil {
return err
if _, err := e.Insert(steps); err != nil {
return nil, false, err
}
return nil
})
task.Steps = steps
}
job.TaskID = task.ID
if n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}); err != nil {
return nil, false, err
} else if n != 1 {
return nil, false, nil
}
task.Job = job
if err := committer.Commit(); err != nil {
return nil, false, err
}
return task, true, nil
}
func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
-65
View File
@@ -306,68 +306,3 @@ func TestStopTaskCancellingFallsBackForMissingRunner(t *testing.T) {
assert.Equal(t, StatusCancelled, jobAfterStop.Status)
assert.NotZero(t, jobAfterStop.Stopped)
}
// TestReleaseTaskForRunner verifies that releasing a freshly-claimed task returns
// its job to the waiting queue and deletes the task and its steps, so a failure
// while assembling the runner response cannot strand the job in running state.
func TestReleaseTaskForRunner(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
run := &ActionRun{
Title: "release-task-test-run",
RepoID: 1,
OwnerID: 2,
WorkflowID: "test.yaml",
Index: 9902,
TriggerUserID: 2,
Ref: "refs/heads/main",
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
Event: "push",
TriggerEvent: "push",
Status: StatusWaiting,
}
require.NoError(t, db.Insert(t.Context(), run))
job := &ActionRunJob{
RunID: run.ID,
RepoID: run.RepoID,
OwnerID: run.OwnerID,
CommitSHA: run.CommitSHA,
Name: "release-job",
Attempt: 1,
JobID: "release-job",
Status: StatusWaiting,
RunsOn: []string{"ubuntu-latest"},
WorkflowPayload: []byte("on: push\njobs:\n release-job:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"),
}
require.NoError(t, db.Insert(t.Context(), job))
runner := &ActionRunner{
UUID: "release-runner-uuid",
Name: "release-runner",
AgentLabels: []string{"ubuntu-latest"},
}
runner.GenerateAndFillToken()
require.NoError(t, db.Insert(t.Context(), runner))
task, ok, err := CreateTaskForRunner(t.Context(), runner)
require.NoError(t, err)
require.True(t, ok)
require.NotNil(t, task)
claimed := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
require.Equal(t, StatusRunning, claimed.Status)
require.Equal(t, task.ID, claimed.TaskID)
require.NoError(t, ReleaseTaskForRunner(t.Context(), task))
// Job is back in the waiting queue with no task assigned.
released := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
assert.Equal(t, StatusWaiting, released.Status)
assert.Zero(t, released.TaskID)
assert.Zero(t, released.Started)
// The task and its steps are gone.
unittest.AssertNotExistsBean(t, &ActionTask{ID: task.ID})
unittest.AssertNotExistsBean(t, &ActionTaskStep{TaskID: task.ID})
}
+1 -1
View File
@@ -166,7 +166,7 @@ func (a *Action) TableIndices() []*schemas.Index {
cudIndex.AddColumn("created_unix", "user_id", "is_deleted")
cuIndex := schemas.NewIndex("c_u", schemas.IndexType)
cuIndex.AddColumn("user_id", "is_deleted", "created_unix")
cuIndex.AddColumn("user_id", "is_deleted")
actUserUserIndex := schemas.NewIndex("au_c_u", schemas.IndexType)
actUserUserIndex.AddColumn("act_user_id", "created_unix", "user_id")
+7 -5
View File
@@ -18,8 +18,6 @@ import (
"gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// Task represents a task
@@ -174,13 +172,17 @@ func (err ErrTaskDoesNotExist) Unwrap() error {
// GetMigratingTask returns the migrating task by repo's id
func GetMigratingTask(ctx context.Context, repoID int64) (*Task, error) {
task, has, err := db.Get[Task](ctx, builder.Eq{"repo_id": repoID, "`type`": structs.TaskTypeMigrateRepo})
task := Task{
RepoID: repoID,
Type: structs.TaskTypeMigrateRepo,
}
has, err := db.GetEngine(ctx).Get(&task)
if err != nil {
return nil, err
} else if !has {
return nil, ErrTaskDoesNotExist{0, repoID, structs.TaskTypeMigrateRepo}
return nil, ErrTaskDoesNotExist{0, repoID, task.Type}
}
return task, nil
return &task, nil
}
// CreateTask creates a task on database
+5 -7
View File
@@ -20,7 +20,6 @@ import (
"gitea.dev/modules/setting"
"strk.kbt.io/projects/go/libravatar"
"xorm.io/builder"
)
const (
@@ -100,13 +99,12 @@ func HashEmail(email string) string {
// GetEmailForHash converts a provided md5sum to the email
func GetEmailForHash(ctx context.Context, md5Sum string) (string, error) {
return cache.GetString("Avatar:"+md5Sum, func() (string, error) {
emailHash, has, err := db.Get[EmailHash](ctx, builder.Eq{"`hash`": strings.ToLower(strings.TrimSpace(md5Sum))})
if err != nil {
return "", err
} else if !has {
return "", nil
emailHash := EmailHash{
Hash: strings.ToLower(strings.TrimSpace(md5Sum)),
}
return emailHash.Email, nil
_, err := db.GetEngine(ctx).Get(&emailHash)
return emailHash.Email, err
})
}
+7 -1
View File
@@ -323,7 +323,13 @@ type RenamedBranch struct {
// FindRenamedBranch check if a branch was renamed
func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *RenamedBranch, exist bool, err error) {
return db.Get[RenamedBranch](ctx, builder.Eq{"repo_id": repoID, "`from`": from})
branch = &RenamedBranch{
RepoID: repoID,
From: from,
}
exist, err = db.GetEngine(ctx).Get(branch)
return branch, exist, err
}
// RenameBranch rename a branch
+2 -1
View File
@@ -126,7 +126,8 @@ func GetLFSMetaObjectByOid(ctx context.Context, repoID int64, oid string) (*LFSM
return nil, ErrLFSObjectNotExist
}
m, has, err := db.Get[LFSMetaObject](ctx, builder.Eq{"repository_id": repoID, "oid": oid})
m := &LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}, RepositoryID: repoID}
has, err := db.GetEngine(ctx).Get(m)
if err != nil {
return nil, err
} else if !has {
+2 -3
View File
@@ -13,8 +13,6 @@ import (
"gitea.dev/models/organization"
"gitea.dev/modules/glob"
"gitea.dev/modules/timeutil"
"xorm.io/builder"
)
// ProtectedTag struct
@@ -113,7 +111,8 @@ func GetProtectedTagByID(ctx context.Context, id int64) (*ProtectedTag, error) {
// GetProtectedTagByNamePattern gets protected tag by name_pattern
func GetProtectedTagByNamePattern(ctx context.Context, repoID int64, pattern string) (*ProtectedTag, error) {
tag, has, err := db.Get[ProtectedTag](ctx, builder.Eq{"name_pattern": pattern, "repo_id": repoID})
tag := &ProtectedTag{NamePattern: pattern, RepoID: repoID}
has, err := db.GetEngine(ctx).Get(tag)
if err != nil {
return nil, err
}
+8 -8
View File
@@ -79,14 +79,6 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
return nil, err
}
if err := comments.loadResolveDoers(ctx); err != nil {
return nil, err
}
if err := comments.loadReactions(ctx, issue.Repo); err != nil {
return nil, err
}
// Find all reviews by ReviewID
reviews := make(map[int64]*Review)
ids := make([]int64, 0, len(comments))
@@ -115,6 +107,14 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
comments[n] = comment
n++
if err := comment.LoadResolveDoer(ctx); err != nil {
return nil, err
}
if err := comment.LoadReactions(ctx, issue.Repo); err != nil {
return nil, err
}
var err error
rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
FootnoteContextID: strconv.FormatInt(comment.ID, 10),
-68
View File
@@ -11,7 +11,6 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
)
// CommentList defines a list of comments
@@ -445,73 +444,6 @@ func (comments CommentList) loadReviews(ctx context.Context) error {
return nil
}
// loadResolveDoers bulk-loads the resolve doer for all code comments that have one.
func (comments CommentList) loadResolveDoers(ctx context.Context) error {
resolveDoerIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) {
return c.ResolveDoerID, c.ResolveDoerID != 0 && c.Type == CommentTypeCode
})
if len(resolveDoerIDs) == 0 {
return nil
}
userMaps, err := user_model.GetUsersMapByIDs(ctx, resolveDoerIDs)
if err != nil {
return err
}
for _, comment := range comments {
if comment.ResolveDoerID == 0 || comment.Type != CommentTypeCode {
continue
}
if u, ok := userMaps[comment.ResolveDoerID]; ok {
comment.ResolveDoer = u
} else {
comment.ResolveDoer = user_model.NewGhostUser()
}
}
return nil
}
// loadReactions bulk-loads reactions for all comments in the list.
func (comments CommentList) loadReactions(ctx context.Context, repo *repo_model.Repository) error {
if len(comments) == 0 {
return nil
}
commentIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) {
return c.ID, c.Reactions == nil
})
if len(commentIDs) == 0 {
return nil
}
var allReactions ReactionList
if err := db.GetEngine(ctx).
Where("`comment_id` > 0").
In("comment_id", commentIDs).
In("`type`", setting.UI.Reactions).
Asc("issue_id", "comment_id", "created_unix", "id").
Find(&allReactions); err != nil {
return err
}
if _, err := allReactions.LoadUsers(ctx, repo); err != nil {
return err
}
reactByComment := make(map[int64]ReactionList, len(commentIDs))
for _, r := range allReactions {
reactByComment[r.CommentID] = append(reactByComment[r.CommentID], r)
}
for _, comment := range comments {
if comment.Reactions == nil {
comment.Reactions = reactByComment[comment.ID]
}
}
return nil
}
// LoadAttributes loads attributes of the comments, except for attachments and
// comments
func (comments CommentList) LoadAttributes(ctx context.Context) (err error) {
+7 -3
View File
@@ -361,8 +361,8 @@ func (issue *Issue) ResetAttributesLoaded() {
// GetIsRead load the `IsRead` field of the issue
func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
issueUser, has, err := db.Get[IssueUser](ctx, builder.Eq{"issue_id": issue.ID, "uid": userID})
if err != nil {
issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
if has, err := db.GetEngine(ctx).Get(issueUser); err != nil {
return err
} else if !has {
issue.IsRead = false
@@ -499,7 +499,11 @@ func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
if index < 1 {
return nil, ErrIssueNotExist{}
}
issue, has, err := db.Get[Issue](ctx, builder.Eq{"repo_id": repoID, "`index`": index})
issue := &Issue{
RepoID: repoID,
Index: index,
}
has, err := db.GetEngine(ctx).Get(issue)
if err != nil {
return nil, err
} else if !has {
+1 -4
View File
@@ -591,12 +591,9 @@ func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*Rev
func (issues IssueList) LoadIsRead(ctx context.Context, userID int64) error {
issueIDs := issues.getIssueIDs()
if len(issueIDs) == 0 {
return nil
}
issueUsers := make([]*IssueUser, 0, len(issueIDs))
if err := db.GetEngine(ctx).Where("uid =?", userID).
In("issue_id", issueIDs).
In("issue_id").
Find(&issueUsers); err != nil {
return err
}
-17
View File
@@ -11,7 +11,6 @@ import (
"gitea.dev/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIssueList_LoadRepositories(t *testing.T) {
@@ -31,22 +30,6 @@ func TestIssueList_LoadRepositories(t *testing.T) {
}
}
func TestIssueList_LoadIsRead(t *testing.T) {
// Regression: In("issue_id") was missing the issueIDs argument, causing
// xorm to generate "0=1" and never mark any issue as read.
require.NoError(t, unittest.PrepareTestDatabase())
issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
// Fixture: uid=1 has is_read=true on issue 1 only.
issueList := issues_model.IssueList{issue1, issue2}
require.NoError(t, issueList.LoadIsRead(t.Context(), 1))
assert.True(t, issue1.IsRead, "issue 1 should be marked read for user 1")
assert.False(t, issue2.IsRead, "issue 2 should not be marked read for user 1")
}
func TestIssueList_LoadAttributes(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
setting.Service.EnableTimetracking = true
+1 -1
View File
@@ -599,7 +599,7 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
continue
}
has, err := db.Exist[organization.TeamUnit](ctx, builder.Eq{"org_id": issue.Repo.Owner.ID, "team_id": team.ID, "`type`": unittype})
has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
if err != nil {
return nil, fmt.Errorf("get team units (%d): %w", team.ID, err)
}
+4 -6
View File
@@ -13,8 +13,6 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/references"
"xorm.io/builder"
)
type crossReference struct {
@@ -191,11 +189,11 @@ func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *cross
func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossReferencesContext, repo *repo_model.Repository,
ref references.IssueReference,
) (*Issue, references.XRefAction, error) {
refIssue := &Issue{RepoID: repo.ID, Index: ref.Index}
refAction := ref.Action
refIssue, has, err := db.Get[Issue](stdCtx, builder.Eq{"repo_id": repo.ID, "`index`": ref.Index})
if err != nil {
return nil, references.XRefActionNone, err
} else if !has {
e := db.GetEngine(stdCtx)
if has, _ := e.Get(refIssue); !has {
return nil, references.XRefActionNone, nil
}
if err := refIssue.LoadRepo(stdCtx); err != nil {
+5 -1
View File
@@ -535,8 +535,12 @@ func GetPullRequestByIndex(ctx context.Context, repoID, index int64) (*PullReque
if index < 1 {
return nil, ErrPullRequestNotExist{}
}
pr := &PullRequest{
BaseRepoID: repoID,
Index: index,
}
pr, has, err := db.Get[PullRequest](ctx, builder.Eq{"base_repo_id": repoID, "`index`": index})
has, err := db.GetEngine(ctx).Get(pr)
if err != nil {
return nil, err
} else if !has {
-5
View File
@@ -415,11 +415,6 @@ func prepareMigrationTasks() []*migration {
newMigration(335, "Add reusable workflow fields and action_run_attempt_job_id_index table for ActionRunJob", v1_27.AddReusableWorkflowFieldsToActionRunJob),
newMigration(336, "Add ActionRunJobSummary table", v1_27.AddActionRunJobSummaryTable),
newMigration(337, "Add visibility to team", v1_27.AddVisibilityToTeam),
newMigration(338, "Expand legacy MSSQL issue/comment long-text columns", v1_27.ExpandIssueAndCommentLongTextFieldsForMSSQL),
newMigration(339, "Extend action c_u index to include created_unix for faster dashboard feed queries", v1_27.AddCreatedUnixToActionUserIsDeletedIndex),
newMigration(340, "Add ContinueOnError column to ActionRunJob", v1_27.AddContinueOnErrorToActionRunJob),
newMigration(341, "Convert legacy MSSQL DATETIME columns to DATETIME2", v1_27.FixLegacyMSSQLDateTimeColumns),
newMigration(342, "Add scoped workflows schema", v1_27.AddScopedWorkflowsSchema),
}
return preparedMigrations
}
-73
View File
@@ -1,73 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"fmt"
"strings"
"gitea.dev/models/db"
"gitea.dev/models/migrations/base"
"xorm.io/xorm/schemas"
)
type issueWithLongTextContent struct {
Content string `xorm:"LONGTEXT"`
}
func (issueWithLongTextContent) TableName() string {
return "issue"
}
type commentWithLongTextFields struct {
Content string `xorm:"LONGTEXT"`
PatchQuoted string `xorm:"LONGTEXT patch"`
}
func (commentWithLongTextFields) TableName() string {
return "comment"
}
func isMSSQLMaxTextColumn(column *schemas.Column) bool {
if column.Length != -1 {
return false
}
return strings.EqualFold(column.SQLType.Name, schemas.Varchar) || strings.EqualFold(column.SQLType.Name, schemas.NVarchar)
}
func modifyLongTextColumnsForMSSQL(x db.EngineMigration, bean any, columnNames ...string) error {
table, err := x.TableInfo(bean)
if err != nil {
return err
}
for _, columnName := range columnNames {
column := table.GetColumn(columnName)
if column == nil {
return fmt.Errorf("column %s does not exist in table %s", columnName, table.Name)
}
if isMSSQLMaxTextColumn(column) {
continue
}
if err := base.ModifyColumn(x, table.Name, column); err != nil {
return fmt.Errorf("modify %s.%s: %w", table.Name, columnName, err)
}
}
return nil
}
// ExpandIssueAndCommentLongTextFieldsForMSSQL expands legacy MSSQL nvarchar(4000)
// columns to nvarchar(max) so PR push comments and long issue content are not truncated.
func ExpandIssueAndCommentLongTextFieldsForMSSQL(x db.EngineMigration) error {
if x.Dialect().URI().DBType != schemas.MSSQL {
return nil
}
if err := modifyLongTextColumnsForMSSQL(x, new(issueWithLongTextContent), "content"); err != nil {
return err
}
return modifyLongTextColumnsForMSSQL(x, new(commentWithLongTextFields), "content", "patch")
}
-52
View File
@@ -1,52 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"strings"
"testing"
"gitea.dev/models/migrations/migrationtest"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/require"
)
type issueBeforeLongTextMSSQLMigration struct {
ID int64 `xorm:"pk autoincr"`
Content string `xorm:"VARCHAR(4000)"`
}
func (issueBeforeLongTextMSSQLMigration) TableName() string {
return "issue"
}
type commentBeforeLongTextMSSQLMigration struct {
ID int64 `xorm:"pk autoincr"`
Content string `xorm:"VARCHAR(4000)"`
Patch string `xorm:"VARCHAR(4000) patch"`
}
func (commentBeforeLongTextMSSQLMigration) TableName() string {
return "comment"
}
func Test_ExpandIssueAndCommentLongTextFieldsForMSSQL(t *testing.T) {
if !setting.Database.Type.IsMSSQL() {
t.Skip("Only MSSQL needs to expand legacy nvarchar(4000) long-text columns")
}
x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(issueBeforeLongTextMSSQLMigration), new(commentBeforeLongTextMSSQLMigration))
defer deferrable()
require.NoError(t, ExpandIssueAndCommentLongTextFieldsForMSSQL(x))
require.NoError(t, ExpandIssueAndCommentLongTextFieldsForMSSQL(x))
longText := strings.Repeat("x", 5000)
_, err := x.Insert(&issueBeforeLongTextMSSQLMigration{Content: longText})
require.NoError(t, err)
_, err = x.Insert(&commentBeforeLongTextMSSQLMigration{Content: longText, Patch: longText})
require.NoError(t, err)
}
-40
View File
@@ -1,40 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"context"
"gitea.dev/models/db"
"xorm.io/xorm/schemas"
)
// AddCreatedUnixToActionUserIsDeletedIndex extends the c_u composite index on
// the action table to include created_unix, enabling efficient ORDER BY on the
// dashboard feed query without a full sort of all matching rows.
func AddCreatedUnixToActionUserIsDeletedIndex(x db.EngineMigration) error {
// xorm Sync cannot reliably update an index when another index already
// covers the same columns in a different order (Equal() is order-insensitive).
// Drop the old c_u index explicitly, then recreate it with the new column set.
indexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action")
if err != nil {
return err
}
for _, idx := range indexes {
if idx.Name == "c_u" {
if _, err := x.Exec(x.Dialect().DropIndexSQL("action", idx)); err != nil {
return err
}
break
}
}
newIndex := schemas.NewIndex("c_u", schemas.IndexType)
newIndex.AddColumn("user_id", "is_deleted", "created_unix")
if _, err := x.Exec(x.Dialect().CreateIndexSQL("action", newIndex)); err != nil {
return err
}
return nil
}
-72
View File
@@ -1,72 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"context"
"testing"
"gitea.dev/models/migrations/migrationtest"
"gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/xorm/schemas"
)
type actionBeforeV339 struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX"`
OpType int
ActUserID int64
RepoID int64
CommentID int64 `xorm:"INDEX"`
IsDeleted bool `xorm:"NOT NULL DEFAULT false"`
RefName string
IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
Content string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
}
func (actionBeforeV339) TableName() string { return "action" }
func (actionBeforeV339) TableIndices() []*schemas.Index {
repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType)
repoIndex.AddColumn("repo_id", "user_id", "is_deleted")
actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType)
actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted")
cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType)
cudIndex.AddColumn("created_unix", "user_id", "is_deleted")
// old 2-column index, before the migration
cuIndex := schemas.NewIndex("c_u", schemas.IndexType)
cuIndex.AddColumn("user_id", "is_deleted")
actUserUserIndex := schemas.NewIndex("au_c_u", schemas.IndexType)
actUserUserIndex.AddColumn("act_user_id", "created_unix", "user_id")
return []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex, actUserUserIndex}
}
func Test_AddCreatedUnixToActionUserIsDeletedIndex(t *testing.T) {
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(actionBeforeV339))
defer deferable()
if x == nil || t.Failed() {
return
}
indexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action")
require.NoError(t, err)
assert.True(t, hasIndexWithColumns(indexes, []string{"user_id", "is_deleted"}, false), "old c_u index should exist before migration")
assert.False(t, hasIndexWithColumns(indexes, []string{"user_id", "is_deleted", "created_unix"}, false), "new c_u index should not exist before migration")
require.NoError(t, AddCreatedUnixToActionUserIsDeletedIndex(x))
indexes, err = x.Dialect().GetIndexes(x.DB(), context.Background(), "action")
require.NoError(t, err)
assert.False(t, hasIndexWithColumns(indexes, []string{"user_id", "is_deleted"}, false), "old 2-column c_u index should be gone after migration")
assert.True(t, hasIndexWithColumns(indexes, []string{"user_id", "is_deleted", "created_unix"}, false), "new 3-column c_u index must exist after migration")
}
-24
View File
@@ -1,24 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"xorm.io/xorm"
)
// AddContinueOnErrorToActionRunJob adds the ContinueOnError column to ActionRunJob,
// storing the job-level continue-on-error value from the workflow YAML.
func AddContinueOnErrorToActionRunJob(x db.EngineMigration) error {
type ActionRunJob struct {
ContinueOnError bool `xorm:"NOT NULL DEFAULT FALSE"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(ActionRunJob))
return err
}
-80
View File
@@ -1,80 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"fmt"
"strings"
"time"
"gitea.dev/models/db"
"gitea.dev/models/migrations/base"
"xorm.io/xorm/schemas"
)
// legacyDateTimeColumns are the persisted real datetime columns that old Gitea
// versions created as MSSQL DATETIME. Every other time value is stored as a
// unix timestamp integer, so these are the only columns affected.
var legacyDateTimeColumns = []struct {
bean any
column string
}{
{new(externalLoginUserWithExpiresAt), "expires_at"},
{new(lfsLockWithCreated), "created"},
}
type externalLoginUserWithExpiresAt struct {
ExpiresAt time.Time
}
func (externalLoginUserWithExpiresAt) TableName() string {
return "external_login_user"
}
type lfsLockWithCreated struct {
Created time.Time `xorm:"created"`
}
func (lfsLockWithCreated) TableName() string {
return "lfs_lock"
}
// FixLegacyMSSQLDateTimeColumns converts legacy locale-dependent DATETIME columns
// to DATETIME2. Databases created by old Gitea versions stored these columns as
// DATETIME, which fails to parse ISO datetime strings ('YYYY-MM-DD HH:MM:SS')
// when the MSSQL session language is not English, breaking external account
// linking and LFS lock creation. New installs already use DATETIME2, so only
// legacy MSSQL columns need converting.
func FixLegacyMSSQLDateTimeColumns(x db.EngineMigration) error {
if x.Dialect().URI().DBType != schemas.MSSQL {
return nil
}
for _, c := range legacyDateTimeColumns {
table, err := x.TableInfo(c.bean)
if err != nil {
return err
}
var dataType string
has, err := x.SQL("SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?", table.Name, c.column).Get(&dataType)
if err != nil {
return err
}
if !has || !strings.EqualFold(dataType, "datetime") {
continue
}
column := table.GetColumn(c.column)
if column == nil {
return fmt.Errorf("column %s does not exist in table %s", c.column, table.Name)
}
if err := base.ModifyColumn(x, table.Name, column); err != nil {
return fmt.Errorf("modify %s.%s: %w", table.Name, c.column, err)
}
}
return nil
}
-80
View File
@@ -1,80 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"testing"
"time"
"gitea.dev/models/db"
"gitea.dev/models/migrations/migrationtest"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/require"
)
type externalLoginUserBeforeDateTimeMigration struct {
ExternalID string `xorm:"pk NOT NULL"`
LoginSourceID int64 `xorm:"pk NOT NULL"`
ExpiresAt time.Time // sync creates DATETIME2; downgraded to legacy DATETIME via raw SQL below
}
func (externalLoginUserBeforeDateTimeMigration) TableName() string {
return "external_login_user"
}
type lfsLockBeforeDateTimeMigration struct {
ID int64 `xorm:"pk autoincr"`
Created time.Time `xorm:"created"`
}
func (lfsLockBeforeDateTimeMigration) TableName() string {
return "lfs_lock"
}
func Test_FixLegacyMSSQLDateTimeColumns(t *testing.T) {
if !setting.Database.Type.IsMSSQL() {
t.Skip("Only MSSQL needs to convert the legacy locale-dependent DATETIME columns")
}
x, deferrable := migrationtest.PrepareTestEnv(t, 0,
new(externalLoginUserBeforeDateTimeMigration),
new(lfsLockBeforeDateTimeMigration),
)
defer deferrable()
// Force the legacy DATETIME column type that old Gitea versions created.
_, err := x.Exec("ALTER TABLE [external_login_user] ALTER COLUMN [expires_at] DATETIME")
require.NoError(t, err)
_, err = x.Exec("ALTER TABLE [lfs_lock] ALTER COLUMN [created] DATETIME")
require.NoError(t, err)
require.Equal(t, "datetime", mssqlColumnType(t, x, "external_login_user", "expires_at"))
require.Equal(t, "datetime", mssqlColumnType(t, x, "lfs_lock", "created"))
require.NoError(t, FixLegacyMSSQLDateTimeColumns(x))
require.NoError(t, FixLegacyMSSQLDateTimeColumns(x)) // idempotent
require.Equal(t, "datetime2", mssqlColumnType(t, x, "external_login_user", "expires_at"))
require.Equal(t, "datetime2", mssqlColumnType(t, x, "lfs_lock", "created"))
// Inserting an ISO-formatted datetime must succeed even under a non-English
// locale, which is the failure the legacy DATETIME columns produced. The
// SET LANGUAGE and INSERT run in one Exec so they share a single connection.
_, err = x.Exec("SET LANGUAGE German; " +
"INSERT INTO [external_login_user] ([external_id], [login_source_id], [expires_at]) " +
"VALUES ('ext-id', 1, '2026-06-25 11:58:39')")
require.NoError(t, err)
_, err = x.Exec("SET LANGUAGE German; " +
"INSERT INTO [lfs_lock] ([created]) VALUES ('2026-06-25 11:58:39')")
require.NoError(t, err)
}
func mssqlColumnType(t *testing.T, x db.EngineMigration, table, column string) string {
t.Helper()
var dataType string
has, err := x.SQL("SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?", table, column).Get(&dataType)
require.NoError(t, err)
require.True(t, has)
return dataType
}
-42
View File
@@ -1,42 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
"xorm.io/xorm"
)
func AddScopedWorkflowsSchema(x db.EngineMigration) error {
// Create the action_scoped_workflow_source table
type ScopedWorkflowConfig struct {
Required bool `json:"required"`
Patterns []string `json:"patterns"`
}
type ActionScopedWorkflowSource struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
SourceRepoID int64 `xorm:"INDEX UNIQUE(owner_repo) NOT NULL DEFAULT 0"`
WorkflowConfigs map[string]*ScopedWorkflowConfig `xorm:"JSON TEXT 'workflow_configs'"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
if err := x.Sync(new(ActionScopedWorkflowSource)); err != nil {
return err
}
// Add the columns that record where a run's workflow content came from
type ActionRun struct {
WorkflowRepoID int64 `xorm:"NOT NULL DEFAULT 0"`
WorkflowCommitSHA string `xorm:"VARCHAR(64) NOT NULL DEFAULT ''"`
IsScopedRun bool `xorm:"NOT NULL DEFAULT false"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(ActionRun))
return err
}
+5 -2
View File
@@ -411,8 +411,11 @@ func GetOrgByName(ctx context.Context, name string) (*Organization, error) {
if len(name) == 0 {
return nil, ErrOrgNotExist{0, name}
}
u, has, err := db.Get[Organization](ctx, builder.Eq{"lower_name": strings.ToLower(name), "`type`": user_model.UserTypeOrganization})
u := &Organization{
LowerName: strings.ToLower(name),
Type: user_model.UserTypeOrganization,
}
has, err := db.GetEngine(ctx).Get(u)
if err != nil {
return nil, err
} else if !has {
+4 -3
View File
@@ -58,7 +58,7 @@ func GetProperties(ctx context.Context, refType PropertyType, refID int64) ([]*P
// GetPropertiesByName gets all properties with a specific name
func GetPropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) ([]*PackageProperty, error) {
pps := make([]*PackageProperty, 0, 10)
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND `name` = ?", refType, refID, name).OrderBy("id").Find(&pps)
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).OrderBy("id").Find(&pps)
}
// UpdateProperty updates a property
@@ -68,12 +68,13 @@ func UpdateProperty(ctx context.Context, pp *PackageProperty) error {
}
func InsertOrUpdateProperty(ctx context.Context, refType PropertyType, refID int64, name, value string) error {
pp, ok, err := db.Get[PackageProperty](ctx, builder.Eq{"ref_type": refType, "ref_id": refID, "`name`": name})
pp := PackageProperty{RefType: refType, RefID: refID, Name: name}
ok, err := db.GetEngine(ctx).Get(&pp)
if err != nil {
return err
}
if ok {
_, err = db.GetEngine(ctx).ID(pp.ID).Cols("value").Update(&PackageProperty{Value: value})
_, err = db.GetEngine(ctx).Where("ref_type=? AND ref_id=? AND name=?", refType, refID, name).Cols("value").Update(&PackageProperty{Value: value})
return err
}
_, err = InsertProperty(ctx, refType, refID, name, value)
+3 -1
View File
@@ -164,7 +164,9 @@ func DeleteVersionsByPackageID(ctx context.Context, packageID int64) error {
// HasVersionFileReferences checks if there are associated files
func HasVersionFileReferences(ctx context.Context, versionID int64) (bool, error) {
return db.Exist[PackageFile](ctx, builder.Eq{"version_id": versionID})
return db.GetEngine(ctx).Get(&PackageFile{
VersionID: versionID,
})
}
// SearchValue describes a value to search
@@ -95,40 +95,6 @@ func TestGetActionsUserRepoPermission(t *testing.T) {
assert.False(t, perm.CanRead(unit.TypeCode))
})
t.Run("CollaborativeOwner_ForkPR_Denied", func(t *testing.T) {
// Target repo15 trusts repo2's owner as a collaborative owner.
repo15ActionsUnit := repo15.MustGetUnit(ctx, unit.TypeActions)
repo15ActionsUnit.ActionsConfig().AddCollaborativeOwner(owner2.ID)
require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo15ActionsUnit))
// Owner cross-repo policy does not allow repo15, so the only branch that
// could grant access is the collaborative-owner one.
require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, actions_model.OwnerActionsConfig{}))
task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
// Non-fork task is legitimately granted code-read via collaborative owner.
task53.IsForkPullRequest = false
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
perm, err := GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID)
require.NoError(t, err)
assert.True(t, perm.CanRead(unit.TypeCode))
// Fork PR must NOT be able to read a third private repo through the
// collaborative-owner branch.
task53.IsForkPullRequest = true
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
perm, err = GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID)
require.NoError(t, err)
assert.False(t, perm.CanRead(unit.TypeCode))
// Restore state for subsequent subtests.
task53.IsForkPullRequest = false
require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request"))
repo15ActionsUnit.ActionsConfig().RemoveCollaborativeOwner(owner2.ID)
require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo15ActionsUnit))
})
t.Run("Inheritance_And_Clamping", func(t *testing.T) {
task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
task53.IsForkPullRequest = false
+1 -3
View File
@@ -369,9 +369,7 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
// 2. The Actions Bot user has been explicitly granted access and repository is private
// 3. The repository is public (handled by botPerm above)
// Fork PRs are never allowed cross-repo access to other private repositories,
// matching the discriminator enforced by checkSameOwnerCrossRepoAccess above.
if taskRepo.IsPrivate && !task.IsForkPullRequest {
if taskRepo.IsPrivate {
actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions)
if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) {
return maxPerm, nil
+3 -10
View File
@@ -11,8 +11,6 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
"xorm.io/builder"
)
// ViewedState stores for a file in which state it is currently viewed
@@ -68,14 +66,9 @@ func (rs *ReviewState) GetViewedFileCount() int {
// If the review didn't exist before in the database, it won't afterwards either.
// The returned boolean shows whether the review exists in the database
func GetReviewState(ctx context.Context, userID, pullID int64, commitSHA string) (*ReviewState, bool, error) {
review, has, err := db.Get[ReviewState](ctx, builder.Eq{"user_id": userID, "pull_id": pullID, "commit_sha": commitSHA})
if err != nil {
return nil, false, err
}
if review == nil {
review = &ReviewState{UserID: userID, PullID: pullID, CommitSHA: commitSHA}
}
return review, has, nil
review := &ReviewState{UserID: userID, PullID: pullID, CommitSHA: commitSHA}
has, err := db.GetEngine(ctx).Get(review)
return review, has, err
}
// UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not
+2 -3
View File
@@ -17,8 +17,6 @@ import (
"gitea.dev/modules/storage"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// Attachment represent a attachment of issue/comment/release.
@@ -158,7 +156,8 @@ func GetAttachmentsByCommentID(ctx context.Context, commentID int64) ([]*Attachm
// GetAttachmentByReleaseIDFileName returns attachment by given releaseId and fileName.
func GetAttachmentByReleaseIDFileName(ctx context.Context, releaseID int64, fileName string) (*Attachment, error) {
attach, has, err := db.Get[Attachment](ctx, builder.Eq{"release_id": releaseID, "`name`": fileName})
attach := &Attachment{ReleaseID: releaseID, Name: fileName}
has, err := db.GetEngine(ctx).Get(attach)
if err != nil {
return nil, err
} else if !has {
+19 -6
View File
@@ -102,13 +102,20 @@ func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*C
// GetCollaboration get collaboration for a repository id with a user id
func GetCollaboration(ctx context.Context, repoID, uid int64) (*Collaboration, error) {
collaboration, _, err := db.Get[Collaboration](ctx, builder.Eq{"repo_id": repoID, "user_id": uid})
collaboration := &Collaboration{
RepoID: repoID,
UserID: uid,
}
has, err := db.GetEngine(ctx).Get(collaboration)
if !has {
collaboration = nil
}
return collaboration, err
}
// IsCollaborator check if a user is a collaborator of a repository
func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
return db.Exist[Collaboration](ctx, builder.Eq{"repo_id": repoID, "user_id": userID})
return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID})
}
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
@@ -119,7 +126,13 @@ func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid in
}
return db.WithTx(ctx, func(ctx context.Context) error {
collaboration, has, err := db.Get[Collaboration](ctx, builder.Eq{"repo_id": repo.ID, "user_id": uid})
e := db.GetEngine(ctx)
collaboration := &Collaboration{
RepoID: repo.ID,
UserID: uid,
}
has, err := e.Get(collaboration)
if err != nil {
return fmt.Errorf("get collaboration: %w", err)
} else if !has {
@@ -131,12 +144,12 @@ func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid in
}
collaboration.Mode = mode
if _, err = db.GetEngine(ctx).
if _, err = e.
ID(collaboration.ID).
Cols("mode").
Update(collaboration); err != nil {
return fmt.Errorf("update collaboration: %w", err)
} else if _, err = db.Exec(ctx, "UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil {
} else if _, err = e.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil {
return fmt.Errorf("update access table: %w", err)
}
@@ -161,5 +174,5 @@ func IsOwnerMemberCollaborator(ctx context.Context, repo *Repository, userID int
return true, nil
}
return db.Exist[Collaboration](ctx, builder.Eq{"repo_id": repo.ID, "user_id": userID})
return db.GetEngine(ctx).Get(&Collaboration{RepoID: repo.ID, UserID: userID})
}
+2 -3
View File
@@ -12,8 +12,6 @@ import (
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ErrMirrorNotExist mirror does not exist error
@@ -78,7 +76,8 @@ func (m *Mirror) ScheduleNextUpdate() {
// GetMirrorByRepoID returns mirror information of a repository.
func GetMirrorByRepoID(ctx context.Context, repoID int64) (*Mirror, error) {
m, has, err := db.Get[Mirror](ctx, builder.Eq{"repo_id": repoID})
m := &Mirror{RepoID: repoID}
has, err := db.GetEngine(ctx).Get(m)
if err != nil {
return nil, err
} else if !has {
+2 -4
View File
@@ -10,8 +10,6 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ErrRedirectNotExist represents a "RedirectNotExist" kind of error.
@@ -54,8 +52,8 @@ func init() {
// LookupRedirect look up if a repository has a redirect name
func LookupRedirect(ctx context.Context, ownerID int64, repoName string) (int64, error) {
repoName = strings.ToLower(repoName)
redirect, has, err := db.Get[Redirect](ctx, builder.Eq{"owner_id": ownerID, "lower_name": repoName})
if err != nil {
redirect := &Redirect{OwnerID: ownerID, LowerName: repoName}
if has, err := db.GetEngine(ctx).Get(redirect); err != nil {
return 0, err
} else if !has {
return 0, ErrRedirectNotExist{OwnerID: ownerID, RepoName: repoName}
+2 -1
View File
@@ -216,7 +216,8 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs
// GetRelease returns release by given ID.
func GetRelease(ctx context.Context, repoID int64, tagName string) (*Release, error) {
rel, has, err := db.Get[Release](ctx, builder.Eq{"repo_id": repoID, "lower_tag_name": strings.ToLower(tagName)})
rel := &Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)}
has, err := db.GetEngine(ctx).Get(rel)
if err != nil {
return nil, err
} else if !has {
+3 -3
View File
@@ -849,9 +849,9 @@ func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repos
}
func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
return db.Exist[Repository](ctx, builder.Eq{
"owner_id": u.ID,
"lower_name": strings.ToLower(repoName),
return db.GetEngine(ctx).Get(&Repository{
OwnerID: u.ID,
LowerName: strings.ToLower(repoName),
})
}
-25
View File
@@ -70,8 +70,6 @@ func MakeRestrictedPermissions() ActionsTokenPermissions {
type ActionsConfig struct {
DisabledWorkflows []string
// DisabledScopedWorkflows maps a scoped workflow's source repository ID to the entry names opted out of in this repository.
DisabledScopedWorkflows map[int64][]string
// CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos.
// Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions.
CollaborativeOwnerIDs []int64
@@ -100,29 +98,6 @@ func (cfg *ActionsConfig) DisableWorkflow(file string) {
cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file)
}
func (cfg *ActionsConfig) IsScopedWorkflowDisabled(sourceRepoID int64, workflowID string) bool {
return slices.Contains(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID)
}
func (cfg *ActionsConfig) DisableScopedWorkflow(sourceRepoID int64, workflowID string) {
if slices.Contains(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID) {
return
}
if cfg.DisabledScopedWorkflows == nil {
cfg.DisabledScopedWorkflows = make(map[int64][]string)
}
cfg.DisabledScopedWorkflows[sourceRepoID] = append(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID)
}
func (cfg *ActionsConfig) EnableScopedWorkflow(sourceRepoID int64, workflowID string) {
workflowIDs := util.SliceRemoveAll(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID)
if len(workflowIDs) == 0 {
delete(cfg.DisabledScopedWorkflows, sourceRepoID)
return
}
cfg.DisabledScopedWorkflows[sourceRepoID] = workflowIDs
}
func (cfg *ActionsConfig) AddCollaborativeOwner(ownerID int64) {
if !slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) {
cfg.CollaborativeOwnerIDs = append(cfg.CollaborativeOwnerIDs, ownerID)
-51
View File
@@ -1,51 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestActionsConfig_ScopedWorkflowOptOut(t *testing.T) {
cfg := &ActionsConfig{}
assert.False(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"))
cfg.DisableScopedWorkflow(100, "ci.yml")
assert.True(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"))
// idempotent
cfg.DisableScopedWorkflow(100, "ci.yml")
assert.Len(t, cfg.DisabledScopedWorkflows, 1)
// keyed by source repo: the same filename from a different source repo is independent
assert.False(t, cfg.IsScopedWorkflowDisabled(200, "ci.yml"))
// must not collide with the repo-level DisabledWorkflows list (bare filename)
assert.False(t, cfg.IsWorkflowDisabled("ci.yml"))
cfg.DisableWorkflow("ci.yml")
assert.True(t, cfg.IsWorkflowDisabled("ci.yml"))
assert.True(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"), "repo-level disable must not touch the scoped entry")
cfg.EnableScopedWorkflow(100, "ci.yml")
assert.False(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"))
assert.True(t, cfg.IsWorkflowDisabled("ci.yml"), "enabling the scoped entry must not touch the repo-level disable")
}
func TestActionsConfig_ScopedWorkflowSerialization(t *testing.T) {
cfg := &ActionsConfig{}
cfg.DisableScopedWorkflow(100, "ci.yml")
cfg.DisableWorkflow("repo.yml")
bs, err := cfg.ToDB()
require.NoError(t, err)
got := &ActionsConfig{}
require.NoError(t, got.FromDB(bs))
assert.True(t, got.IsScopedWorkflowDisabled(100, "ci.yml"))
assert.True(t, got.IsWorkflowDisabled("repo.yml"))
}
+1 -3
View File
@@ -9,8 +9,6 @@ import (
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/timeutil"
"xorm.io/builder"
)
// Star represents a starred repo by a user.
@@ -70,7 +68,7 @@ func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star
// IsStaring checks if user has starred given repository.
func IsStaring(ctx context.Context, userID, repoID int64) bool {
has, _ := db.Exist[Star](ctx, builder.Eq{"uid": userID, "repo_id": repoID})
has, _ := db.GetEngine(ctx).Get(&Star{UID: userID, RepoID: repoID})
return has
}
+4 -8
View File
@@ -10,8 +10,6 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"gitea.dev/modules/timeutil"
"xorm.io/builder"
)
// WatchMode specifies what kind of watch the user has on a repository
@@ -43,14 +41,12 @@ func init() {
}
// GetWatch gets what kind of subscription a user has on a given repository; returns dummy record if none found
func GetWatch(ctx context.Context, userID, repoID int64) (*Watch, error) {
watch, has, err := db.Get[Watch](ctx, builder.Eq{"user_id": userID, "repo_id": repoID})
func GetWatch(ctx context.Context, userID, repoID int64) (Watch, error) {
watch := Watch{UserID: userID, RepoID: repoID}
has, err := db.GetEngine(ctx).Get(&watch)
if err != nil {
return watch, err
}
if watch == nil {
watch = &Watch{UserID: userID, RepoID: repoID}
}
if !has {
watch.Mode = WatchModeNone
}
@@ -68,7 +64,7 @@ func IsWatching(ctx context.Context, userID, repoID int64) bool {
return err == nil && IsWatchMode(watch.Mode)
}
func watchRepoMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) {
func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error) {
if watch.Mode == mode {
return nil
}
+3 -3
View File
@@ -7,8 +7,6 @@ import (
"context"
"gitea.dev/models/db"
"xorm.io/builder"
)
// AppState represents a state record in database
@@ -46,7 +44,9 @@ func SaveAppStateContent(ctx context.Context, key, content string) error {
// GetAppStateContent gets an app state from database
func GetAppStateContent(ctx context.Context, key string) (content string, err error) {
appState, has, err := db.Get[AppState](ctx, builder.Eq{"id": key})
e := db.GetEngine(ctx)
appState := &AppState{ID: key}
has, err := e.Get(appState)
if err != nil {
return "", err
} else if !has {
+2 -2
View File
@@ -348,8 +348,8 @@ func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddres
opts := &TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateEmail, NewEmail: email}
data := makeTimeLimitCodeHashData(opts, user)
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
emailAddress, has, _ := db.Get[EmailAddress](ctx, builder.Eq{"uid": user.ID, "email": email})
if has {
emailAddress := &EmailAddress{UID: user.ID, Email: email}
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
return emailAddress
}
}
+1 -3
View File
@@ -8,8 +8,6 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/timeutil"
"xorm.io/builder"
)
// Follow represents relations of user and their followers.
@@ -26,7 +24,7 @@ func init() {
// IsFollowing returns true if user is following followID.
func IsFollowing(ctx context.Context, userID, followID int64) bool {
has, _ := db.Exist[Follow](ctx, builder.Eq{"user_id": userID, "follow_id": followID})
has, _ := db.GetEngine(ctx).Get(&Follow{UserID: userID, FollowID: followID})
return has
}
+1 -3
View File
@@ -9,8 +9,6 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// UserOpenID is the list of all OpenID identities of a user.
@@ -45,7 +43,7 @@ func isOpenIDUsed(ctx context.Context, uri string) (bool, error) {
return true, nil
}
return db.Exist[UserOpenID](ctx, builder.Eq{"uri": uri})
return db.GetEngine(ctx).Get(&UserOpenID{URI: uri})
}
// ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error.
+2 -4
View File
@@ -10,8 +10,6 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// ErrUserRedirectNotExist represents a "UserRedirectNotExist" kind of error.
@@ -52,8 +50,8 @@ func init() {
// LookupUserRedirect look up userID if a user has a redirect name
func LookupUserRedirect(ctx context.Context, userName string) (int64, error) {
userName = strings.ToLower(userName)
redirect, has, err := db.Get[Redirect](ctx, builder.Eq{"lower_name": userName})
if err != nil {
redirect := &Redirect{LowerName: userName}
if has, err := db.GetEngine(ctx).Get(redirect); err != nil {
return 0, err
} else if !has {
return 0, ErrUserRedirectNotExist{Name: userName}
+2 -1
View File
@@ -125,7 +125,8 @@ func GetUserSetting(ctx context.Context, userID int64, key string, def ...string
return "", err
}
setting, has, err := db.Get[Setting](ctx, builder.Eq{"user_id": userID, "setting_key": key})
setting := &Setting{UserID: userID, SettingKey: key}
has, err := db.GetEngine(ctx).Get(setting)
if err != nil {
return "", err
}
+6 -21
View File
@@ -1276,7 +1276,8 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
email = strings.ToLower(email)
// Otherwise, check in alternative list for activated email addresses
emailAddress, has, err := db.Get[EmailAddress](ctx, builder.Eq{"lower_email": email, "is_activated": true})
emailAddress := &EmailAddress{LowerEmail: email, IsActivated: true}
has, err := db.GetEngine(ctx).Get(emailAddress)
if err != nil {
return nil, err
}
@@ -1296,29 +1297,13 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
return nil, ErrUserNotExist{Name: email}
}
func GetIndividualUserByPrimaryEmail(ctx context.Context, email string) (*User, error) {
email = strings.ToLower(strings.TrimSpace(email))
if len(email) == 0 {
return nil, ErrUserNotExist{Name: email}
}
user, has, err := db.Get[User](ctx, builder.Eq{"email": email, "type": UserTypeIndividual})
if err != nil {
return nil, err
}
if !has {
return nil, ErrUserNotExist{Name: email}
}
return user, nil
}
func GetIndividualUserByLoginSource(ctx context.Context, loginType auth.Type, loginSource int64, loginName string) (*User, bool, error) {
user, has, err := db.Get[User](ctx, builder.Eq{"login_type": loginType, "login_source": loginSource, "login_name": loginName})
func GetIndividualUser(ctx context.Context, user *User) (bool, error) {
// FIXME: the design is wrong, empty User fields won't apply, this function should be removed in the future
has, err := db.GetEngine(ctx).Get(user)
if has && user.Type != UserTypeIndividual {
has = false
user = nil
}
return user, has, err
return has, err
}
// GetUserByOpenID returns the user object by given OpenID if exists.
-3
View File
@@ -69,9 +69,6 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
runsOn[i] = evaluator.Interpolate(v)
}
job.RawRunsOn = encodeRunsOn(runsOn)
if err := evaluator.EvaluateYamlNode(&job.RawContinueOnError); err != nil {
return nil, fmt.Errorf("evaluate continue-on-error for job %q: %w", id, err)
}
swf := &SingleWorkflow{
Name: workflow.Name,
RawOn: workflow.RawOn,
@@ -58,11 +58,6 @@ func TestParse(t *testing.T) {
options: nil,
wantErr: false,
},
{
name: "continue_on_error_expr",
options: nil,
wantErr: false,
},
}
invalidFileTests := []struct {
name string
+34 -49
View File
@@ -79,37 +79,23 @@ func (w *SingleWorkflow) Marshal() ([]byte, error) {
}
type Job struct {
Name string `yaml:"name,omitempty"`
RawNeeds yaml.Node `yaml:"needs,omitempty"`
RawRunsOn yaml.Node `yaml:"runs-on,omitempty"`
Env yaml.Node `yaml:"env,omitempty"`
If yaml.Node `yaml:"if,omitempty"`
Steps []*Step `yaml:"steps,omitempty"`
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"`
Services map[string]*ContainerSpec `yaml:"services,omitempty"`
Strategy Strategy `yaml:"strategy,omitempty"`
RawContainer yaml.Node `yaml:"container,omitempty"`
Defaults Defaults `yaml:"defaults,omitempty"`
Outputs map[string]string `yaml:"outputs,omitempty"`
Uses string `yaml:"uses,omitempty"`
With map[string]any `yaml:"with,omitempty"`
RawSecrets yaml.Node `yaml:"secrets,omitempty"`
RawConcurrency *model.RawConcurrency `yaml:"concurrency,omitempty"`
RawPermissions yaml.Node `yaml:"permissions,omitempty"`
}
// GetContinueOnError decodes the continue-on-error field to a bool.
// The field may be a literal bool or an already-evaluated expression node.
func (j *Job) GetContinueOnError() bool {
if j.RawContinueOnError.Kind == 0 {
return false
}
var v bool
if err := j.RawContinueOnError.Decode(&v); err != nil {
return false
}
return v
Name string `yaml:"name,omitempty"`
RawNeeds yaml.Node `yaml:"needs,omitempty"`
RawRunsOn yaml.Node `yaml:"runs-on,omitempty"`
Env yaml.Node `yaml:"env,omitempty"`
If yaml.Node `yaml:"if,omitempty"`
Steps []*Step `yaml:"steps,omitempty"`
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
Services map[string]*ContainerSpec `yaml:"services,omitempty"`
Strategy Strategy `yaml:"strategy,omitempty"`
RawContainer yaml.Node `yaml:"container,omitempty"`
Defaults Defaults `yaml:"defaults,omitempty"`
Outputs map[string]string `yaml:"outputs,omitempty"`
Uses string `yaml:"uses,omitempty"`
With map[string]any `yaml:"with,omitempty"`
RawSecrets yaml.Node `yaml:"secrets,omitempty"`
RawConcurrency *model.RawConcurrency `yaml:"concurrency,omitempty"`
RawPermissions yaml.Node `yaml:"permissions,omitempty"`
}
func (j *Job) Clone() *Job {
@@ -117,24 +103,23 @@ func (j *Job) Clone() *Job {
return nil
}
return &Job{
Name: j.Name,
RawNeeds: j.RawNeeds,
RawRunsOn: j.RawRunsOn,
Env: j.Env,
If: j.If,
Steps: j.Steps,
TimeoutMinutes: j.TimeoutMinutes,
RawContinueOnError: j.RawContinueOnError,
Services: j.Services,
Strategy: j.Strategy,
RawContainer: j.RawContainer,
Defaults: j.Defaults,
Outputs: j.Outputs,
Uses: j.Uses,
With: j.With,
RawSecrets: j.RawSecrets,
RawConcurrency: j.RawConcurrency,
RawPermissions: j.RawPermissions,
Name: j.Name,
RawNeeds: j.RawNeeds,
RawRunsOn: j.RawRunsOn,
Env: j.Env,
If: j.If,
Steps: j.Steps,
TimeoutMinutes: j.TimeoutMinutes,
Services: j.Services,
Strategy: j.Strategy,
RawContainer: j.RawContainer,
Defaults: j.Defaults,
Outputs: j.Outputs,
Uses: j.Uses,
With: j.With,
RawSecrets: j.RawSecrets,
RawConcurrency: j.RawConcurrency,
RawPermissions: j.RawPermissions,
}
}

Some files were not shown because too many files have changed in this diff Show More