Merge branch 'main' into main

This commit is contained in:
Karthik Bhandary authored and GitHub committed 2026-04-27 19:18:38 +05:30
commit ce940d2b92
714 files changed
+12752 -7192

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "Gitea DevContainer", "name": "Gitea DevContainer",
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie", "image": "mcr.microsoft.com/devcontainers/go:1.26-trixie",
"containerEnv": { "containerEnv": {
// override "local" from packaged version // override "local" from packaged version
"GOTOOLCHAIN": "auto" "GOTOOLCHAIN": "auto"
+47
View File
@@ -0,0 +1,47 @@
name: go-caches
description: Restore and save go module, build, and golangci-lint caches
inputs:
cache-name:
description: Short identifier used in the per-caller build cache key
required: true
build-cache:
description: Whether to include ~/.cache/go-build
default: "true"
build-cache-rotate:
description: Whether to rotate the build cache key per run so Go's test result cache can accumulate across runs
default: "false"
lint-cache:
description: Whether to include ~/.cache/golangci-lint
default: "false"
runs:
using: composite
steps:
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/go/pkg/mod
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.build-cache == 'true' && inputs.build-cache-rotate == 'true' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/go-build
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}-${{ github.run_id }}
restore-keys: |
gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}
gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}
gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
gobuild-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.build-cache == 'true' && inputs.build-cache-rotate != 'true' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/go-build
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.lint-cache == 'true' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/golangci-lint
key: golangci-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum', '.golangci.yml') }}
restore-keys: golangci-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}
-10
View File
@@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
labels: [modifies/dependencies]
directory: /
schedule:
interval: daily
cooldown:
default-days: 5
+75
View File
@@ -0,0 +1,75 @@
# Populates the go module, build, and golangci-lint caches under the default
# branch's cache scope so that PR runs have a warm fallback to restore from.
#
# GitHub Actions caches are scoped per ref: a PR run can only write to its own
# branch's scope, but can read from the base branch's scope as a fallback.
# PRs therefore cannot seed main's scope themselves. Running the same cache
# steps on push-to-main is the only opportunity to populate that fallback
# scope so fresh PR branches start with a useful cache on first run.
# A PR job's exact key lives in its own PR-scope (empty on first run, filled
# by later runs of the same PR); on miss, actions/cache's restore-keys fall
# back to prefix matches against entries this seeder saves in main's scope.
name: cache-seeder
on:
push:
branches:
- main
paths:
- "go.sum"
- ".golangci.yml"
- ".github/actions/go-cache/action.yml"
- ".github/workflows/cache-seeder.yml"
concurrency:
group: cache-seeder
cancel-in-progress: true
jobs:
gobuild:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: seed
- run: make deps-backend
- run: TAGS="bindata" make backend
- run: TAGS="bindata sqlite sqlite_unlock_notify" make backend
- run: TAGS="bindata gogit sqlite sqlite_unlock_notify" GOEXPERIMENT="" make backend
lint:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- { job: lint-backend, tags: "bindata sqlite sqlite_unlock_notify", target: "lint-backend" }
- { job: lint-go-windows, tags: "bindata sqlite sqlite_unlock_notify", target: "lint-go-windows" }
- { job: lint-go-gogit, tags: "bindata sqlite sqlite_unlock_notify gogit", target: "lint-go" }
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: ${{ matrix.job }}
lint-cache: "true"
- run: make deps-backend deps-tools
- run: make ${{ matrix.target }}
env:
TAGS: ${{ matrix.tags }}
-22
View File
@@ -1,22 +0,0 @@
name: cron-flake-updater
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0' # runs weekly on Sunday at 00:00
jobs:
nix-flake-update:
permissions:
contents: write
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: DeterminateSystems/determinate-nix-action@v3
- uses: DeterminateSystems/update-flake-lock@main
with:
pr-title: "Update Nix flake"
pr-labels: |
dependencies
+3 -3
View File
@@ -12,15 +12,15 @@ jobs:
permissions: permissions:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- run: make generate-gitignore - run: make generate-gitignore
timeout-minutes: 40 timeout-minutes: 40
- name: push translations to repo - name: push translations to repo
uses: appleboy/git-push-action@v1.2.0 uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0
with: with:
author_email: "teabot@gitea.io" author_email: "teabot@gitea.io"
author_name: GiteaBot author_name: GiteaBot
+31
View File
@@ -0,0 +1,31 @@
name: cron-renovate
on:
schedule:
- cron: "0 1 * * *" # daily at 01:00 UTC
workflow_dispatch:
concurrency:
group: cron-renovate
env:
RENOVATE_VERSION: 43.141.5 # renovate: datasource=docker depName=ghcr.io/renovatebot/renovate
jobs:
cron-renovate:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea' # prevent running on forks
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: renovatebot/github-action@83ec54fee49ab67d9cd201084c1ff325b4b462e4 # v46.1.10
with:
renovate-version: ${{ env.RENOVATE_VERSION }}
configurationFile: renovate.json5
token: ${{ secrets.RENOVATE_TOKEN }}
env:
RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks.
RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg)$"]'
RENOVATE_REPOSITORIES: '["go-gitea/gitea"]'
+3 -3
View File
@@ -12,8 +12,8 @@ jobs:
permissions: permissions:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: crowdin/github-action@v2 - uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
with: with:
upload_sources: true upload_sources: true
upload_translations: false upload_translations: false
@@ -29,7 +29,7 @@ jobs:
- name: update locales - name: update locales
run: ./build/update-locales.sh run: ./build/update-locales.sh
- name: push translations to repo - name: push translations to repo
uses: appleboy/git-push-action@v1.2.0 uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0
with: with:
author_email: "teabot@gitea.io" author_email: "teabot@gitea.io"
author_name: GiteaBot author_name: GiteaBot
+2 -2
View File
@@ -42,8 +42,8 @@ jobs:
json: ${{ steps.changes.outputs.json }} json: ${{ steps.changes.outputs.json }}
e2e: ${{ steps.changes.outputs.e2e }} e2e: ${{ steps.changes.outputs.e2e }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dorny/paths-filter@v4 - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes id: changes
with: with:
filters: | filters: |
+58 -32
View File
@@ -20,11 +20,16 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: lint-backend
lint-cache: "true"
- run: make deps-backend deps-tools - run: make deps-backend deps-tools
- run: make lint-backend - run: make lint-backend
env: env:
@@ -37,11 +42,11 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@v8.0.0 - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- run: uv python install 3.14 - run: uv python install 3.14
- uses: pnpm/action-setup@v5 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@@ -57,8 +62,8 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@v8.0.0 - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- run: uv python install 3.14 - run: uv python install 3.14
- run: make deps-py - run: make deps-py
- run: make lint-yaml - run: make lint-yaml
@@ -70,11 +75,13 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@v5 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@v5 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24 node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: make deps-frontend - run: make deps-frontend
- run: make lint-json - run: make lint-json
@@ -85,9 +92,9 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@v5 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@@ -102,8 +109,8 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -116,11 +123,16 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: lint-go-windows
lint-cache: "true"
- run: make deps-backend deps-tools - run: make deps-backend deps-tools
- run: make lint-go-windows - run: make lint-go-windows
env: env:
@@ -135,11 +147,16 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: lint-go-gogit
lint-cache: "true"
- run: make deps-backend deps-tools - run: make deps-backend deps-tools
- run: make lint-go - run: make lint-go
env: env:
@@ -152,11 +169,16 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: checks-backend
build-cache: "false"
- run: make deps-backend deps-tools - run: make deps-backend deps-tools
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs - run: make --always-make checks-backend # ensure the "go-licenses" make target runs
@@ -167,9 +189,9 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@v5 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@@ -187,11 +209,15 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: backend
# no frontend build here as backend should be able to build # no frontend build here as backend should be able to build
# even without any frontend files # even without any frontend files
- run: make deps-backend - run: make deps-backend
@@ -221,9 +247,9 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@v5 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@@ -238,8 +264,8 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
+35 -14
View File
@@ -42,11 +42,15 @@ jobs:
ports: ports:
- "9000:9000" - "9000:9000"
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: pgsql
- name: Add hosts to /etc/hosts - name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts' run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
- run: make deps-backend - run: make deps-backend
@@ -60,7 +64,6 @@ jobs:
timeout-minutes: 50 timeout-minutes: 50
env: env:
TAGS: bindata gogit TAGS: bindata gogit
RACE_ENABLED: true
TEST_TAGS: gogit TEST_TAGS: gogit
TEST_LDAP: 1 TEST_LDAP: 1
@@ -71,11 +74,15 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: sqlite
- run: make deps-backend - run: make deps-backend
- run: GOEXPERIMENT='' make backend - run: GOEXPERIMENT='' make backend
env: env:
@@ -130,11 +137,16 @@ jobs:
ports: ports:
- 10000:10000 - 10000:10000
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: unit
build-cache-rotate: "true"
- name: Add hosts to /etc/hosts - 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' run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
- run: make deps-backend - run: make deps-backend
@@ -142,16 +154,18 @@ jobs:
env: env:
TAGS: bindata TAGS: bindata
- name: unit-tests - name: unit-tests
run: make unit-test-coverage test-check run: make test-backend test-check
env: env:
TAGS: bindata TAGS: bindata
RACE_ENABLED: true RACE_ENABLED: true
GOTESTFLAGS: -timeout=20m
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
- name: unit-tests-gogit - name: unit-tests-gogit
run: GOEXPERIMENT='' make unit-test-coverage test-check run: GOEXPERIMENT='' make test-backend test-check
env: env:
TAGS: bindata gogit TAGS: bindata gogit
RACE_ENABLED: true RACE_ENABLED: true
GOTESTFLAGS: -timeout=20m
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
test-mysql: test-mysql:
@@ -185,11 +199,15 @@ jobs:
- "587:587" - "587:587"
- "993:993" - "993:993"
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: mysql
- name: Add hosts to /etc/hosts - 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' run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
- run: make deps-backend - run: make deps-backend
@@ -203,7 +221,6 @@ jobs:
run: make test-mysql run: make test-mysql
env: env:
TAGS: bindata TAGS: bindata
RACE_ENABLED: true
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
test-mssql: test-mssql:
@@ -226,11 +243,15 @@ jobs:
ports: ports:
- 10000:10000 - 10000:10000
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
cache: false
- uses: ./.github/actions/go-cache
with:
cache-name: mssql
- name: Add hosts to /etc/hosts - 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' run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
- run: make deps-backend - run: make deps-backend
+5 -5
View File
@@ -20,18 +20,18 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: docker/setup-qemu-action@v4 - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@v4 - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build regular container image - name: Build regular container image
uses: docker/build-push-action@v7 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64
push: false push: false
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
- name: Build rootless container image - name: Build rootless container image
uses: docker/build-push-action@v7 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
context: . context: .
push: false push: false
+9 -4
View File
@@ -20,13 +20,18 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- uses: pnpm/action-setup@v5 cache: false
- uses: actions/setup-node@v6 - uses: ./.github/actions/go-cache
with:
cache-name: e2e
build-cache: "false"
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
+1 -1
View File
@@ -15,6 +15,6 @@ jobs:
contents: read contents: read
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/labeler@v6 - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with: with:
sync-labels: true sync-labels: true
+15 -15
View File
@@ -14,16 +14,16 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # 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 # 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 - run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- uses: pnpm/action-setup@v5 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@@ -35,7 +35,7 @@ jobs:
TAGS: bindata sqlite sqlite_unlock_notify TAGS: bindata sqlite sqlite_unlock_notify
- name: import gpg key - name: import gpg key
id: import_gpg id: import_gpg
uses: crazy-max/ghaction-import-gpg@v7 uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
with: with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }} gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
@@ -52,7 +52,7 @@ jobs:
echo "Cleaned name is ${REF_NAME}" echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- name: configure aws - name: configure aws
uses: aws-actions/configure-aws-credentials@v6 uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with: with:
aws-region: ${{ secrets.AWS_REGION }} aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -67,18 +67,18 @@ jobs:
contents: read contents: read
packages: write # to publish to ghcr.io packages: write # to publish to ghcr.io
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # 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 # 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 - run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@v4 - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@v4 - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Get cleaned branch name - name: Get cleaned branch name
id: clean_name id: clean_name
run: | run: |
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- uses: docker/metadata-action@v6 - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta id: meta
with: with:
images: |- images: |-
@@ -88,7 +88,7 @@ jobs:
type=raw,value=${{ steps.clean_name.outputs.branch }} type=raw,value=${{ steps.clean_name.outputs.branch }}
annotations: | annotations: |
org.opencontainers.image.authors="maintainers@gitea.io" org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@v6 - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta_rootless id: meta_rootless
with: with:
images: |- images: |-
@@ -102,18 +102,18 @@ jobs:
annotations: | annotations: |
org.opencontainers.image.authors="maintainers@gitea.io" org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v4 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT - name: Login to GHCR using PAT
uses: docker/login-action@v4 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular docker image - name: build regular docker image
uses: docker/build-push-action@v7 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64
@@ -123,7 +123,7 @@ jobs:
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
- name: build rootless docker image - name: build rootless docker image
uses: docker/build-push-action@v7 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64
+16 -16
View File
@@ -15,16 +15,16 @@ jobs:
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # 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 # 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 - run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- uses: pnpm/action-setup@v5 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@@ -36,7 +36,7 @@ jobs:
TAGS: bindata sqlite sqlite_unlock_notify TAGS: bindata sqlite sqlite_unlock_notify
- name: import gpg key - name: import gpg key
id: import_gpg id: import_gpg
uses: crazy-max/ghaction-import-gpg@v7 uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
with: with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }} gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
@@ -53,7 +53,7 @@ jobs:
echo "Cleaned name is ${REF_NAME}" echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws - name: configure aws
uses: aws-actions/configure-aws-credentials@v6 uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with: with:
aws-region: ${{ secrets.AWS_REGION }} aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -62,7 +62,7 @@ jobs:
run: | run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
- name: Install GH CLI - name: Install GH CLI
uses: dev-hanz-ops/install-gh-cli-action@v0.2.1 uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
with: with:
gh-cli-version: 2.39.1 gh-cli-version: 2.39.1
- name: create github release - name: create github release
@@ -77,13 +77,13 @@ jobs:
contents: read contents: read
packages: write # to publish to ghcr.io packages: write # to publish to ghcr.io
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # 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 # 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 - run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@v4 - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@v4 - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/metadata-action@v6 - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta id: meta
with: with:
images: |- images: |-
@@ -96,7 +96,7 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
annotations: | annotations: |
org.opencontainers.image.authors="maintainers@gitea.io" org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@v6 - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta_rootless id: meta_rootless
with: with:
images: |- images: |-
@@ -112,18 +112,18 @@ jobs:
annotations: | annotations: |
org.opencontainers.image.authors="maintainers@gitea.io" org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v4 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT - name: Login to GHCR using PAT
uses: docker/login-action@v4 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image - name: build regular container image
uses: docker/build-push-action@v7 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64
@@ -131,7 +131,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }} annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image - name: build rootless container image
uses: docker/build-push-action@v7 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64
+16 -16
View File
@@ -18,16 +18,16 @@ jobs:
contents: read contents: read
packages: write # to publish to ghcr.io packages: write # to publish to ghcr.io
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # 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 # 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 - run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v6 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- uses: pnpm/action-setup@v5 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@@ -39,7 +39,7 @@ jobs:
TAGS: bindata sqlite sqlite_unlock_notify TAGS: bindata sqlite sqlite_unlock_notify
- name: import gpg key - name: import gpg key
id: import_gpg id: import_gpg
uses: crazy-max/ghaction-import-gpg@v7 uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
with: with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }} gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
@@ -56,7 +56,7 @@ jobs:
echo "Cleaned name is ${REF_NAME}" echo "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: configure aws - name: configure aws
uses: aws-actions/configure-aws-credentials@v6 uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with: with:
aws-region: ${{ secrets.AWS_REGION }} aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -65,7 +65,7 @@ jobs:
run: | run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
- name: Install GH CLI - name: Install GH CLI
uses: dev-hanz-ops/install-gh-cli-action@v0.2.1 uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1
with: with:
gh-cli-version: 2.39.1 gh-cli-version: 2.39.1
- name: create github release - name: create github release
@@ -80,13 +80,13 @@ jobs:
contents: read contents: read
packages: write # to publish to ghcr.io packages: write # to publish to ghcr.io
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # 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 # 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 - run: git fetch --unshallow --quiet --tags --force
- uses: docker/setup-qemu-action@v4 - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@v4 - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/metadata-action@v6 - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta id: meta
with: with:
images: |- images: |-
@@ -103,7 +103,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
annotations: | annotations: |
org.opencontainers.image.authors="maintainers@gitea.io" org.opencontainers.image.authors="maintainers@gitea.io"
- uses: docker/metadata-action@v6 - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta_rootless id: meta_rootless
with: with:
images: |- images: |-
@@ -124,18 +124,18 @@ jobs:
annotations: | annotations: |
org.opencontainers.image.authors="maintainers@gitea.io" org.opencontainers.image.authors="maintainers@gitea.io"
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v4 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT - name: Login to GHCR using PAT
uses: docker/login-action@v4 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: build regular container image - name: build regular container image
uses: docker/build-push-action@v7 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64
@@ -143,7 +143,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }} annotations: ${{ steps.meta.outputs.annotations }}
- name: build rootless container image - name: build rootless container image
uses: docker/build-push-action@v7 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/riscv64 platforms: linux/amd64,linux/arm64,linux/riscv64
+3
View File
@@ -2,6 +2,9 @@
- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them - Run `make fmt` to format `.go` files, and run `make lint-go` to lint them
- Run `make lint-js` to lint `.ts` files - Run `make lint-js` to lint `.ts` files
- Run `make tidy` after any `go.mod` changes - Run `make tidy` after any `go.mod` changes
- Run single go tests with `go test -tags 'sqlite sqlite_unlock_notify' -run '^TestName$' ./modulepath/`
- Run single js test files with `pnpm exec vitest <path-filter>`
- Run single playwright e2e test files with `GITEA_TEST_E2E_FLAGS='<filepath>' make test-e2e`
- Add the current year into the copyright header of new `.go` files - Add the current year into the copyright header of new `.go` files
- Ensure no trailing whitespace in edited files - Ensure no trailing whitespace in edited files
- Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates - Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates
+22
View File
@@ -4,6 +4,28 @@ 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 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). been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.26.1](https://github.com/go-gitea/gitea/releases/tag/v1.26.1) - 2026-04-21
* BUGFIXES
* Add event.schedule context for schedule actions task (#37320) (#37348)
* Fix an issue where changing an organization's visibility caused problems when users had forked its repositories. (#37324) (#37344)
* Use modern "git update-index --cacheinfo" syntax to support more file names (#37338) (#37343)
* Fix URL related escaping for oauth2 (#37334) (#37340)
* When the requested arch rpm is missing fall back to noarch (#37236) (#37339)
* Fix actions concurrency groups cross-branch leak (#37311) (#37331)
* Fix bug when accessing user badges (#37321) (#37329)
* Fix AppFullLink (#37325) (#37328)
* Fix container auth for public instance (#37290) (#37294)
* Enhance GetActionWorkflow to support fallback references (#37189) (#37283)
* Fix vite manifest update masking build errors (#37279) (#37310)
* Fix Mermaid diagrams failing when node labels contain line breaks (#37296) (#37299)
* Use TriggerEvent instead of Event in workflow runs API response for scheduled runs (#37288) #37360
* Add URL to Learn more about blocking a user. (#37355) #37367
* Fix button layout shift when collapsing file tree in editor (#37363) #37375
* Fix org team assignee/reviewer lookups for team member permissions (#37365) #37391
* Fix repo init README EOL (#37388) #37399
* Fix: dump with default zip type produces uncompressed zip (#37401) #37402
## [1.26.0](https://github.com/go-gitea/gitea/releases/tag/v1.26.0) - 2026-04-17 ## [1.26.0](https://github.com/go-gitea/gitea/releases/tag/v1.26.0) - 2026-04-17
* BREAKING * BREAKING
+10 -9
View File
@@ -12,15 +12,15 @@ COMMA := ,
XGO_VERSION := go-1.25.x XGO_VERSION := go-1.25.x
AIR_PACKAGE ?= github.com/air-verse/air@v1 AIR_PACKAGE ?= github.com/air-verse/air@v1 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 # renovate: datasource=go
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 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 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.33.1 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 # renovate: datasource=go
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.11 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.11 # renovate: datasource=go
DOCKER_IMAGE ?= gitea/gitea DOCKER_IMAGE ?= gitea/gitea
DOCKER_TAG ?= latest DOCKER_TAG ?= latest
@@ -518,7 +518,8 @@ test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test
.PHONY: playwright .PHONY: playwright
playwright: deps-frontend playwright: deps-frontend
@pnpm exec playwright install --with-deps chromium firefox webkit $(PLAYWRIGHT_FLAGS) @# on GitHub Actions VMs, playwright's system deps are pre-installed
@pnpm exec playwright install $(if $(GITHUB_ACTIONS),,--with-deps) chromium firefox $(PLAYWRIGHT_FLAGS)
.PHONY: test-e2e .PHONY: test-e2e
test-e2e: playwright $(EXECUTABLE_E2E) test-e2e: playwright $(EXECUTABLE_E2E)
+4 -1
View File
@@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"io"
"testing" "testing"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@@ -82,7 +83,9 @@ func TestChangePasswordCommand(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
err := microcmdUserChangePassword().Run(ctx, tc.args) cmd := microcmdUserChangePassword()
cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard
err := cmd.Run(ctx, tc.args)
require.Error(t, err) require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErr) require.Contains(t, err.Error(), tc.expectedErr)
}) })
+2
View File
@@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"io"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -107,6 +108,7 @@ func TestCertCommandFailures(t *testing.T) {
for _, c := range cases { for _, c := range cases {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
app := cmdCert() app := cmdCert()
app.Writer, app.ErrWriter = io.Discard, io.Discard
tempDir := t.TempDir() tempDir := t.TempDir()
certFile := filepath.Join(tempDir, "cert.pem") certFile := filepath.Join(tempDir, "cert.pem")
-38
View File
@@ -1,38 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
func TestDefaultCommand(t *testing.T) {
test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) {
called := false
cmd := &cli.Command{
DefaultCommand: "test",
Commands: []*cli.Command{
{
Name: "test",
Action: func(ctx context.Context, command *cli.Command) error {
retName, retValid := isValidDefaultSubCommand(command)
assert.Equal(t, expectedRetName, retName)
assert.Equal(t, expectedRetValid, retValid)
called = true
return nil
},
},
},
}
assert.NoError(t, cmd.Run(t.Context(), args))
assert.True(t, called)
}
test(t, []string{"./gitea"}, "", true)
test(t, []string{"./gitea", "test"}, "", true)
test(t, []string{"./gitea", "other"}, "other", false)
}
+237
View File
@@ -0,0 +1,237 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Tests here reload the config system multiple times with uncontrollable details.
// So they must be in a separate package, to avoid affecting other tests
package cmdtest
import (
"context"
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"testing"
"code.gitea.io/gitea/cmd"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}
func makePathOutput(workPath, customPath, customConf string) string {
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
}
func newTestApp(testCmd cli.Command) *cli.Command {
app := cmd.NewMainApp(cmd.AppVersion{})
testCmd.Name = util.IfZero(testCmd.Name, "test-cmd")
cmd.PrepareSubcommandWithGlobalFlags(&testCmd)
app.Commands = append(app.Commands, &testCmd)
app.DefaultCommand = testCmd.Name
return app
}
type runResult struct {
Stdout string
Stderr string
ExitCode int
}
func runTestApp(app *cli.Command, args ...string) (runResult, error) {
outBuf := new(strings.Builder)
errBuf := new(strings.Builder)
app.Writer = outBuf
app.ErrWriter = errBuf
exitCode := -1
defer test.MockVariableValue(&cli.ErrWriter, app.ErrWriter)()
defer test.MockVariableValue(&cli.OsExiter, func(code int) {
if exitCode == -1 {
exitCode = code // save the exit code once and then reset the writer (to simulate the exit)
app.Writer, app.ErrWriter, cli.ErrWriter = io.Discard, io.Discard, io.Discard
}
})()
err := cmd.RunMainApp(app, args...)
return runResult{outBuf.String(), errBuf.String(), exitCode}, err
}
func TestCliCmd(t *testing.T) {
defaultWorkPath := filepath.FromSlash("/tmp/mocked-work-path")
defaultCustomPath := filepath.Join(defaultWorkPath, "custom")
defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
defer setting.MockBuiltinPaths(defaultWorkPath, "", "")()
cli.CommandHelpTemplate = "(command help template)"
cli.RootCommandHelpTemplate = "(app help template)"
cli.SubcommandHelpTemplate = "(subcommand help template)"
cases := []struct {
env map[string]string
cmd string
exp string
}{
// help commands
{
cmd: "./gitea -h",
exp: "DEFAULT CONFIGURATION:",
},
{
cmd: "./gitea help",
exp: "DEFAULT CONFIGURATION:",
},
{
cmd: "./gitea -c /dev/null -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea help -c /dev/null",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null test-cmd -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -c /dev/null -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -h -c /dev/null",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null test-cmd help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -c /dev/null help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd help -c /dev/null",
exp: "ConfigFile: /dev/null",
},
// parse paths
{
cmd: "./gitea test-cmd",
exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf),
},
{
cmd: "./gitea -c /tmp/app.ini test-cmd",
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
},
{
cmd: "./gitea test-cmd -c /tmp/app.ini",
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
},
{
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmd: "./gitea test-cmd",
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"),
},
{
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmd: "./gitea test-cmd --work-path /tmp/other",
exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"),
},
{
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmd: "./gitea test-cmd --config /tmp/app-other.ini",
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"),
},
}
for _, c := range cases {
t.Run(c.cmd, func(t *testing.T) {
app := newTestApp(cli.Command{
Action: func(ctx context.Context, cmd *cli.Command) error {
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
return nil
},
})
for k, v := range c.env {
t.Setenv(k, v)
}
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
r, err := runTestApp(app, args...)
assert.NoError(t, err, c.cmd)
assert.NotEmpty(t, c.exp, c.cmd)
if !assert.Contains(t, r.Stdout, c.exp, c.cmd) {
t.Log("Full output:\n" + r.Stdout)
t.Log("Expected:\n" + c.exp)
}
})
}
}
func TestCliCmdError(t *testing.T) {
app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }})
r, err := runTestApp(app, "./gitea", "test-cmd")
assert.Error(t, err)
assert.Equal(t, 1, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "Command error: normal error\n", r.Stderr)
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }})
r, err = runTestApp(app, "./gitea", "test-cmd")
assert.Error(t, err)
assert.Equal(t, 2, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "exit error\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", "--no-such")
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\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")
assert.NoError(t, err)
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
assert.Empty(t, r.Stdout)
assert.Empty(t, r.Stderr)
}
func TestCliCmdBefore(t *testing.T) {
ctxNew := context.WithValue(context.Background(), any("key"), "value")
configValues := map[string]string{}
setting.CustomConf = "/tmp/any.ini"
var actionCtx context.Context
app := newTestApp(cli.Command{
Before: func(context.Context, *cli.Command) (context.Context, error) {
configValues["before"] = setting.CustomConf
return ctxNew, nil
},
Action: func(ctx context.Context, cmd *cli.Command) error {
configValues["action"] = setting.CustomConf
actionCtx = ctx
return nil
},
})
_, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd")
assert.NoError(t, err)
assert.Equal(t, ctxNew, actionCtx)
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
assert.Equal(t, "/dev/null", configValues["action"])
}
+1 -1
View File
@@ -124,7 +124,7 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cl
if setting.InstallLock { if setting.InstallLock {
// During config loading, there might also be logs (for example: deprecation warnings). // During config loading, there might also be logs (for example: deprecation warnings).
// It must make sure that console logger is set up before config is loaded. // It must make sure that console logger is set up before config is loaded.
log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it.") log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it. CustomConf=%s", setting.CustomConf)
return nil, errors.New("console logger must be setup before config is loaded") return nil, errors.New("console logger must be setup before config is loaded")
} }
level := defaultLevel level := defaultLevel
+2 -2
View File
@@ -48,7 +48,7 @@ DEFAULT CONFIGURATION:
} }
} }
func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) { func PrepareSubcommandWithGlobalFlags(originCmd *cli.Command) {
originBefore := originCmd.Before originBefore := originCmd.Before
originCmd.Before = func(ctxOrig context.Context, cmd *cli.Command) (ctx context.Context, err error) { originCmd.Before = func(ctxOrig context.Context, cmd *cli.Command) (ctx context.Context, err error) {
ctx = ctxOrig ctx = ctxOrig
@@ -145,7 +145,7 @@ func NewMainApp(appVer AppVersion) *cli.Command {
app.Before = PrepareConsoleLoggerLevel(log.INFO) app.Before = PrepareConsoleLoggerLevel(log.INFO)
for i := range subCmdWithConfig { for i := range subCmdWithConfig {
prepareSubcommandWithGlobalFlags(subCmdWithConfig[i]) PrepareSubcommandWithGlobalFlags(subCmdWithConfig[i])
} }
app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdWithConfig...)
app.Commands = append(app.Commands, subCmdStandalone...) app.Commands = append(app.Commands, subCmdStandalone...)
+22 -211
View File
@@ -5,17 +5,9 @@ package cmd
import ( import (
"context" "context"
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"testing" "testing"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
@@ -25,209 +17,28 @@ func TestMain(m *testing.M) {
unittest.MainTest(m) unittest.MainTest(m)
} }
func makePathOutput(workPath, customPath, customConf string) string { func TestDefaultCommand(t *testing.T) {
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf) test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) {
} called := false
cmd := &cli.Command{
func newTestApp(testCmd cli.Command) *cli.Command { DefaultCommand: "test",
app := NewMainApp(AppVersion{}) Commands: []*cli.Command{
testCmd.Name = util.IfZero(testCmd.Name, "test-cmd") {
prepareSubcommandWithGlobalFlags(&testCmd) Name: "test",
app.Commands = append(app.Commands, &testCmd) Action: func(ctx context.Context, command *cli.Command) error {
app.DefaultCommand = testCmd.Name retName, retValid := isValidDefaultSubCommand(command)
return app assert.Equal(t, expectedRetName, retName)
} assert.Equal(t, expectedRetValid, retValid)
called = true
type runResult struct { return nil
Stdout string },
Stderr string
ExitCode int
}
func runTestApp(app *cli.Command, args ...string) (runResult, error) {
outBuf := new(strings.Builder)
errBuf := new(strings.Builder)
app.Writer = outBuf
app.ErrWriter = errBuf
exitCode := -1
defer test.MockVariableValue(&cli.ErrWriter, app.ErrWriter)()
defer test.MockVariableValue(&cli.OsExiter, func(code int) {
if exitCode == -1 {
exitCode = code // save the exit code once and then reset the writer (to simulate the exit)
app.Writer, app.ErrWriter, cli.ErrWriter = io.Discard, io.Discard, io.Discard
}
})()
err := RunMainApp(app, args...)
return runResult{outBuf.String(), errBuf.String(), exitCode}, err
}
func TestCliCmd(t *testing.T) {
defaultWorkPath := filepath.Dir(setting.AppPath)
defaultCustomPath := filepath.Join(defaultWorkPath, "custom")
defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
cli.CommandHelpTemplate = "(command help template)"
cli.RootCommandHelpTemplate = "(app help template)"
cli.SubcommandHelpTemplate = "(subcommand help template)"
cases := []struct {
env map[string]string
cmd string
exp string
}{
// help commands
{
cmd: "./gitea -h",
exp: "DEFAULT CONFIGURATION:",
},
{
cmd: "./gitea help",
exp: "DEFAULT CONFIGURATION:",
},
{
cmd: "./gitea -c /dev/null -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea help -c /dev/null",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null test-cmd -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -c /dev/null -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -h -c /dev/null",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null test-cmd help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -c /dev/null help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd help -c /dev/null",
exp: "ConfigFile: /dev/null",
},
// parse paths
{
cmd: "./gitea test-cmd",
exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf),
},
{
cmd: "./gitea -c /tmp/app.ini test-cmd",
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
},
{
cmd: "./gitea test-cmd -c /tmp/app.ini",
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
},
{
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmd: "./gitea test-cmd",
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"),
},
{
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmd: "./gitea test-cmd --work-path /tmp/other",
exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"),
},
{
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmd: "./gitea test-cmd --config /tmp/app-other.ini",
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"),
},
}
for _, c := range cases {
t.Run(c.cmd, func(t *testing.T) {
defer test.MockVariableValue(&setting.InstallLock, false)()
app := newTestApp(cli.Command{
Action: func(ctx context.Context, cmd *cli.Command) error {
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
return nil
}, },
}) },
for k, v := range c.env { }
t.Setenv(k, v) assert.NoError(t, cmd.Run(t.Context(), args))
} assert.True(t, called)
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
r, err := runTestApp(app, args...)
assert.NoError(t, err, c.cmd)
assert.NotEmpty(t, c.exp, c.cmd)
if !assert.Contains(t, r.Stdout, c.exp, c.cmd) {
t.Log("Full output:\n" + r.Stdout)
t.Log("Expected:\n" + c.exp)
}
})
} }
} test(t, []string{"./gitea"}, "", true)
test(t, []string{"./gitea", "test"}, "", true)
func TestCliCmdError(t *testing.T) { test(t, []string{"./gitea", "other"}, "other", false)
app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }})
r, err := runTestApp(app, "./gitea", "test-cmd")
assert.Error(t, err)
assert.Equal(t, 1, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "Command error: normal error\n", r.Stderr)
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }})
r, err = runTestApp(app, "./gitea", "test-cmd")
assert.Error(t, err)
assert.Equal(t, 2, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "exit error\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", "--no-such")
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\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")
assert.NoError(t, err)
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
assert.Empty(t, r.Stdout)
assert.Empty(t, r.Stderr)
}
func TestCliCmdBefore(t *testing.T) {
ctxNew := context.WithValue(context.Background(), any("key"), "value")
configValues := map[string]string{}
setting.CustomConf = "/tmp/any.ini"
var actionCtx context.Context
app := newTestApp(cli.Command{
Before: func(context.Context, *cli.Command) (context.Context, error) {
configValues["before"] = setting.CustomConf
return ctxNew, nil
},
Action: func(ctx context.Context, cmd *cli.Command) error {
configValues["action"] = setting.CustomConf
actionCtx = ctx
return nil
},
})
_, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd")
assert.NoError(t, err)
assert.Equal(t, ctxNew, actionCtx)
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
assert.Equal(t, "/dev/null", configValues["action"])
} }
+11 -1
View File
@@ -525,8 +525,11 @@ INTERNAL_TOKEN =
;; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to repositories via API or web. ;; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to repositories via API or web.
;TWO_FACTOR_AUTH = ;TWO_FACTOR_AUTH =
;; ;;
;; The value of the X-Frame-Options HTTP header for HTML responses. Use "unset" to remove the header. ;; The value of the X-Frame-Options HTTP header for all responses. Use "unset" to remove the header.
;X_FRAME_OPTIONS = SAMEORIGIN ;X_FRAME_OPTIONS = SAMEORIGIN
;;
;; The value of the X-Content-Type-Options HTTP header for all responses. Use "unset" to remove the header.
;X_CONTENT_TYPE_OPTIONS = nosniff
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -592,6 +595,11 @@ ENABLED = true
;; * https://github.com/git-ecosystem/git-credential-manager ;; * https://github.com/git-ecosystem/git-credential-manager
;; * https://gitea.com/gitea/tea ;; * https://gitea.com/gitea/tea
;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea ;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea
;;
;; By default, OAuth2 applications can only use "http" and "https" as their redirect URI schemes.
;; If you need to use other schemes (e.g. for desktop applications), you can specify them here as a comma-separated list.
;; For example: set "my-scheme, com.example.app" to support "my-scheme://..." and "com.example.app://..." redirect URIs.
;CUSTOM_SCHEMES =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2968,6 +2976,8 @@ LEVEL = Info
;; Comma-separated list of workflow directories, the first one to exist ;; Comma-separated list of workflow directories, the first one to exist
;; in a repo is used to find Actions workflow files ;; in a repo is used to find Actions workflow files
;WORKFLOW_DIRS = .gitea/workflows,.github/workflows ;WORKFLOW_DIRS = .gitea/workflows,.github/workflows
;; Maximum number of attempts a single workflow run can have. Default value is 50.
;MAX_RERUN_ATTEMPTS = 50
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1776169885, "lastModified": 1776877367,
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57",
"type": "github" "type": "github"
}, },
"original": { "original": {
+10 -10
View File
@@ -27,8 +27,8 @@ require (
github.com/PuerkitoBio/goquery v1.12.0 github.com/PuerkitoBio/goquery v1.12.0
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0
github.com/alecthomas/chroma/v2 v2.23.1 github.com/alecthomas/chroma/v2 v2.23.1
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 github.com/aws/aws-sdk-go-v2/credentials v1.19.15
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.12 github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.13
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.5.7 github.com/blevesearch/bleve/v2 v2.5.7
github.com/bohde/codel v0.2.0 github.com/bohde/codel v0.2.0
@@ -37,7 +37,7 @@ require (
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21 github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21
github.com/chi-middleware/proxy v1.1.1 github.com/chi-middleware/proxy v1.1.1
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
github.com/dlclark/regexp2 v1.11.5 github.com/dlclark/regexp2 v1.12.0
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/editorconfig/editorconfig-core-go/v2 v2.6.4 github.com/editorconfig/editorconfig-core-go/v2 v2.6.4
@@ -49,14 +49,14 @@ require (
github.com/gliderlabs/ssh v0.3.8 github.com/gliderlabs/ssh v0.3.8
github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/cors v1.2.2 github.com/go-chi/cors v1.2.2
github.com/go-co-op/gocron/v2 v2.20.0 github.com/go-co-op/gocron/v2 v2.21.0
github.com/go-enry/go-enry/v2 v2.9.6 github.com/go-enry/go-enry/v2 v2.9.6
github.com/go-git/go-billy/v5 v5.8.0 github.com/go-git/go-billy/v5 v5.8.0
github.com/go-git/go-git/v5 v5.18.0 github.com/go-git/go-git/v5 v5.18.0
github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-redsync/redsync/v4 v4.16.0 github.com/go-redsync/redsync/v4 v4.16.0
github.com/go-sql-driver/mysql v1.9.3 github.com/go-sql-driver/mysql v1.9.3
github.com/go-webauthn/webauthn v0.16.4 github.com/go-webauthn/webauthn v0.16.5
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-jwt/jwt/v5 v5.3.1
@@ -139,10 +139,10 @@ require (
github.com/andybalholm/brotli v1.2.1 // indirect github.com/andybalholm/brotli v1.2.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect github.com/aws/aws-sdk-go-v2 v1.41.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 // indirect
github.com/aws/smithy-go v1.24.2 // indirect github.com/aws/smithy-go v1.25.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect
@@ -258,7 +258,7 @@ require (
github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/tinylib/msgp v1.6.3 // indirect github.com/tinylib/msgp v1.6.4 // indirect
github.com/unknwon/com v1.0.1 // indirect github.com/unknwon/com v1.0.1 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
+20 -20
View File
@@ -92,18 +92,18 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ=
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI= github.com/aws/aws-sdk-go-v2/credentials v1.19.15 h1:fyvgWTszojq8hEnMi8PPBTvZdTtEVmAVyo+NFLHBhH4=
github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w= github.com/aws/aws-sdk-go-v2/credentials v1.19.15/go.mod h1:gJiYyMOjNg8OEdRWOf3CrFQxM2a98qmrtjx1zuiQfB8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 h1:GmLa5Kw1ESqtFpXsx5MmC84QWa/ZrLZvlJGa2y+4kcQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22/go.mod h1:6sW9iWm9DK9YRpRGga/qzrzNLgKpT2cIxb7Vo2eNOp0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 h1:dY4kWZiSaXIzxnKlj17nHnBcXXBfac6UlsAx2qL6XrU=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22/go.mod h1:KIpEUx0JuRZLO7U6cbV204cWAEco2iC3l061IxlwLtI=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.12 h1:yv3mfWt/eiDTTry6fkN5hh8wHJfU5ygnw+DJp10C0/c= github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.13 h1:IIW5QmNI9PrnDTBCPa75HcD0g+hoD/a+d388dbIAkEM=
github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.12/go.mod h1:voO3LP/dZ4CTERiNWCz3DFLbK/8hbfeC1OJkLW+sang= github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.13/go.mod h1:B1FUSufCQp3d8VUzob1EY+AdSo2OuEWNEY6GQff7EHA=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -239,8 +239,8 @@ github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21/go.mod h1:xJvkyD6Y2r
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= 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/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= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@@ -288,8 +288,8 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-co-op/gocron/v2 v2.20.0 h1:9IMrnnVSWjfSh3E54gWmWCHbloQJLh6f9+nwyKfLNpc= github.com/go-co-op/gocron/v2 v2.21.0 h1:e1nt9AEFglarRH9/9y9q0V5sblwxlknpHPjttEajrwQ=
github.com/go-co-op/gocron/v2 v2.20.0/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U= github.com/go-co-op/gocron/v2 v2.21.0/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
github.com/go-enry/go-enry/v2 v2.9.6 h1:np63eOtMV56zfYDHnFVgpEVOk8fr2kmylcMnAZUDbSs= github.com/go-enry/go-enry/v2 v2.9.6 h1:np63eOtMV56zfYDHnFVgpEVOk8fr2kmylcMnAZUDbSs=
github.com/go-enry/go-enry/v2 v2.9.6/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= github.com/go-enry/go-enry/v2 v2.9.6/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
@@ -325,8 +325,8 @@ github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.16.4 h1:R9jqR/cYZa7hRquFF7Za/8qoH/K/TIs1/Q/4CyGN+1Q= github.com/go-webauthn/webauthn v0.16.5 h1:x+vADHlaiIjta23kGhtwyCIlB5mayKx6SBlpwQ5NF9A=
github.com/go-webauthn/webauthn v0.16.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4= github.com/go-webauthn/webauthn v0.16.5/go.mod h1:mQC6L0lZ5Kiu35G70zeB2WnrW4+vbHjR8Koq4HdVaMg=
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA= github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA=
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk= github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
@@ -695,8 +695,8 @@ github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKN
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ= github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo= github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+38 -13
View File
@@ -12,6 +12,7 @@ import (
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@@ -61,7 +62,8 @@ const (
// ActionArtifact is a file that is stored in the artifact storage. // ActionArtifact is a file that is stored in the artifact storage.
type ActionArtifact struct { type ActionArtifact struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
RunID int64 `xorm:"index unique(runid_name_path)"` // The run id of the artifact RunID int64 `xorm:"index unique(runid_attempt_name_path)"` // The run id of the artifact
RunAttemptID int64 `xorm:"index unique(runid_attempt_name_path) NOT NULL DEFAULT 0"`
RunnerID int64 RunnerID int64
RepoID int64 `xorm:"index"` RepoID int64 `xorm:"index"`
OwnerID int64 OwnerID int64
@@ -80,9 +82,9 @@ type ActionArtifact struct {
// * "application/pdf", "text/html", etc.: real content type of the artifact // * "application/pdf", "text/html", etc.: real content type of the artifact
ContentEncodingOrType string `xorm:"content_encoding"` ContentEncodingOrType string `xorm:"content_encoding"`
ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it ArtifactPath string `xorm:"index unique(runid_attempt_name_path)"` // The path to the artifact when runner uploads it
ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it ArtifactName string `xorm:"index unique(runid_attempt_name_path)"` // The name of the artifact when runner uploads it
Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete
CreatedUnix timeutil.TimeStamp `xorm:"created"` CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired
@@ -92,12 +94,13 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa
if err := t.LoadJob(ctx); err != nil { if err := t.LoadJob(ctx); err != nil {
return nil, err return nil, err
} }
artifact, err := getArtifactByNameAndPath(ctx, t.Job.RunID, artifactName, artifactPath) artifact, err := getArtifactByNameAndPath(ctx, t.Job.RunID, t.Job.RunAttemptID, artifactName, artifactPath)
if errors.Is(err, util.ErrNotExist) { if errors.Is(err, util.ErrNotExist) {
artifact := &ActionArtifact{ artifact := &ActionArtifact{
ArtifactName: artifactName, ArtifactName: artifactName,
ArtifactPath: artifactPath, ArtifactPath: artifactPath,
RunID: t.Job.RunID, RunID: t.Job.RunID,
RunAttemptID: t.Job.RunAttemptID,
RunnerID: t.RunnerID, RunnerID: t.RunnerID,
RepoID: t.RepoID, RepoID: t.RepoID,
OwnerID: t.OwnerID, OwnerID: t.OwnerID,
@@ -122,9 +125,9 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa
return artifact, nil return artifact, nil
} }
func getArtifactByNameAndPath(ctx context.Context, runID int64, name, fpath string) (*ActionArtifact, error) { func getArtifactByNameAndPath(ctx context.Context, runID, runAttemptID int64, name, fpath string) (*ActionArtifact, error) {
var art ActionArtifact var art ActionArtifact
has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ?", runID, name, fpath).Get(&art) has, err := db.GetEngine(ctx).Where("run_id = ? AND run_attempt_id = ? AND artifact_name = ? AND artifact_path = ?", runID, runAttemptID, name, fpath).Get(&art)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !has {
@@ -144,6 +147,7 @@ type FindArtifactsOptions struct {
db.ListOptions db.ListOptions
RepoID int64 RepoID int64
RunID int64 RunID int64
RunAttemptID optional.Option[int64] // use optional to allow filtering by zero (legacy artifacts have run_attempt_id=0)
ArtifactName string ArtifactName string
Status int Status int
FinalizedArtifactsV4 bool FinalizedArtifactsV4 bool
@@ -163,6 +167,9 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond {
if opts.RunID > 0 { if opts.RunID > 0 {
cond = cond.And(builder.Eq{"run_id": opts.RunID}) cond = cond.And(builder.Eq{"run_id": opts.RunID})
} }
if opts.RunAttemptID.Has() {
cond = cond.And(builder.Eq{"run_attempt_id": opts.RunAttemptID.Value()})
}
if opts.ArtifactName != "" { if opts.ArtifactName != "" {
cond = cond.And(builder.Eq{"artifact_name": opts.ArtifactName}) cond = cond.And(builder.Eq{"artifact_name": opts.ArtifactName})
} }
@@ -186,11 +193,12 @@ type ActionArtifactMeta struct {
ExpiredUnix timeutil.TimeStamp ExpiredUnix timeutil.TimeStamp
} }
// ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run // ListUploadedArtifactsMetaByRunAttempt returns uploaded artifacts meta scoped to a specific run and attempt.
func ListUploadedArtifactsMeta(ctx context.Context, repoID, runID int64) ([]*ActionArtifactMeta, error) { // Pass runAttemptID=0 to target legacy artifacts (pre-v331) belonging to the run.
func ListUploadedArtifactsMetaByRunAttempt(ctx context.Context, repoID, runID, runAttemptID int64) ([]*ActionArtifactMeta, error) {
arts := make([]*ActionArtifactMeta, 0, 10) arts := make([]*ActionArtifactMeta, 0, 10)
return arts, db.GetEngine(ctx).Table("action_artifact"). return arts, db.GetEngine(ctx).Table("action_artifact").
Where("repo_id=? AND run_id=? AND (status=? OR status=?)", repoID, runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired). Where("repo_id=? AND run_id=? AND run_attempt_id=? AND (status=? OR status=?)", repoID, runID, runAttemptID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired).
GroupBy("artifact_name"). GroupBy("artifact_name").
Select("artifact_name, sum(file_size) as file_size, max(status) as status, max(expired_unix) as expired_unix"). Select("artifact_name, sum(file_size) as file_size, max(status) as status, max(expired_unix) as expired_unix").
Find(&arts) Find(&arts)
@@ -217,12 +225,29 @@ func SetArtifactExpired(ctx context.Context, artifactID int64) error {
return err return err
} }
// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it // SetArtifactNeedDeleteByID sets an artifact to need-delete by ID, cron job will delete it.
func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error { func SetArtifactNeedDeleteByID(ctx context.Context, artifactID int64) error {
_, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion}) _, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion})
return err return err
} }
// SetArtifactNeedDeleteByRunAttempt sets an artifact to need-delete in a run attempt, cron job will delete it.
// runAttemptID may be 0 for legacy artifacts created before ActionRunAttempt existed.
func SetArtifactNeedDeleteByRunAttempt(ctx context.Context, runID, runAttemptID int64, name string) error {
_, err := db.GetEngine(ctx).Where("run_id=? AND run_attempt_id=? AND artifact_name=? AND status = ?", runID, runAttemptID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion})
return err
}
// GetArtifactsByRunAttemptAndName returns all artifacts with the given name in the specified run attempt.
// This supports both attempt-scoped data and legacy artifacts with run_attempt_id=0.
func GetArtifactsByRunAttemptAndName(ctx context.Context, runID, runAttemptID int64, artifactName string) ([]*ActionArtifact, error) {
arts := make([]*ActionArtifact, 0)
return arts, db.GetEngine(ctx).
Where("run_id = ? AND run_attempt_id = ? AND artifact_name = ?", runID, runAttemptID, artifactName).
OrderBy("id").
Find(&arts)
}
// SetArtifactDeleted sets an artifact to deleted // SetArtifactDeleted sets an artifact to deleted
func SetArtifactDeleted(ctx context.Context, artifactID int64) error { func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
_, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusDeleted}) _, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusDeleted})
+52 -28
View File
@@ -30,7 +30,7 @@ import (
type ActionRun struct { type ActionRun struct {
ID int64 ID int64
Title string Title string
RepoID int64 `xorm:"unique(repo_index) index(repo_concurrency)"` RepoID int64 `xorm:"unique(repo_index)"`
Repo *repo_model.Repository `xorm:"-"` Repo *repo_model.Repository `xorm:"-"`
OwnerID int64 `xorm:"index"` OwnerID int64 `xorm:"index"`
WorkflowID string `xorm:"index"` // the name of workflow file WorkflowID string `xorm:"index"` // the name of workflow file
@@ -50,15 +50,20 @@ type ActionRun struct {
Status Status `xorm:"index"` Status Status `xorm:"index"`
Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed
RawConcurrency string // raw concurrency RawConcurrency string // raw concurrency
ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"`
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` // Started and Stopped are identical to the latest attempt after ActionRunAttempt was introduced.
// Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0 // When a rerun creates a new latest attempt, they are reset until the new attempt starts and stops.
Started timeutil.TimeStamp Started timeutil.TimeStamp
Stopped timeutil.TimeStamp Stopped timeutil.TimeStamp
// PreviousDuration is used for recording previous duration
// PreviousDuration is kept only for legacy runs created before ActionRunAttempt existed.
// New runs and reruns no longer update this field and use attempt-scoped durations instead.
PreviousDuration time.Duration PreviousDuration time.Duration
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"` LatestAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"`
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
} }
func init() { func init() {
@@ -115,7 +120,7 @@ func (run *ActionRun) RefTooltip() string {
} }
// LoadAttributes load Repo TriggerUser if not loaded // LoadAttributes load Repo TriggerUser if not loaded
func (run *ActionRun) LoadAttributes(ctx context.Context) error { func (run *ActionRun) LoadAttributes(ctx context.Context) (err error) {
if run == nil { if run == nil {
return nil return nil
} }
@@ -129,11 +134,10 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error {
} }
if run.TriggerUser == nil { if run.TriggerUser == nil {
u, err := user_model.GetPossibleUserByID(ctx, run.TriggerUserID) run.TriggerUserID, run.TriggerUser, err = user_model.GetPossibleUserByID(ctx, run.TriggerUserID)
if err != nil { if err != nil {
return err return err
} }
run.TriggerUser = u
} }
return nil return nil
@@ -160,6 +164,31 @@ func (run *ActionRun) Duration() time.Duration {
return d return d
} }
// GetLatestAttempt returns
// - the latest attempt of the run
// - (nil, false, nil) for legacy runs that have no attempt records
func (run *ActionRun) GetLatestAttempt(ctx context.Context) (*ActionRunAttempt, bool, error) {
if run.LatestAttemptID == 0 {
return nil, false, nil
}
attempt, err := GetRunAttemptByRepoAndID(ctx, run.RepoID, run.LatestAttemptID)
if err != nil {
return nil, false, err
}
return attempt, true, nil
}
func (run *ActionRun) GetEffectiveConcurrency(ctx context.Context) (string, bool, error) {
attempt, has, err := run.GetLatestAttempt(ctx)
if err != nil {
return "", false, err
}
if has {
return attempt.ConcurrencyGroup, attempt.ConcurrencyCancel, nil
}
return "", false, nil
}
func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) { func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) {
if run.Event == webhook_module.HookEventPush { if run.Event == webhook_module.HookEventPush {
var payload api.PushPayload var payload api.PushPayload
@@ -406,14 +435,11 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
type ActionRunIndex db.ResourceIndex type ActionRunIndex db.ResourceIndex
func GetConcurrentRunsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRun, []*ActionRunJob, error) { // GetConcurrentRunAttemptsAndJobs returns run attempts and jobs in the same concurrency group by statuses.
runs, err := db.Find[ActionRun](ctx, &FindRunOptions{ func GetConcurrentRunAttemptsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRunAttempt, []*ActionRunJob, error) {
RepoID: repoID, attempts, err := FindConcurrentRunAttempts(ctx, repoID, concurrencyGroup, status)
ConcurrencyGroup: concurrencyGroup,
Status: status,
})
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("find runs: %w", err) return nil, nil, fmt.Errorf("find run attempts: %w", err)
} }
jobs, err := db.Find[ActionRunJob](ctx, &FindRunJobOptions{ jobs, err := db.Find[ActionRunJob](ctx, &FindRunJobOptions{
@@ -425,36 +451,34 @@ func GetConcurrentRunsAndJobs(ctx context.Context, repoID int64, concurrencyGrou
return nil, nil, fmt.Errorf("find jobs: %w", err) return nil, nil, fmt.Errorf("find jobs: %w", err)
} }
return runs, jobs, nil return attempts, jobs, nil
} }
func CancelPreviousJobsByRunConcurrency(ctx context.Context, actionRun *ActionRun) ([]*ActionRunJob, error) { func CancelPreviousJobsByRunConcurrency(ctx context.Context, attempt *ActionRunAttempt) ([]*ActionRunJob, error) {
if actionRun.ConcurrencyGroup == "" { if attempt.ConcurrencyGroup == "" {
return nil, nil return nil, nil
} }
var jobsToCancel []*ActionRunJob var jobsToCancel []*ActionRunJob
statusFindOption := []Status{StatusWaiting, StatusBlocked} statusFindOption := []Status{StatusWaiting, StatusBlocked}
if actionRun.ConcurrencyCancel { if attempt.ConcurrencyCancel {
statusFindOption = append(statusFindOption, StatusRunning) statusFindOption = append(statusFindOption, StatusRunning)
} }
runs, jobs, err := GetConcurrentRunsAndJobs(ctx, actionRun.RepoID, actionRun.ConcurrencyGroup, statusFindOption) attempts, jobs, err := GetConcurrentRunAttemptsAndJobs(ctx, attempt.RepoID, attempt.ConcurrencyGroup, statusFindOption)
if err != nil { if err != nil {
return nil, fmt.Errorf("find concurrent runs and jobs: %w", err) return nil, fmt.Errorf("find concurrent runs and jobs: %w", err)
} }
jobsToCancel = append(jobsToCancel, jobs...) jobsToCancel = append(jobsToCancel, jobs...)
// cancel runs in the same concurrency group // cancel runs in the same concurrency group
for _, run := range runs { for _, concurrentAttempt := range attempts {
if run.ID == actionRun.ID { if concurrentAttempt.RunID == attempt.RunID {
continue continue
} }
jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{ jobs, err := GetRunJobsByRunAndAttemptID(ctx, concurrentAttempt.RunID, concurrentAttempt.ID)
RunID: run.ID,
})
if err != nil { if err != nil {
return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err) return nil, fmt.Errorf("find run %d attempt %d jobs: %w", concurrentAttempt.RunID, concurrentAttempt.ID, err)
} }
jobsToCancel = append(jobsToCancel, jobs...) jobsToCancel = append(jobsToCancel, jobs...)
} }
+144
View File
@@ -0,0 +1,144 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"fmt"
"slices"
"time"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
)
// ActionRunAttempt represents a single execution attempt of an ActionRun.
type ActionRunAttempt struct {
ID int64
RepoID int64 `xorm:"index(repo_concurrency_status)"`
RunID int64 `xorm:"UNIQUE(run_attempt)"`
Run *ActionRun `xorm:"-"`
Attempt int64 `xorm:"UNIQUE(run_attempt)"`
TriggerUserID int64
TriggerUser *user_model.User `xorm:"-"`
ConcurrencyGroup string `xorm:"index(repo_concurrency_status) NOT NULL DEFAULT ''"`
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"`
Status Status `xorm:"index(repo_concurrency_status)"`
Started timeutil.TimeStamp
Stopped timeutil.TimeStamp
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
}
func (*ActionRunAttempt) TableName() string {
return "action_run_attempt"
}
func init() {
db.RegisterModel(new(ActionRunAttempt))
}
func (attempt *ActionRunAttempt) Duration() time.Duration {
return calculateDuration(attempt.Started, attempt.Stopped, attempt.Status, attempt.Updated)
}
func (attempt *ActionRunAttempt) LoadAttributes(ctx context.Context) (err error) {
if attempt == nil {
return nil
}
if attempt.Run == nil {
run, err := GetRunByRepoAndID(ctx, attempt.RepoID, attempt.RunID)
if err != nil {
return err
}
if err := run.LoadAttributes(ctx); err != nil {
return err
}
attempt.Run = run
}
if attempt.TriggerUser == nil {
attempt.TriggerUserID, attempt.TriggerUser, err = user_model.GetPossibleUserByID(ctx, attempt.TriggerUserID)
if err != nil {
return err
}
}
return nil
}
func GetRunAttemptByRepoAndID(ctx context.Context, repoID, attemptID int64) (*ActionRunAttempt, error) {
var attempt ActionRunAttempt
has, err := db.GetEngine(ctx).Where("repo_id=? AND id=?", repoID, attemptID).Get(&attempt)
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("run attempt %d in repo %d: %w", attemptID, repoID, util.ErrNotExist)
}
return &attempt, nil
}
func GetRunAttemptByRunIDAndAttemptNum(ctx context.Context, runID, attemptNum int64) (*ActionRunAttempt, error) {
var attempt ActionRunAttempt
has, err := db.GetEngine(ctx).Where("run_id=? AND attempt=?", runID, attemptNum).Get(&attempt)
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("run attempt %d for run %d: %w", attemptNum, runID, util.ErrNotExist)
}
return &attempt, nil
}
// FindConcurrentRunAttempts returns attempts in the given concurrency group and status set.
// Results are unordered; callers must not depend on any particular row order.
func FindConcurrentRunAttempts(ctx context.Context, repoID int64, concurrencyGroup string, statuses []Status) ([]*ActionRunAttempt, error) {
attempts := make([]*ActionRunAttempt, 0)
sess := db.GetEngine(ctx).Where("repo_id=? AND concurrency_group=?", repoID, concurrencyGroup)
if len(statuses) > 0 {
sess = sess.In("status", statuses)
}
return attempts, sess.Find(&attempts)
}
func UpdateRunAttempt(ctx context.Context, attempt *ActionRunAttempt, cols ...string) error {
if slices.Contains(cols, "status") && attempt.Started.IsZero() && attempt.Status.IsRunning() {
attempt.Started = timeutil.TimeStampNow()
cols = append(cols, "started")
}
sess := db.GetEngine(ctx).ID(attempt.ID)
if len(cols) > 0 {
sess.Cols(cols...)
}
if _, err := sess.Update(attempt); err != nil {
return err
}
// Only status/timing changes on an attempt need to update the latest run.
if len(cols) > 0 && !slices.Contains(cols, "status") && !slices.Contains(cols, "started") && !slices.Contains(cols, "stopped") {
return nil
}
run, err := GetRunByRepoAndID(ctx, attempt.RepoID, attempt.RunID)
if err != nil {
return err
}
if run.LatestAttemptID != attempt.ID {
log.Warn("run %d cannot be updated by an old attempt %d", run.LatestAttemptID, attempt.ID)
return nil
}
run.Status = attempt.Status
run.Started = attempt.Started
run.Stopped = attempt.Stopped
return UpdateRun(ctx, run, "status", "started", "stopped")
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
)
type ActionRunAttemptList []*ActionRunAttempt
// GetUserIDs returns a slice of user's id
func (attempts ActionRunAttemptList) GetUserIDs() []int64 {
return container.FilterSlice(attempts, func(attempt *ActionRunAttempt) (int64, bool) {
return attempt.TriggerUserID, true
})
}
func (attempts ActionRunAttemptList) LoadTriggerUser(ctx context.Context) error {
userIDs := attempts.GetUserIDs()
users := make(map[int64]*user_model.User, len(userIDs))
if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil {
return err
}
for _, attempt := range attempts {
if attempt.TriggerUserID == user_model.ActionsUserID {
attempt.TriggerUser = user_model.NewActionsUser()
} else {
attempt.TriggerUser = users[attempt.TriggerUserID]
if attempt.TriggerUser == nil {
attempt.TriggerUser = user_model.NewGhostUser()
}
}
}
return nil
}
// ListRunAttemptsByRunID returns all attempts of a run, ordered by attempt number DESC (newest first).
func ListRunAttemptsByRunID(ctx context.Context, runID int64) (ActionRunAttemptList, error) {
var attempts ActionRunAttemptList
return attempts, db.GetEngine(ctx).Where("run_id=?", runID).OrderBy("attempt DESC").Find(&attempts)
}
+119 -30
View File
@@ -34,7 +34,10 @@ type ActionRunJob struct {
CommitSHA string `xorm:"index"` CommitSHA string `xorm:"index"`
IsForkPullRequest bool IsForkPullRequest bool
Name string `xorm:"VARCHAR(255)"` Name string `xorm:"VARCHAR(255)"`
Attempt int64
// for legacy jobs, this counts how many times the job has run;
// otherwise it matches the Attempt of the ActionRunAttempt identified by job.RunAttemptID
Attempt int64
// WorkflowPayload is act/jobparser.SingleWorkflow for act/jobparser.Parse // WorkflowPayload is act/jobparser.SingleWorkflow for act/jobparser.Parse
// it should contain exactly one job with global workflow fields for this model // it should contain exactly one job with global workflow fields for this model
@@ -43,8 +46,11 @@ type ActionRunJob struct {
JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id
Needs []string `xorm:"JSON TEXT"` Needs []string `xorm:"JSON TEXT"`
RunsOn []string `xorm:"JSON TEXT"` RunsOn []string `xorm:"JSON TEXT"`
TaskID int64 // the latest task of the job
Status Status `xorm:"index"` TaskID int64 // the task created by this job in its own attempt
SourceTaskID int64 `xorm:"NOT NULL DEFAULT 0"` // SourceTaskID points to a historical task when this job reuses an earlier attempt's result.
Status Status `xorm:"index"`
RawConcurrency string // raw concurrency from job YAML's "concurrency" section RawConcurrency string // raw concurrency from job YAML's "concurrency" section
@@ -61,6 +67,14 @@ type ActionRunJob struct {
// It is JSON-encoded repo_model.ActionsTokenPermissions and may be empty if not specified. // It is JSON-encoded repo_model.ActionsTokenPermissions and may be empty if not specified.
TokenPermissions *repo_model.ActionsTokenPermissions `xorm:"JSON TEXT"` TokenPermissions *repo_model.ActionsTokenPermissions `xorm:"JSON TEXT"`
// RunAttemptID identifies the ActionRunAttempt this job belongs to.
// A value of 0 indicates a legacy job created before ActionRunAttempt existed.
RunAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"`
// AttemptJobID is unique within a single attempt.
// For jobs created after ActionRunAttempt was introduced, the same logical job is expected to keep the same AttemptJobID across attempts.
// A value of 0 indicates a legacy job created before ActionRunAttempt existed.
AttemptJobID int64 `xorm:"index NOT NULL DEFAULT 0"`
Started timeutil.TimeStamp Started timeutil.TimeStamp
Stopped timeutil.TimeStamp Stopped timeutil.TimeStamp
Created timeutil.TimeStamp `xorm:"created"` Created timeutil.TimeStamp `xorm:"created"`
@@ -75,6 +89,13 @@ func (job *ActionRunJob) Duration() time.Duration {
return calculateDuration(job.Started, job.Stopped, job.Status, job.Updated) return calculateDuration(job.Started, job.Stopped, job.Status, job.Updated)
} }
func (job *ActionRunJob) EffectiveTaskID() int64 {
if job.TaskID > 0 {
return job.TaskID
}
return job.SourceTaskID
}
func (job *ActionRunJob) LoadRun(ctx context.Context) error { func (job *ActionRunJob) LoadRun(ctx context.Context) error {
if job.Run == nil { if job.Run == nil {
run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID) run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
@@ -152,9 +173,50 @@ func GetRunJobByRunAndID(ctx context.Context, runID, jobID int64) (*ActionRunJob
return &job, nil return &job, nil
} }
func GetRunJobsByRunID(ctx context.Context, runID int64) (ActionJobList, error) { func GetRunJobByAttemptJobID(ctx context.Context, runID, attemptID, attemptJobID int64) (*ActionRunJob, error) {
var job ActionRunJob
has, err := db.GetEngine(ctx).Where("run_id=? AND run_attempt_id=? AND attempt_job_id=?", runID, attemptID, attemptJobID).Get(&job)
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("run job with attempt_job_id %d in run %d attempt %d: %w", attemptJobID, runID, attemptID, util.ErrNotExist)
}
return &job, nil
}
// GetLatestAttemptJobsByRepoAndRunID returns the jobs of the latest attempt for a run.
// It prefers the latest attempt when one exists, and falls back to legacy jobs with run_attempt_id=0 for runs created before ActionRunAttempt existed.
func GetLatestAttemptJobsByRepoAndRunID(ctx context.Context, repoID, runID int64) (ActionJobList, error) {
run, err := GetRunByRepoAndID(ctx, repoID, runID)
if err != nil {
return nil, err
}
if run.LatestAttemptID > 0 {
return GetRunJobsByRunAndAttemptID(ctx, runID, run.LatestAttemptID)
}
var jobs []*ActionRunJob var jobs []*ActionRunJob
if err := db.GetEngine(ctx).Where("run_id=?", runID).OrderBy("id").Find(&jobs); err != nil { if err := db.GetEngine(ctx).Where("repo_id=? AND run_id=? AND run_attempt_id=0", repoID, runID).OrderBy("id").Find(&jobs); err != nil {
return nil, err
}
return jobs, nil
}
// GetAllRunJobsByRepoAndRunID returns all jobs for a run across all attempts.
func GetAllRunJobsByRepoAndRunID(ctx context.Context, repoID, runID int64) (ActionJobList, error) {
var jobs []*ActionRunJob
if err := db.GetEngine(ctx).Where("repo_id=? AND run_id=?", repoID, runID).OrderBy("id").Find(&jobs); err != nil {
return nil, err
}
return jobs, nil
}
// GetRunJobsByRunAndAttemptID returns jobs for a run within a specific attempt.
// runAttemptID may be 0 to address legacy jobs that were created before ActionRunAttempt existed and therefore have no attempt association.
func GetRunJobsByRunAndAttemptID(ctx context.Context, runID, runAttemptID int64) (ActionJobList, error) {
var jobs []*ActionRunJob
if err := db.GetEngine(ctx).Where("run_id=? AND run_attempt_id=?", runID, runAttemptID).OrderBy("id").Find(&jobs); err != nil {
return nil, err return nil, err
} }
return jobs, nil return jobs, nil
@@ -196,25 +258,51 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
} }
{ {
// Other goroutines may aggregate the status of the run and update it too. // Other goroutines may aggregate the status of the attempt/run and update it too.
// So we need load the run and its jobs before updating the run. // So we need to load the current jobs before updating the aggregate state.
run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID) if job.RunAttemptID > 0 {
if err != nil { attempt, err := GetRunAttemptByRepoAndID(ctx, job.RepoID, job.RunAttemptID)
return 0, err if err != nil {
} return 0, err
jobs, err := GetRunJobsByRunID(ctx, job.RunID) }
if err != nil { jobs, err := GetRunJobsByRunAndAttemptID(ctx, job.RunID, job.RunAttemptID)
return 0, err if err != nil {
} return 0, err
run.Status = AggregateJobStatus(jobs) }
if run.Started.IsZero() && run.Status.IsRunning() { attempt.Status = AggregateJobStatus(jobs)
run.Started = timeutil.TimeStampNow() if attempt.Started.IsZero() && attempt.Status.IsRunning() {
} attempt.Started = timeutil.TimeStampNow()
if run.Stopped.IsZero() && run.Status.IsDone() { }
run.Stopped = timeutil.TimeStampNow() if attempt.Stopped.IsZero() && attempt.Status.IsDone() {
} attempt.Stopped = timeutil.TimeStampNow()
if err := UpdateRun(ctx, run, "status", "started", "stopped"); err != nil { }
return 0, fmt.Errorf("update run %d: %w", run.ID, err) if err := UpdateRunAttempt(ctx, attempt, "status", "started", "stopped"); err != nil {
return 0, fmt.Errorf("update run attempt %d: %w", attempt.ID, err)
}
} else {
// TODO: Remove this fallback in the future.
// Legacy fallback: jobs created before migration v331 have RunAttemptID=0 and are NOT backfilled.
// This path keeps those runs' status consistent when their jobs finish, including:
// - jobs created before migration v331 and complete on the new version starts
// - zombie/abandoned cleanup cron tasks that call UpdateRunJob on legacy jobs
run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
if err != nil {
return 0, err
}
jobs, err := GetLatestAttemptJobsByRepoAndRunID(ctx, job.RepoID, job.RunID)
if err != nil {
return 0, err
}
run.Status = AggregateJobStatus(jobs)
if run.Started.IsZero() && run.Status.IsRunning() {
run.Started = timeutil.TimeStampNow()
}
if run.Stopped.IsZero() && run.Status.IsDone() {
run.Stopped = timeutil.TimeStampNow()
}
if err := UpdateRun(ctx, run, "status", "started", "stopped"); err != nil {
return 0, fmt.Errorf("update run %d: %w", run.ID, err)
}
} }
} }
@@ -269,7 +357,7 @@ func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob)
if job.ConcurrencyCancel { if job.ConcurrencyCancel {
statusFindOption = append(statusFindOption, StatusRunning) statusFindOption = append(statusFindOption, StatusRunning)
} }
runs, jobs, err := GetConcurrentRunsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, statusFindOption) attempts, jobs, err := GetConcurrentRunAttemptsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, statusFindOption)
if err != nil { if err != nil {
return nil, fmt.Errorf("find concurrent runs and jobs: %w", err) return nil, fmt.Errorf("find concurrent runs and jobs: %w", err)
} }
@@ -277,12 +365,13 @@ func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob)
jobsToCancel = append(jobsToCancel, jobs...) jobsToCancel = append(jobsToCancel, jobs...)
// cancel runs in the same concurrency group // cancel runs in the same concurrency group
for _, run := range runs { for _, attempt := range attempts {
jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{ if attempt.ID == job.RunAttemptID {
RunID: run.ID, continue
}) }
jobs, err := GetRunJobsByRunAndAttemptID(ctx, attempt.RunID, attempt.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err) return nil, fmt.Errorf("find run %d attempt %d jobs: %w", attempt.RunID, attempt.ID, err)
} }
jobsToCancel = append(jobsToCancel, jobs...) jobsToCancel = append(jobsToCancel, jobs...)
} }
+5
View File
@@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder" "xorm.io/builder"
@@ -70,6 +71,7 @@ func (jobs ActionJobList) LoadAttributes(ctx context.Context, withRepo bool) err
type FindRunJobOptions struct { type FindRunJobOptions struct {
db.ListOptions db.ListOptions
RunID int64 RunID int64
RunAttemptID optional.Option[int64] // use optional to allow filtering by zero (legacy jobs have run_attempt_id=0)
RepoID int64 RepoID int64
OwnerID int64 OwnerID int64
CommitSHA string CommitSHA string
@@ -83,6 +85,9 @@ func (opts FindRunJobOptions) ToConds() builder.Cond {
if opts.RunID > 0 { if opts.RunID > 0 {
cond = cond.And(builder.Eq{"`action_run_job`.run_id": opts.RunID}) cond = cond.And(builder.Eq{"`action_run_job`.run_id": opts.RunID})
} }
if opts.RunAttemptID.Has() {
cond = cond.And(builder.Eq{"`action_run_job`.run_attempt_id": opts.RunAttemptID.Value()})
}
if opts.RepoID > 0 { if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID}) cond = cond.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID})
} }
-25
View File
@@ -7,7 +7,6 @@ import (
"context" "context"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
@@ -25,12 +24,6 @@ func (runs RunList) GetUserIDs() []int64 {
}) })
} }
func (runs RunList) GetRepoIDs() []int64 {
return container.FilterSlice(runs, func(run *ActionRun) (int64, bool) {
return run.RepoID, true
})
}
func (runs RunList) LoadTriggerUser(ctx context.Context) error { func (runs RunList) LoadTriggerUser(ctx context.Context) error {
userIDs := runs.GetUserIDs() userIDs := runs.GetUserIDs()
users := make(map[int64]*user_model.User, len(userIDs)) users := make(map[int64]*user_model.User, len(userIDs))
@@ -50,18 +43,6 @@ func (runs RunList) LoadTriggerUser(ctx context.Context) error {
return nil return nil
} }
func (runs RunList) LoadRepos(ctx context.Context) error {
repoIDs := runs.GetRepoIDs()
repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs)
if err != nil {
return err
}
for _, run := range runs {
run.Repo = repos[run.RepoID]
}
return nil
}
type FindRunOptions struct { type FindRunOptions struct {
db.ListOptions db.ListOptions
RepoID int64 RepoID int64
@@ -102,12 +83,6 @@ func (opts FindRunOptions) ToConds() builder.Cond {
if opts.CommitSHA != "" { if opts.CommitSHA != "" {
cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA}) cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA})
} }
if len(opts.ConcurrencyGroup) > 0 {
if opts.RepoID == 0 {
panic("Invalid FindRunOptions: repo_id is required")
}
cond = cond.And(builder.Eq{"`action_run`.concurrency_group": opts.ConcurrencyGroup})
}
return cond return cond
} }
-49
View File
@@ -4,62 +4,13 @@
package actions package actions
import ( import (
"context"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"xorm.io/builder" "xorm.io/builder"
) )
type ScheduleList []*ActionSchedule type ScheduleList []*ActionSchedule
// GetUserIDs returns a slice of user's id
func (schedules ScheduleList) GetUserIDs() []int64 {
return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) {
return schedule.TriggerUserID, true
})
}
func (schedules ScheduleList) GetRepoIDs() []int64 {
return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) {
return schedule.RepoID, true
})
}
func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error {
userIDs := schedules.GetUserIDs()
users := make(map[int64]*user_model.User, len(userIDs))
if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil {
return err
}
for _, schedule := range schedules {
if schedule.TriggerUserID == user_model.ActionsUserID {
schedule.TriggerUser = user_model.NewActionsUser()
} else {
schedule.TriggerUser = users[schedule.TriggerUserID]
if schedule.TriggerUser == nil {
schedule.TriggerUser = user_model.NewGhostUser()
}
}
}
return nil
}
func (schedules ScheduleList) LoadRepos(ctx context.Context) error {
repoIDs := schedules.GetRepoIDs()
repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs)
if err != nil {
return err
}
for _, schedule := range schedules {
schedule.Repo = repos[schedule.RepoID]
}
return nil
}
type FindScheduleOptions struct { type FindScheduleOptions struct {
db.ListOptions db.ListOptions
RepoID int64 RepoID int64
-1
View File
@@ -272,7 +272,6 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
} }
now := timeutil.TimeStampNow() now := timeutil.TimeStampNow()
job.Attempt++
job.Started = now job.Started = now
job.Status = StatusRunning job.Status = StatusRunning
+1 -9
View File
@@ -186,15 +186,7 @@ func (a *Action) LoadActUser(ctx context.Context) {
if a.ActUser != nil { if a.ActUser != nil {
return return
} }
var err error a.ActUserID, a.ActUser, _ = user_model.GetPossibleUserByID(ctx, a.ActUserID)
a.ActUser, err = user_model.GetPossibleUserByID(ctx, a.ActUserID)
if err == nil {
return
} else if user_model.IsErrUserNotExist(err) {
a.ActUser = user_model.NewGhostUser()
} else {
log.Error("GetUserByID(%d): %v", a.ActUserID, err)
}
} }
func (a *Action) LoadRepo(ctx context.Context) error { func (a *Action) LoadRepo(ctx context.Context) error {
-6
View File
@@ -282,9 +282,3 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
return actions, count, nil return actions, count, nil
} }
func CountUserFeeds(ctx context.Context, userID int64) (int64, error) {
return db.GetEngine(ctx).Where("user_id = ?", userID).
And("is_deleted = ?", false).
Count(&Action{})
}
-22
View File
@@ -192,28 +192,6 @@ func (err ErrGPGKeyIDAlreadyUsed) Unwrap() error {
return util.ErrAlreadyExist return util.ErrAlreadyExist
} }
// ErrGPGKeyAccessDenied represents a "GPGKeyAccessDenied" kind of Error.
type ErrGPGKeyAccessDenied struct {
UserID int64
KeyID int64
}
// IsErrGPGKeyAccessDenied checks if an error is a ErrGPGKeyAccessDenied.
func IsErrGPGKeyAccessDenied(err error) bool {
_, ok := err.(ErrGPGKeyAccessDenied)
return ok
}
// Error pretty-prints an error of type ErrGPGKeyAccessDenied.
func (err ErrGPGKeyAccessDenied) Error() string {
return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d]",
err.UserID, err.KeyID)
}
func (err ErrGPGKeyAccessDenied) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error. // ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error.
type ErrKeyAccessDenied struct { type ErrKeyAccessDenied struct {
UserID int64 UserID int64
-8
View File
@@ -105,14 +105,6 @@ func addDeployKey(ctx context.Context, keyID, repoID int64, name, fingerprint st
return key, db.Insert(ctx, key) return key, db.Insert(ctx, key)
} }
// HasDeployKey returns true if public key is a deploy key of given repository.
func HasDeployKey(ctx context.Context, keyID, repoID int64) bool {
has, _ := db.GetEngine(ctx).
Where("key_id = ? AND repo_id = ?", keyID, repoID).
Get(new(DeployKey))
return has
}
// AddDeployKey add new deploy key to database and authorized_keys file. // AddDeployKey add new deploy key to database and authorized_keys file.
func AddDeployKey(ctx context.Context, repoID int64, name, content string, readOnly bool) (*DeployKey, error) { func AddDeployKey(ctx context.Context, repoID int64, name, content string, readOnly bool) (*DeployKey, error) {
fingerprint, err := CalcFingerprint(content) fingerprint, err := CalcFingerprint(content)
+3 -3
View File
@@ -217,7 +217,7 @@ func (app *OAuth2Application) GetGrantByUserID(ctx context.Context, userID int64
return grant, nil return grant, nil
} }
// CreateGrant generates a grant for an user // CreateGrant generates a grant for a user
func (app *OAuth2Application) CreateGrant(ctx context.Context, userID int64, scope string) (*OAuth2Grant, error) { func (app *OAuth2Application) CreateGrant(ctx context.Context, userID int64, scope string) (*OAuth2Grant, error) {
grant := &OAuth2Grant{ grant := &OAuth2Grant{
ApplicationID: app.ID, ApplicationID: app.ID,
@@ -461,7 +461,7 @@ func GetOAuth2AuthorizationByCode(ctx context.Context, code string) (auth *OAuth
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
// OAuth2Grant represents the permission of an user for a specific application to access resources // OAuth2Grant represents the permission of a user for a specific application to access resources
type OAuth2Grant struct { type OAuth2Grant struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX unique(user_application)"` UserID int64 `xorm:"INDEX unique(user_application)"`
@@ -627,7 +627,7 @@ func GetActiveOAuth2SourceByAuthName(ctx context.Context, name string) (*Source,
} }
if !has { if !has {
return nil, fmt.Errorf("oauth2 source not found, name: %q", name) return nil, util.NewNotExistErrorf("oauth2 source not found, name: %q", name)
} }
return authSource, nil return authSource, nil
+22 -30
View File
@@ -12,19 +12,30 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestOAuth2AuthorizationCodeValidity(t *testing.T) { func TestOAuth2AuthorizationCode(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) require.NoError(t, unittest.PrepareTestDatabase())
t.Run("GenerateSetsValidUntil", func(t *testing.T) { t.Run("GenerateSetsValidUntil", func(t *testing.T) {
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1}) grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1})
expectedValidUntil := timeutil.TimeStamp(time.Now().Unix() + 600) expectedValidUntil := timeutil.TimeStamp(time.Now().Unix() + 600)
code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "") code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expectedValidUntil, code.ValidUntil) assert.Equal(t, expectedValidUntil, code.ValidUntil)
assert.False(t, code.IsExpired()) assert.False(t, code.IsExpired())
assert.Equal(t, int64(1), code.ID)
code2, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), code.Code)
require.NoError(t, err)
assert.Equal(t, code.Code, code2.Code)
assert.NoError(t, code.Invalidate(t.Context())) assert.NoError(t, code.Invalidate(t.Context()))
code, err = auth_model.GetOAuth2AuthorizationByCode(t.Context(), "does not exist")
require.NoError(t, err)
require.Nil(t, code)
}) })
t.Run("Expired", func(t *testing.T) { t.Run("Expired", func(t *testing.T) {
@@ -34,13 +45,14 @@ func TestOAuth2AuthorizationCodeValidity(t *testing.T) {
assert.True(t, code.IsExpired()) assert.True(t, code.IsExpired())
}) })
t.Run("InvalidateTwice", func(t *testing.T) { t.Run("Invalidate", func(t *testing.T) {
code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode") grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1})
assert.NoError(t, err) code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "")
if assert.NotNil(t, code) { require.NoError(t, err)
assert.NoError(t, code.Invalidate(t.Context())) require.NotNil(t, code)
assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated) require.NoError(t, code.Invalidate(t.Context()))
} unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: code.Code})
assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated)
}) })
} }
@@ -224,19 +236,6 @@ func TestRevokeOAuth2Grant(t *testing.T) {
//////////////////// Authorization Code //////////////////// Authorization Code
func TestGetOAuth2AuthorizationByCode(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode")
assert.NoError(t, err)
assert.NotNil(t, code)
assert.Equal(t, "authcode", code.Code)
assert.Equal(t, int64(1), code.ID)
code, err = auth_model.GetOAuth2AuthorizationByCode(t.Context(), "does not exist")
assert.NoError(t, err)
assert.Nil(t, code)
}
func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) { func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) {
// test plain // test plain
code := &auth_model.OAuth2AuthorizationCode{ code := &auth_model.OAuth2AuthorizationCode{
@@ -284,13 +283,6 @@ func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) {
assert.Equal(t, "https://example.com/callback?code=thecode", redirect.String()) assert.Equal(t, "https://example.com/callback?code=thecode", redirect.String())
} }
func TestOAuth2AuthorizationCode_Invalidate(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
code := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"})
assert.NoError(t, code.Invalidate(t.Context()))
unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"})
}
func TestOAuth2AuthorizationCode_TableName(t *testing.T) { func TestOAuth2AuthorizationCode_TableName(t *testing.T) {
assert.Equal(t, "oauth2_authorization_code", new(auth_model.OAuth2AuthorizationCode).TableName()) assert.Equal(t, "oauth2_authorization_code", new(auth_model.OAuth2AuthorizationCode).TableName())
} }
-10
View File
@@ -200,13 +200,3 @@ func DeleteCredential(ctx context.Context, id, userID int64) (bool, error) {
had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{}) had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{})
return had > 0, err return had > 0, err
} }
// WebAuthnCredentials implements the webauthn.User interface
func WebAuthnCredentials(ctx context.Context, userID int64) ([]webauthn.Credential, error) {
dbCreds, err := GetWebAuthnCredentialsByUID(ctx, userID)
if err != nil {
return nil, err
}
return dbCreds.ToCredentials(), nil
}
+1 -1
View File
@@ -89,7 +89,7 @@
ref: "refs/heads/test" ref: "refs/heads/test"
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
event: "push" event: "push"
trigger_event: "push" trigger_event: "schedule"
is_fork_pull_request: 0 is_fork_pull_request: 0
status: 1 status: 1
started: 1683636528 started: 1683636528
+1 -38
View File
@@ -1,39 +1,2 @@
- []
id: 1
hook_id: 1
uuid: uuid1
is_delivered: true
is_succeed: false
request_content: >
{
"url": "/matrix-delivered",
"http_method":"PUT",
"headers": {
"X-Head": "42"
},
"body": "{}"
}
-
id: 2
hook_id: 1
uuid: uuid2
is_delivered: true
-
id: 3
hook_id: 1
uuid: uuid3
is_delivered: true
is_succeed: true
payload_content: '{"key":"value"}' # legacy task, payload saved in payload_content (and not in request_content)
request_content: >
{
"url": "/matrix-success",
"http_method":"PUT",
"headers": {
"X-Head": "42"
}
}
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly # DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
+1 -1
View File
@@ -4,7 +4,7 @@
name: "Test" name: "Test"
client_id: "da7da3ba-9a13-4167-856f-3899de0b0138" client_id: "da7da3ba-9a13-4167-856f-3899de0b0138"
client_secret: "$2a$10$UYRgUSgekzBp6hYe8pAdc.cgB4Gn06QRKsORUnIYTYQADs.YR/uvi" # bcrypt of "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA= client_secret: "$2a$10$UYRgUSgekzBp6hYe8pAdc.cgB4Gn06QRKsORUnIYTYQADs.YR/uvi" # bcrypt of "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=
redirect_uris: '["a", "https://example.com/xyzzy"]' redirect_uris: '["https://example.com"]'
created_unix: 1546869730 created_unix: 1546869730
updated_unix: 1546869730 updated_unix: 1546869730
confidential_client: true confidential_client: true
+1 -16
View File
@@ -1,17 +1,2 @@
- id: 1 []
grant_id: 1
code: "authcode"
code_challenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg" # Code Verifier: N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt
code_challenge_method: "S256"
redirect_uri: "a"
valid_until: 3546869730
- id: 2
grant_id: 4
code: "authcodepublic"
code_challenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg" # Code Verifier: N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt
code_challenge_method: "S256"
redirect_uri: "http://127.0.0.1/"
valid_until: 3546869730
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly # DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
+1 -53
View File
@@ -1,54 +1,2 @@
- []
id: 1
repo_id: 1
url: https://www.example.com/url1
content_type: 1 # json
events: '{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}'
is_active: true
-
id: 2
repo_id: 1
url: https://www.example.com/url2
content_type: 1 # json
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
is_active: false
-
id: 3
owner_id: 3
repo_id: 3
url: https://www.example.com/url3
content_type: 1 # json
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
is_active: true
-
id: 4
repo_id: 2
url: https://www.example.com/url4
content_type: 1 # json
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
is_active: true
-
id: 5
repo_id: 0
owner_id: 0
url: https://www.example.com/url5
content_type: 1 # json
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
is_active: true
is_system_webhook: true
-
id: 6
repo_id: 0
owner_id: 0
url: https://www.example.com/url6
content_type: 1 # json
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
is_active: true
is_system_webhook: false
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly # DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
-19
View File
@@ -7,7 +7,6 @@ import (
"context" "context"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
@@ -60,24 +59,6 @@ func (branches BranchList) LoadPusher(ctx context.Context) error {
return nil return nil
} }
func (branches BranchList) LoadRepo(ctx context.Context) error {
ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) {
return branch.RepoID, branch.RepoID > 0 && branch.Repo == nil
})
reposMap := make(map[int64]*repo_model.Repository, len(ids))
if err := db.GetEngine(ctx).In("id", ids).Find(&reposMap); err != nil {
return err
}
for _, branch := range branches {
if branch.RepoID <= 0 || branch.Repo != nil {
continue
}
branch.Repo = reposMap[branch.RepoID]
}
return nil
}
type FindBranchOptions struct { type FindBranchOptions struct {
db.ListOptions db.ListOptions
RepoID int64 RepoID int64
+1 -10
View File
@@ -400,16 +400,7 @@ func (c *Comment) LoadPoster(ctx context.Context) (err error) {
if c.Poster != nil { if c.Poster != nil {
return nil return nil
} }
c.PosterID, c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID)
c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
c.PosterID = user_model.GhostUserID
c.Poster = user_model.NewGhostUser()
} else {
log.Error("getUserByID[%d]: %v", c.ID, err)
}
}
return err return err
} }
-6
View File
@@ -89,12 +89,6 @@ type ErrUnknownDependencyType struct {
Type DependencyType Type DependencyType
} }
// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType
func IsErrUnknownDependencyType(err error) bool {
_, ok := err.(ErrUnknownDependencyType)
return ok
}
func (err ErrUnknownDependencyType) Error() string { func (err ErrUnknownDependencyType) Error() string {
return fmt.Sprintf("unknown dependency type [type: %d]", err.Type) return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
} }
+3 -25
View File
@@ -48,21 +48,6 @@ func (err ErrIssueNotExist) Unwrap() error {
return util.ErrNotExist return util.ErrNotExist
} }
// ErrNewIssueInsert is used when the INSERT statement in newIssue fails
type ErrNewIssueInsert struct {
OriginalError error
}
// IsErrNewIssueInsert checks if an error is a ErrNewIssueInsert.
func IsErrNewIssueInsert(err error) bool {
_, ok := err.(ErrNewIssueInsert)
return ok
}
func (err ErrNewIssueInsert) Error() string {
return err.OriginalError.Error()
}
var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed") var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed")
// Issue represents an issue or pull request of repository. // Issue represents an issue or pull request of repository.
@@ -190,17 +175,10 @@ func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
// LoadPoster loads poster // LoadPoster loads poster
func (issue *Issue) LoadPoster(ctx context.Context) (err error) { func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
if issue.Poster == nil && issue.PosterID != 0 { if issue.Poster != nil {
issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID) return nil
if err != nil {
issue.PosterID = user_model.GhostUserID
issue.Poster = user_model.NewGhostUser()
if !user_model.IsErrUserNotExist(err) {
return fmt.Errorf("getUserByID.(poster) [%d]: %w", issue.PosterID, err)
}
return nil
}
} }
issue.PosterID, issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
return err return err
} }
-21
View File
@@ -165,27 +165,6 @@ func MovePin(ctx context.Context, issue *Issue, newPosition int) error {
}) })
} }
func GetPinnedIssueIDs(ctx context.Context, repoID int64, isPull bool) ([]int64, error) {
var issuePins []IssuePin
if err := db.GetEngine(ctx).
Table("issue_pin").
Where("repo_id = ?", repoID).
And("is_pull = ?", isPull).
Find(&issuePins); err != nil {
return nil, err
}
sort.Slice(issuePins, func(i, j int) bool {
return issuePins[i].PinOrder < issuePins[j].PinOrder
})
var ids []int64
for _, pin := range issuePins {
ids = append(ids, pin.IssueID)
}
return ids, nil
}
func GetIssuePinsByRepoID(ctx context.Context, repoID int64, isPull bool) ([]*IssuePin, error) { func GetIssuePinsByRepoID(ctx context.Context, repoID int64, isPull bool) ([]*IssuePin, error) {
var pins []*IssuePin var pins []*IssuePin
if err := db.GetEngine(ctx).Where("repo_id = ? AND is_pull = ?", repoID, isPull).Find(&pins); err != nil { if err := db.GetEngine(ctx).Where("repo_id = ? AND is_pull = ?", repoID, isPull).Find(&pins); err != nil {
+1 -7
View File
@@ -93,12 +93,6 @@ type ErrIssueIsOpen struct {
Index int64 Index int64
} }
// IsErrIssueIsOpen checks if an error is a ErrIssueIsOpen.
func IsErrIssueIsOpen(err error) bool {
_, ok := err.(ErrIssueIsOpen)
return ok
}
func (err ErrIssueIsOpen) Error() string { func (err ErrIssueIsOpen) Error() string {
return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already open", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index) return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already open", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index)
} }
@@ -441,7 +435,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la
LabelIDs: labelIDs, LabelIDs: labelIDs,
Attachments: uuids, Attachments: uuids,
}); err != nil { }); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
return err return err
} }
return fmt.Errorf("newIssue: %w", err) return fmt.Errorf("newIssue: %w", err)
+1 -1
View File
@@ -67,7 +67,7 @@ func GetIssueWatch(ctx context.Context, userID, issueID int64) (iw *IssueWatch,
return iw, exists, err return iw, exists, err
} }
// CheckIssueWatch check if an user is watching an issue // CheckIssueWatch check if a user is watching an issue
// it takes participants and repo watch into account // it takes participants and repo watch into account
func CheckIssueWatch(ctx context.Context, user *user_model.User, issue *Issue) (bool, error) { func CheckIssueWatch(ctx context.Context, user *user_model.User, issue *Issue) (bool, error) {
iw, exist, err := GetIssueWatch(ctx, user.ID, issue.ID) iw, exist, err := GetIssueWatch(ctx, user.ID, issue.ID)
+3 -3
View File
@@ -437,8 +437,8 @@ func (pr *PullRequest) IsChecking() bool {
return pr.Status == PullRequestStatusChecking return pr.Status == PullRequestStatusChecking
} }
// CanAutoMerge returns true if this pull request can be merged automatically. // IsStatusMergeable returns true if this pull request is mergeable to its base
func (pr *PullRequest) CanAutoMerge() bool { func (pr *PullRequest) IsStatusMergeable() bool {
return pr.Status == PullRequestStatusMergeable return pr.Status == PullRequestStatusMergeable
} }
@@ -475,7 +475,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss
LabelIDs: labelIDs, LabelIDs: labelIDs,
Attachments: uuids, Attachments: uuids,
}); err != nil { }); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
return err return err
} }
return fmt.Errorf("newIssue: %w", err) return fmt.Errorf("newIssue: %w", err)
+4 -12
View File
@@ -67,7 +67,7 @@ func (err ErrNotValidReviewRequest) Unwrap() error {
return util.ErrInvalidArgument return util.ErrInvalidArgument
} }
// ErrReviewRequestOnClosedPR represents an error when an user tries to request a re-review on a closed or merged PR. // ErrReviewRequestOnClosedPR represents an error when a user tries to request a re-review on a closed or merged PR.
type ErrReviewRequestOnClosedPR struct{} type ErrReviewRequestOnClosedPR struct{}
// IsErrReviewRequestOnClosedPR checks if an error is an ErrReviewRequestOnClosedPR. // IsErrReviewRequestOnClosedPR checks if an error is an ErrReviewRequestOnClosedPR.
@@ -176,15 +176,7 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) {
if r.ReviewerID == 0 || r.Reviewer != nil { if r.ReviewerID == 0 || r.Reviewer != nil {
return err return err
} }
r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID) r.ReviewerID, r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
return fmt.Errorf("GetPossibleUserByID [%d]: %w", r.ReviewerID, err)
}
r.ReviewerID = user_model.GhostUserID
r.Reviewer = user_model.NewGhostUser()
return nil
}
return err return err
} }
@@ -908,8 +900,8 @@ func MarkConversation(ctx context.Context, comment *Comment, doer *user_model.Us
// CanMarkConversation Add or remove Conversation mark for a code comment permission check // CanMarkConversation Add or remove Conversation mark for a code comment permission check
// the PR writer , official reviewer and poster can do it // the PR writer , official reviewer and poster can do it
func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) { func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) {
if doer == nil || issue == nil { if doer == nil {
return false, errors.New("issue or doer is nil") return false, nil
} }
if err = issue.LoadRepo(ctx); err != nil { if err = issue.LoadRepo(ctx); err != nil {
+10 -9
View File
@@ -75,7 +75,7 @@ func deleteDB() error {
} }
db.Close() db.Close()
// Check if we need to setup a specific schema // Check if we need to set up a specific schema
if len(setting.Database.Schema) != 0 { if len(setting.Database.Schema) != 0 {
db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s",
setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode))
@@ -91,7 +91,7 @@ func deleteDB() error {
defer schrows.Close() defer schrows.Close()
if !schrows.Next() { if !schrows.Next() {
// Create and setup a DB schema // Create and set up a DB schema
_, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema) _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema)
if err != nil { if err != nil {
return err return err
@@ -134,7 +134,8 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
ourSkip := 2 ourSkip := 2
ourSkip += skip ourSkip += skip
deferFn := testlogger.PrintCurrentTest(t, ourSkip) deferFn := testlogger.PrintCurrentTest(t, ourSkip)
require.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath)) giteaRoot := setting.GetGiteaTestSourceRoot()
require.NoError(t, unittest.SyncDirs(filepath.Join(giteaRoot, "tests/gitea-repositories-meta"), setting.RepoRootPath))
if err := deleteDB(); err != nil { if err := deleteDB(); err != nil {
t.Fatalf("unable to reset database: %v", err) t.Fatalf("unable to reset database: %v", err)
@@ -166,7 +167,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
} }
} }
fixturesDir := filepath.Join(filepath.Dir(setting.AppPath), "models", "migrations", "fixtures", t.Name()) fixturesDir := filepath.Join(giteaRoot, "models", "migrations", "fixtures", t.Name())
if _, err := os.Stat(fixturesDir); err == nil { if _, err := os.Stat(fixturesDir); err == nil {
t.Logf("initializing fixtures from: %s", fixturesDir) t.Logf("initializing fixtures from: %s", fixturesDir)
@@ -203,17 +204,17 @@ func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table
func mainTest(m *testing.M) int { func mainTest(m *testing.M) int {
testlogger.Init() testlogger.Init()
tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data") tempWorkPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("migration-test-data-")
if err != nil { if err != nil {
testlogger.Panicf("Unable to create temporary data path %v\n", err) return testlogger.MainErrorf("Unable to create temporary dir for migration test: %v", err)
} }
defer cleanup() defer cleanup()
setting.AppDataPath = tmpDataPath setting.MockBuiltinPaths(tempWorkPath, "", "")
setting.SetupGiteaTestEnv()
unittest.InitSettingsForTesting()
if err = git.InitFull(); err != nil { if err = git.InitFull(); err != nil {
testlogger.Panicf("Unable to InitFull: %v\n", err) return testlogger.MainErrorf("Unable to InitFull: %v", err)
} }
setting.LoadDBSetting() setting.LoadDBSetting()
setting.InitLoggersForTest() setting.InitLoggersForTest()
+4
View File
@@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/models/migrations/v1_24" "code.gitea.io/gitea/models/migrations/v1_24"
"code.gitea.io/gitea/models/migrations/v1_25" "code.gitea.io/gitea/models/migrations/v1_25"
"code.gitea.io/gitea/models/migrations/v1_26" "code.gitea.io/gitea/models/migrations/v1_26"
"code.gitea.io/gitea/models/migrations/v1_27"
"code.gitea.io/gitea/models/migrations/v1_6" "code.gitea.io/gitea/models/migrations/v1_6"
"code.gitea.io/gitea/models/migrations/v1_7" "code.gitea.io/gitea/models/migrations/v1_7"
"code.gitea.io/gitea/models/migrations/v1_8" "code.gitea.io/gitea/models/migrations/v1_8"
@@ -405,6 +406,9 @@ func prepareMigrationTasks() []*migration {
newMigration(328, "Add TokenPermissions column to ActionRunJob", v1_26.AddTokenPermissionsToActionRunJob), newMigration(328, "Add TokenPermissions column to ActionRunJob", v1_26.AddTokenPermissionsToActionRunJob),
newMigration(329, "Add unique constraint for user badge", v1_26.AddUniqueIndexForUserBadge), newMigration(329, "Add unique constraint for user badge", v1_26.AddUniqueIndexForUserBadge),
newMigration(330, "Add name column to webhook", v1_26.AddNameToWebhook), newMigration(330, "Add name column to webhook", v1_26.AddNameToWebhook),
// Gitea 1.26.0 ends at migration ID number 330 (database version 331)
newMigration(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel),
} }
return preparedMigrations return preparedMigrations
} }
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
)
func TestMain(m *testing.M) {
base.MainTest(m)
}
+158
View File
@@ -0,0 +1,158 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"context"
"time"
"code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)
type actionRunAttempt struct {
ID int64
RepoID int64 `xorm:"index(repo_concurrency_status)"`
RunID int64 `xorm:"UNIQUE(run_attempt)"`
Attempt int64 `xorm:"UNIQUE(run_attempt)"`
TriggerUserID int64
ConcurrencyGroup string `xorm:"index(repo_concurrency_status) NOT NULL DEFAULT ''"`
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"`
Status int `xorm:"index(repo_concurrency_status)"`
Started timeutil.TimeStamp
Stopped timeutil.TimeStamp
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
}
func (actionRunAttempt) TableName() string {
return "action_run_attempt"
}
type actionArtifact struct {
ID int64 `xorm:"pk autoincr"`
RunID int64 `xorm:"index unique(runid_attempt_name_path)"`
RunAttemptID int64 `xorm:"index unique(runid_attempt_name_path) NOT NULL DEFAULT 0"`
RunnerID int64
RepoID int64 `xorm:"index"`
OwnerID int64
CommitSHA string
StoragePath string
FileSize int64
FileCompressedSize int64
ContentEncoding string `xorm:"content_encoding"`
ArtifactPath string `xorm:"index unique(runid_attempt_name_path)"`
ArtifactName string `xorm:"index unique(runid_attempt_name_path)"`
Status int `xorm:"index"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
ExpiredUnix timeutil.TimeStamp `xorm:"index"`
}
func (actionArtifact) TableName() string {
return "action_artifact"
}
// actionRun mirrors the post-migration action_run schema.
type actionRun struct {
ID int64
Title string
RepoID int64 `xorm:"unique(repo_index)"`
OwnerID int64 `xorm:"index"`
WorkflowID string `xorm:"index"`
Index int64 `xorm:"index unique(repo_index)"`
TriggerUserID int64 `xorm:"index"`
ScheduleID int64
Ref string `xorm:"index"`
CommitSHA string
IsForkPullRequest bool
NeedApproval bool
ApprovedBy int64 `xorm:"index"`
Event string
EventPayload string `xorm:"LONGTEXT"`
TriggerEvent string
Status int `xorm:"index"`
Version int `xorm:"version default 0"`
RawConcurrency string
Started timeutil.TimeStamp
Stopped timeutil.TimeStamp
PreviousDuration time.Duration
LatestAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"`
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
}
func (actionRun) TableName() string {
return "action_run"
}
// AddActionRunAttemptModel adds the ActionRunAttempt table and the supporting ActionRun/ActionRunJob fields.
func AddActionRunAttemptModel(x *xorm.Engine) error {
// add "action_run_attempt"
if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
}, new(actionRunAttempt)); err != nil {
return err
}
// update "action_run_job"
type ActionRunJob struct {
RunAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"`
AttemptJobID int64 `xorm:"index NOT NULL DEFAULT 0"`
SourceTaskID int64 `xorm:"NOT NULL DEFAULT 0"`
}
if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
}, new(ActionRunJob)); err != nil {
return err
}
// update "action_artifact": let xorm sync add the new 4-column unique index (runid_attempt_name_path) and drop the old 3-column unique (runid_name_path)
if err := x.Sync(new(actionArtifact)); err != nil {
return err
}
// update "action_run"
//
// This migration intentionally removes the legacy run-level concurrency columns after
// introducing attempt-level concurrency on action_run_attempt.
//
// Existing values from action_run.concurrency_group / action_run.concurrency_cancel are
// not backfilled into action_run_attempt:
// - the old fields are only meaningful while a run is actively participating in
// concurrency scheduling
// - for completed legacy runs, keeping or backfilling those values has no practical
// effect on future scheduling behavior
// - scanning and backfilling old runs would add significant migration cost for little value
//
// This means the schema change is destructive for those two legacy columns by design.
//
// Let xorm sync add the latest_attempt_id column and drop the now-orphan (repo_id, concurrency_group) index.
if err := x.Sync(new(actionRun)); err != nil {
return err
}
concurrencyColumns := make([]string, 0, 2)
for _, col := range []string{"concurrency_group", "concurrency_cancel"} {
exist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "action_run", col)
if err != nil {
return err
}
if exist {
concurrencyColumns = append(concurrencyColumns, col)
}
}
if len(concurrencyColumns) == 0 {
return nil
}
sess := x.NewSession()
defer sess.Close()
if err := base.DropTableColumns(sess, "action_run", concurrencyColumns...); err != nil {
return err
}
// DropTableColumns rebuilds the table on SQLite, which drops all existing indexes.
// Re-sync to restore the indexes defined on actionRun.
return x.Sync(new(actionRun))
}
+156
View File
@@ -0,0 +1,156 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"context"
"slices"
"testing"
"code.gitea.io/gitea/models/migrations/base"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/xorm/schemas"
)
type actionRunBeforeV331 struct {
ID int64 `xorm:"pk autoincr"`
ConcurrencyGroup string
ConcurrencyCancel bool
LatestAttemptID int64 `xorm:"-"`
}
func (actionRunBeforeV331) TableName() string {
return "action_run"
}
type actionRunJobBeforeV331 struct {
ID int64 `xorm:"pk autoincr"`
RunID int64 `xorm:"index"`
RepoID int64 `xorm:"index"`
}
func (actionRunJobBeforeV331) TableName() string {
return "action_run_job"
}
type actionArtifactBeforeV331 struct {
ID int64 `xorm:"pk autoincr"`
RunID int64 `xorm:"index unique(runid_name_path)"`
RepoID int64 `xorm:"index"`
ArtifactPath string `xorm:"index unique(runid_name_path)"`
ArtifactName string `xorm:"index unique(runid_name_path)"`
}
func (actionArtifactBeforeV331) TableName() string {
return "action_artifact"
}
func Test_AddActionRunAttemptModel(t *testing.T) {
x, deferable := base.PrepareTestEnv(t, 0,
new(actionRunBeforeV331),
new(actionRunJobBeforeV331),
new(actionArtifactBeforeV331),
)
defer deferable()
if x == nil || t.Failed() {
return
}
_, err := x.Insert(&actionArtifactBeforeV331{
RunID: 1,
RepoID: 1,
ArtifactPath: "artifact/path",
ArtifactName: "artifact-name",
})
require.NoError(t, err)
require.NoError(t, AddActionRunAttemptModel(x))
tableMap := base.LoadTableSchemasMap(t, x)
attemptTable := tableMap["action_run_attempt"]
require.NotNil(t, attemptTable)
attemptTablCols := []string{"id", "repo_id", "run_id", "attempt", "trigger_user_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "created", "updated"}
require.ElementsMatch(t, attemptTable.ColumnsSeq(), attemptTablCols)
runTable := tableMap["action_run"]
require.NotNil(t, runTable)
require.Contains(t, runTable.ColumnsSeq(), "latest_attempt_id")
require.NotContains(t, runTable.ColumnsSeq(), "concurrency_group")
require.NotContains(t, runTable.ColumnsSeq(), "concurrency_cancel")
jobTable := tableMap["action_run_job"]
require.NotNil(t, jobTable)
require.Contains(t, jobTable.ColumnsSeq(), "run_attempt_id")
require.Contains(t, jobTable.ColumnsSeq(), "attempt_job_id")
require.Contains(t, jobTable.ColumnsSeq(), "source_task_id")
attemptIndexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action_run_attempt")
require.NoError(t, err)
assert.True(t, hasIndexWithColumns(attemptIndexes, []string{"run_id", "attempt"}, true))
assert.True(t, hasIndexWithColumns(attemptIndexes, []string{"repo_id", "concurrency_group", "status"}, false))
runIndexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action_run")
require.NoError(t, err)
assert.True(t, hasIndexWithColumns(runIndexes, []string{"latest_attempt_id"}, false))
assert.False(t, hasIndexWithColumns(runIndexes, []string{"repo_id", "concurrency_group"}, false))
jobIndexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action_run_job")
require.NoError(t, err)
assert.True(t, hasIndexWithColumns(jobIndexes, []string{"run_attempt_id"}, false))
assert.True(t, hasIndexWithColumns(jobIndexes, []string{"attempt_job_id"}, false))
indexes, err := x.Dialect().GetIndexes(x.DB(), context.Background(), "action_artifact")
require.NoError(t, err)
assert.False(t, hasIndexWithColumns(indexes, []string{"run_id", "artifact_path", "artifact_name"}, true))
assert.True(t, hasIndexWithColumns(indexes, []string{"run_id", "run_attempt_id", "artifact_path", "artifact_name"}, true))
_, err = x.Insert(&actionArtifact{
RunID: 1,
RunAttemptID: 2,
RepoID: 1,
ArtifactPath: "artifact/path",
ArtifactName: "artifact-name",
})
require.NoError(t, err)
_, err = x.Insert(&actionArtifact{
RunID: 1,
RunAttemptID: 2,
RepoID: 1,
ArtifactPath: "artifact/path",
ArtifactName: "artifact-name",
})
require.Error(t, err)
_, err = x.Insert(&actionRunAttempt{
RepoID: 1,
RunID: 1,
Attempt: 2,
TriggerUserID: 1,
Status: 1,
})
require.NoError(t, err)
_, err = x.Insert(&actionRunAttempt{
RepoID: 1,
RunID: 1,
Attempt: 2,
TriggerUserID: 2,
Status: 1,
})
require.Error(t, err)
}
func hasIndexWithColumns(indexes map[string]*schemas.Index, cols []string, isUnique bool) bool {
for _, index := range indexes {
if isUnique && index.Type != schemas.UniqueType {
continue
}
if slices.Equal(index.Cols, cols) {
return true
}
}
return false
}
-46
View File
@@ -8,10 +8,7 @@ import (
"fmt" "fmt"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"xorm.io/builder" "xorm.io/builder"
@@ -129,49 +126,6 @@ func IsUserOrgOwner(ctx context.Context, users user_model.UserList, orgID int64)
return results return results
} }
// GetOrgAssignees returns all users that have write access and can be assigned to issues
// of the any repository in the organization.
func GetOrgAssignees(ctx context.Context, orgID int64) (_ []*user_model.User, err error) {
e := db.GetEngine(ctx)
userIDs := make([]int64, 0, 10)
if err = e.Table("access").
Join("INNER", "repository", "`repository`.id = `access`.repo_id").
Where("`repository`.owner_id = ? AND `access`.mode >= ?", orgID, perm.AccessModeWrite).
Select("user_id").
Find(&userIDs); err != nil {
return nil, err
}
additionalUserIDs := make([]int64, 0, 10)
if err = e.Table("team_user").
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
Join("INNER", "repository", "`repository`.id = `team_repo`.repo_id").
Where("`repository`.owner_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
orgID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
Distinct("`team_user`.uid").
Select("`team_user`.uid").
Find(&additionalUserIDs); err != nil {
return nil, err
}
uniqueUserIDs := make(container.Set[int64])
uniqueUserIDs.AddMultiple(userIDs...)
uniqueUserIDs.AddMultiple(additionalUserIDs...)
users := make([]*user_model.User, 0, len(uniqueUserIDs))
if len(userIDs) > 0 {
if err = e.In("id", uniqueUserIDs.Values()).
Where(builder.Eq{"`user`.is_active": true}).
OrderBy(user_model.GetOrderByName()).
Find(&users); err != nil {
return nil, err
}
}
return users, nil
}
func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) { func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) {
if len(users) == 0 { if len(users) == 0 {
return nil, nil //nolint:nilnil // return nil when there are no users return nil, nil //nolint:nilnil // return nil when there are no users
-16
View File
@@ -28,19 +28,3 @@ func (t *TeamUnit) Unit() unit.Unit {
func getUnitsByTeamID(ctx context.Context, teamID int64) (units []*TeamUnit, err error) { func getUnitsByTeamID(ctx context.Context, teamID int64) (units []*TeamUnit, err error) {
return units, db.GetEngine(ctx).Where("team_id = ?", teamID).Find(&units) return units, db.GetEngine(ctx).Where("team_id = ?", teamID).Find(&units)
} }
// UpdateTeamUnits updates a teams's units
func UpdateTeamUnits(ctx context.Context, team *Team, units []TeamUnit) (err error) {
return db.WithTx(ctx, func(ctx context.Context) error {
if _, err = db.GetEngine(ctx).Where("team_id = ?", team.ID).Delete(new(TeamUnit)); err != nil {
return err
}
if len(units) > 0 {
if err = db.Insert(ctx, units); err != nil {
return err
}
}
return nil
})
}
-8
View File
@@ -36,14 +36,6 @@ type SearchMembersOptions struct {
TeamID int64 TeamID int64
} }
func (opts SearchMembersOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.TeamID > 0 {
cond = cond.And(builder.Eq{"": opts.TeamID})
}
return cond
}
// GetTeamMembers returns all members in given team of organization. // GetTeamMembers returns all members in given team of organization.
func GetTeamMembers(ctx context.Context, opts *SearchMembersOptions) ([]*user_model.User, error) { func GetTeamMembers(ctx context.Context, opts *SearchMembersOptions) ([]*user_model.User, error) {
var members []*user_model.User var members []*user_model.User
-14
View File
@@ -337,20 +337,6 @@ func SetDefaultColumn(ctx context.Context, projectID, columnID int64) error {
}) })
} }
// UpdateColumnSorting update project column sorting
func UpdateColumnSorting(ctx context.Context, cl ColumnList) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for i := range cl {
if _, err := db.GetEngine(ctx).ID(cl[i].ID).Cols(
"sorting",
).Update(cl[i]); err != nil {
return err
}
}
return nil
})
}
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) { func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) {
columns := make([]*Column, 0, 5) columns := make([]*Column, 0, 5)
if len(columnsIDs) == 0 { if len(columnsIDs) == 0 {
+2 -12
View File
@@ -5,14 +5,12 @@ package pull
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
) )
// AutoMerge represents a pull request scheduled for merging when checks succeed // AutoMerge represents a pull request scheduled for merging when checks succeed
@@ -78,16 +76,8 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
return false, nil, err return false, nil, err
} }
doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID) scheduledPRM.DoerID, scheduledPRM.Doer, err = user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
if errors.Is(err, util.ErrNotExist) { return true, scheduledPRM, err
doer, err = user_model.NewGhostUser(), nil
}
if err != nil {
return false, nil, err
}
scheduledPRM.Doer = doer
return true, scheduledPRM, nil
} }
// DeleteScheduledAutoMerge delete a scheduled pull request // DeleteScheduledAutoMerge delete a scheduled pull request
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
) )
// Star represents a starred repo by an user. // Star represents a starred repo by a user.
type Star struct { type Star struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"UNIQUE(s)"` UID int64 `xorm:"UNIQUE(s)"`
-6
View File
@@ -44,12 +44,6 @@ type ErrTopicNotExist struct {
Name string Name string
} }
// IsErrTopicNotExist checks if an error is an ErrTopicNotExist.
func IsErrTopicNotExist(err error) bool {
_, ok := err.(ErrTopicNotExist)
return ok
}
// Error implements error interface // Error implements error interface
func (err ErrTopicNotExist) Error() string { func (err ErrTopicNotExist) Error() string {
return fmt.Sprintf("topic is not exist [name: %s]", err.Name) return fmt.Sprintf("topic is not exist [name: %s]", err.Name)
+5 -11
View File
@@ -8,6 +8,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@@ -94,8 +95,7 @@ func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Reposit
return db.FindAndCount[Repository](ctx, opts) return db.FindAndCount[Repository](ctx, opts)
} }
// GetRepoAssignees returns all users that have write access and can be assigned to issues // GetRepoAssignees returns all users that have write access and can be assigned to issues or pull-requests of the repository,
// of the repository,
func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.User, err error) { func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.User, err error) {
if err = repo.LoadOwner(ctx); err != nil { if err = repo.LoadOwner(ctx); err != nil {
return nil, err return nil, err
@@ -114,15 +114,9 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
uniqueUserIDs.AddMultiple(userIDs...) uniqueUserIDs.AddMultiple(userIDs...)
if repo.Owner.IsOrganization() { if repo.Owner.IsOrganization() {
additionalUserIDs := make([]int64, 0, 10) // issues and pull requests both need "assignee list"
if err = e.Table("team_user"). additionalUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeIssues, unit.TypePullRequests)
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). if err != nil {
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
Distinct("`team_user`.uid").
Select("`team_user`.uid").
Find(&additionalUserIDs); err != nil {
return nil, err return nil, err
} }
uniqueUserIDs.AddMultiple(additionalUserIDs...) uniqueUserIDs.AddMultiple(additionalUserIDs...)
+34 -4
View File
@@ -6,7 +6,12 @@ package repo_test
import ( import (
"testing" "testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
perm_model "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@@ -14,9 +19,14 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestRepoAssignees(t *testing.T) { func TestUserRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) require.NoError(t, unittest.PrepareTestDatabase())
t.Run("GetIssuePostersWithSearch", testUserRepoGetIssuePostersWithSearch)
t.Run("Assignees", testUserRepoAssignees)
t.Run("AssigneesNoTeamUnit", testRepoAssigneesNoTeamUnit)
}
func testUserRepoAssignees(t *testing.T) {
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
users, err := repo_model.GetRepoAssignees(t.Context(), repo2) users, err := repo_model.GetRepoAssignees(t.Context(), repo2)
assert.NoError(t, err) assert.NoError(t, err)
@@ -39,9 +49,29 @@ func TestRepoAssignees(t *testing.T) {
} }
} }
func TestGetIssuePostersWithSearch(t *testing.T) { func testRepoAssigneesNoTeamUnit(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) ctx := t.Context()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32})
require.NoError(t, repo.LoadOwner(ctx))
require.True(t, repo.Owner.IsOrganization())
require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{}, &access_model.Access{}))
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
team := &organization.Team{OrgID: repo.OwnerID, LowerName: "admin-team", AccessMode: perm_model.AccessModeAdmin}
require.NoError(t, db.Insert(ctx, team))
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: repo.OwnerID, TeamID: team.ID, UID: user.ID}))
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: repo.OwnerID, TeamID: team.ID, RepoID: repo.ID}))
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: repo.OwnerID, TeamID: team.ID, Type: unit.TypePullRequests, AccessMode: perm_model.AccessModeNone}))
users, err := repo_model.GetRepoAssignees(ctx, repo)
require.NoError(t, err)
require.Len(t, users, 1)
assert.ElementsMatch(t, []int64{4}, []int64{users[0].ID})
}
func testUserRepoGetIssuePostersWithSearch(t *testing.T) {
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER") users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER")
-13
View File
@@ -13,7 +13,6 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
) )
// NoticeType describes the notice type // NoticeType describes the notice type
@@ -60,18 +59,6 @@ func CreateRepositoryNotice(desc string, args ...any) error {
return CreateNotice(graceful.GetManager().ShutdownContext(), NoticeRepository, desc, args...) return CreateNotice(graceful.GetManager().ShutdownContext(), NoticeRepository, desc, args...)
} }
// RemoveAllWithNotice removes all directories in given path and
// creates a system notice when error occurs.
func RemoveAllWithNotice(ctx context.Context, title, path string) {
if err := util.RemoveAll(path); err != nil {
desc := fmt.Sprintf("%s [%s]: %v", title, path, err)
log.Warn(title+" [%s]: %v", path, err)
if err = CreateNotice(graceful.GetManager().ShutdownContext(), NoticeRepository, desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err)
}
}
}
// RemoveStorageWithNotice removes a file from the storage and // RemoveStorageWithNotice removes a file from the storage and
// creates a system notice when error occurs. // creates a system notice when error occurs.
func RemoveStorageWithNotice(ctx context.Context, bucket storage.ObjectStorage, title, path string) { func RemoveStorageWithNotice(ctx context.Context, bucket storage.ObjectStorage, title, path string) {
+142
View File
@@ -0,0 +1,142 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package unittest
import (
"fmt"
"io"
"maps"
"net/http"
"net/http/httptest"
"net/url"
"os"
"slices"
"strings"
"testing"
"code.gitea.io/gitea/modules/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// MockServerOptions tweaks NewMockWebServer behavior.
type MockServerOptions struct {
// Routes installs extra handlers on the mux before the fixture fallback;
// more specific patterns win.
Routes func(mux *http.ServeMux)
// StripPrefix is trimmed from the request path before forwarding upstream,
// useful when the client prepends a prefix the real upstream does not use
// (e.g. go-github prepends "/api/v3").
StripPrefix string
}
// NewMockWebServer returns a test HTTP server that records upstream responses on demand
// and replays them from disk on subsequent runs.
//
// - liveMode=true: requests are forwarded to liveServerBaseURL and responses written as
// fixture files under testDataDir.
// - liveMode=false: responses come from existing fixture files.
//
// Fixture format: header lines ("Name: value"), a blank line, then the body. Before
// replay, occurrences of liveServerBaseURL in the body are swapped for the mock URL.
//
// The typical switch is an env var holding an API token; fixtures ship committed so the
// default run (no token) works offline.
//
// token := os.Getenv("GITEA_TOKEN")
// mock := NewMockWebServer(t, "https://gitea.com", fixtureDir, token != "")
func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveMode bool, opts ...MockServerOptions) *httptest.Server {
t.Helper()
var opt MockServerOptions
if len(opts) > 0 {
opt = opts[0]
}
ignoredHeaders := []string{"cf-ray", "server", "date", "report-to", "nel", "x-request-id", "set-cookie"}
var mockURL string
fallback := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqPath := r.URL.EscapedPath()
if r.URL.RawQuery != "" {
reqPath += "?" + r.URL.RawQuery
}
log.Info("mock server: %s %s", r.Method, reqPath)
fixturePath := fmt.Sprintf("%s/%s_%s", testDataDir, r.Method, url.QueryEscape(reqPath))
if strings.Contains(r.URL.Path, ".git/") {
fixturePath = fmt.Sprintf("%s/%s_%s", testDataDir, r.Method, url.QueryEscape(r.URL.Path))
}
if liveMode {
require.NoError(t, os.MkdirAll(testDataDir, 0o755))
liveURL := liveServerBaseURL + strings.TrimPrefix(reqPath, opt.StripPrefix)
req, err := http.NewRequest(r.Method, liveURL, r.Body)
require.NoError(t, err, "building upstream request to %s", liveURL)
for name, values := range r.Header {
if strings.EqualFold(name, "accept-encoding") {
continue
}
for _, value := range values {
req.Header.Add(name, value)
}
}
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err, "upstream request to %s failed", liveURL)
defer resp.Body.Close()
assert.Less(t, resp.StatusCode, 400, "upstream %s returned status %d", liveURL, resp.StatusCode)
out, err := os.Create(fixturePath)
require.NoError(t, err, "creating fixture %s", fixturePath)
defer out.Close()
for _, name := range slices.Sorted(maps.Keys(resp.Header)) {
if slices.Contains(ignoredHeaders, strings.ToLower(name)) {
continue
}
for _, value := range resp.Header[name] {
_, err := fmt.Fprintf(out, "%s: %s\n", name, value)
require.NoError(t, err)
}
}
_, err = out.WriteString("\n")
require.NoError(t, err)
_, err = io.Copy(out, resp.Body)
require.NoError(t, err, "writing fixture body for %s", liveURL)
require.NoError(t, out.Sync())
}
raw, err := os.ReadFile(fixturePath)
require.NoError(t, err, "missing fixture: %s", fixturePath)
replayed := strings.ReplaceAll(string(raw), liveServerBaseURL, mockURL)
headers, body, _ := strings.Cut(replayed, "\n\n")
for line := range strings.SplitSeq(headers, "\n") {
name, value, ok := strings.Cut(line, ": ")
if !ok || strings.EqualFold(name, "Content-Length") {
continue
}
w.Header().Set(name, value)
}
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(body))
require.NoError(t, err)
})
mux := http.NewServeMux()
if opt.Routes != nil {
opt.Routes(mux)
}
mux.Handle("/", fallback)
server := httptest.NewServer(mux)
mockURL = server.URL
t.Cleanup(server.Close)
return server
}
+17 -72
View File
@@ -13,10 +13,8 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/auth/password/hash"
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config" "code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
@@ -29,37 +27,6 @@ import (
"xorm.io/xorm/names" "xorm.io/xorm/names"
) )
// InitSettingsForTesting initializes config provider and load common settings for tests
func InitSettingsForTesting() {
setting.SetupGiteaTestEnv()
log.OsExiter = func(code int) {
if code != 0 {
// non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details
panic(fmt.Errorf("non-zero exit code during testing: %d", code))
}
os.Exit(0)
}
if setting.CustomConf == "" {
setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
_ = os.Remove(setting.CustomConf)
}
// init paths and config system for testing
getTestEnv := func(key string) string {
return ""
}
setting.InitWorkPathAndCommonConfig(getTestEnv, setting.ArgWorkPathAndCustomConf{CustomConf: setting.CustomConf})
if err := setting.PrepareAppDataPath(); err != nil {
log.Fatal("Can not prepare APP_DATA_PATH: %v", err)
}
// register the dummy hash algorithm function used in the test fixtures
_ = hash.Register("dummy", hash.NewDummyHasher)
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
}
// TestOptions represents test options // TestOptions represents test options
type TestOptions struct { type TestOptions struct {
FixtureFiles []string FixtureFiles []string
@@ -75,11 +42,20 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
func mainTest(m *testing.M, testOptsArg ...*TestOptions) int { func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
testOpts := util.OptionalArg(testOptsArg, &TestOptions{}) testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
InitSettingsForTesting()
tempWorkPath, tempCleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("unit-test-dir-")
if err != nil {
return testlogger.MainErrorf("Failed to create temp dir for unit test: %v", err)
}
defer tempCleanup()
defer setting.MockBuiltinPaths(tempWorkPath, "", "")()
setting.SetupGiteaTestEnv()
giteaRoot := setting.GetGiteaTestSourceRoot() giteaRoot := setting.GetGiteaTestSourceRoot()
fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles} fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles}
if err := CreateTestEngine(fixturesOpts); err != nil { if err := CreateTestEngine(fixturesOpts); err != nil {
testlogger.Panicf("Error creating test engine: %v\n", err) return testlogger.MainErrorf("Error creating test database engine: %v", err)
} }
setting.AppURL = "https://try.gitea.io/" setting.AppURL = "https://try.gitea.io/"
@@ -91,59 +67,28 @@ func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
setting.SSH.Domain = "try.gitea.io" setting.SSH.Domain = "try.gitea.io"
setting.Database.Type = "sqlite3" setting.Database.Type = "sqlite3"
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master" setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
repoRootPath, cleanup1, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("repos")
if err != nil {
testlogger.Panicf("TempDir: %v\n", err)
}
defer cleanup1()
setting.RepoRootPath = repoRootPath
appDataPath, cleanup2, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("appdata")
if err != nil {
testlogger.Panicf("TempDir: %v\n", err)
}
defer cleanup2()
setting.AppDataPath = appDataPath
setting.GravatarSource = "https://secure.gravatar.com/avatar/" setting.GravatarSource = "https://secure.gravatar.com/avatar/"
setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments")
setting.LFS.Storage.Path = filepath.Join(setting.AppDataPath, "lfs")
setting.Avatar.Storage.Path = filepath.Join(setting.AppDataPath, "avatars")
setting.RepoAvatar.Storage.Path = filepath.Join(setting.AppDataPath, "repo-avatars")
setting.RepoArchive.Storage.Path = filepath.Join(setting.AppDataPath, "repo-archive")
setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages")
setting.Actions.LogStorage.Path = filepath.Join(setting.AppDataPath, "actions_log")
setting.Git.HomePath = filepath.Join(setting.AppDataPath, "home")
setting.IncomingEmail.ReplyToAddress = "incoming+%{token}@localhost" setting.IncomingEmail.ReplyToAddress = "incoming+%{token}@localhost"
config.SetDynGetter(system.NewDatabaseDynKeyGetter()) config.SetDynGetter(system.NewDatabaseDynKeyGetter())
if err = cache.Init(); err != nil { if err = cache.Init(); err != nil {
testlogger.Panicf("cache.Init: %v\n", err) return testlogger.MainErrorf("cache.Init: %v", err)
} }
if err = storage.Init(); err != nil { if err = storage.Init(); err != nil {
testlogger.Panicf("storage.Init: %v\n", err) return testlogger.MainErrorf("storage.Init: %v", err)
} }
if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil { if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
testlogger.Panicf("util.SyncDirs: %v\n", err) return testlogger.MainErrorf("util.SyncDirs: %v", err)
} }
if err = git.InitFull(); err != nil { if err = git.InitFull(); err != nil {
testlogger.Panicf("git.Init: %v\n", err) return testlogger.MainErrorf("git.Init: %v", err)
} }
if testOpts.SetUp != nil { if testOpts.SetUp != nil {
if err := testOpts.SetUp(); err != nil { if err := testOpts.SetUp(); err != nil {
testlogger.Panicf("set up failed: %v\n", err) return testlogger.MainErrorf("set up failed: %v", err)
} }
} }
@@ -151,7 +96,7 @@ func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
if testOpts.TearDown != nil { if testOpts.TearDown != nil {
if err := testOpts.TearDown(); err != nil { if err := testOpts.TearDown(); err != nil {
testlogger.Panicf("tear down failed: %v\n", err) return testlogger.MainErrorf("tear down failed: %v", err)
} }
} }
return exitStatus return exitStatus
+1 -20
View File
@@ -64,7 +64,7 @@ type GetBadgeUsersOptions struct {
func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, int64, error) { func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, int64, error) {
sess := db.GetEngine(ctx). sess := db.GetEngine(ctx).
Select("`user`.*"). Select("`user`.*").
Join("INNER", "user_badge", "`user_badge`.user_id=user.id"). Join("INNER", "user_badge", "`user_badge`.user_id=`user`.id").
Join("INNER", "badge", "`user_badge`.badge_id=badge.id"). Join("INNER", "badge", "`user_badge`.badge_id=badge.id").
Where("badge.slug=?", opts.BadgeSlug) Where("badge.slug=?", opts.BadgeSlug)
@@ -212,12 +212,6 @@ func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
}) })
} }
// RemoveAllUserBadges removes all badges from a user.
func RemoveAllUserBadges(ctx context.Context, u *User) error {
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
return err
}
// SearchBadgeOptions represents the options when finding badges // SearchBadgeOptions represents the options when finding badges
type SearchBadgeOptions struct { type SearchBadgeOptions struct {
db.ListOptions db.ListOptions
@@ -258,16 +252,3 @@ func (opts *SearchBadgeOptions) ToOrders() string {
func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) { func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) {
return db.FindAndCount[Badge](ctx, opts) return db.FindAndCount[Badge](ctx, opts)
} }
// GetBadgeByID returns a specific badge by ID
func GetBadgeByID(ctx context.Context, id int64) (*Badge, error) {
badge := new(Badge)
has, err := db.GetEngine(ctx).ID(id).Get(badge)
if err != nil {
return nil, err
}
if !has {
return nil, util.NewNotExistErrorf("badge does not exist [id: %d]", id)
}
return badge, nil
}
+1 -1
View File
@@ -90,7 +90,7 @@ func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, er
return nil, err return nil, err
} }
if len(blocks) == 0 { if len(blocks) == 0 {
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist return nil, util.NewNotExistErrorf("blocking record doesn't exist")
} }
return blocks[0], nil return blocks[0], nil
} }
-5
View File
@@ -147,11 +147,6 @@ func InsertEmailAddress(ctx context.Context, email *EmailAddress) (*EmailAddress
return email, nil return email, nil
} }
func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
_, err := db.GetEngine(ctx).ID(email.ID).AllCols().Update(email)
return err
}
// ValidateEmail check if email is a valid & allowed address // ValidateEmail check if email is a valid & allowed address
func ValidateEmail(email string) error { func ValidateEmail(email string) error {
if err := validateEmailBasic(email); err != nil { if err := validateEmailBasic(email); err != nil {
-21
View File
@@ -71,27 +71,6 @@ func (err ErrUserProhibitLogin) Unwrap() error {
return util.ErrPermissionDenied return util.ErrPermissionDenied
} }
// ErrUserInactive represents a "ErrUserInactive" kind of error.
type ErrUserInactive struct {
UID int64
Name string
}
// IsErrUserInactive checks if an error is a ErrUserInactive
func IsErrUserInactive(err error) bool {
_, ok := err.(ErrUserInactive)
return ok
}
func (err ErrUserInactive) Error() string {
return fmt.Sprintf("user is inactive [uid: %d, name: %s]", err.UID, err.Name)
}
// Unwrap unwraps this error as a ErrPermission error
func (err ErrUserInactive) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrUserIsNotLocal represents a "ErrUserIsNotLocal" kind of error. // ErrUserIsNotLocal represents a "ErrUserIsNotLocal" kind of error.
type ErrUserIsNotLocal struct { type ErrUserIsNotLocal struct {
UID int64 UID int64
-12
View File
@@ -21,12 +21,6 @@ type ErrExternalLoginUserAlreadyExist struct {
LoginSourceID int64 LoginSourceID int64
} }
// IsErrExternalLoginUserAlreadyExist checks if an error is a ExternalLoginUserAlreadyExist.
func IsErrExternalLoginUserAlreadyExist(err error) bool {
_, ok := err.(ErrExternalLoginUserAlreadyExist)
return ok
}
func (err ErrExternalLoginUserAlreadyExist) Error() string { func (err ErrExternalLoginUserAlreadyExist) Error() string {
return fmt.Sprintf("external login user already exists [externalID: %s, userID: %d, loginSourceID: %d]", err.ExternalID, err.UserID, err.LoginSourceID) return fmt.Sprintf("external login user already exists [externalID: %s, userID: %d, loginSourceID: %d]", err.ExternalID, err.UserID, err.LoginSourceID)
} }
@@ -41,12 +35,6 @@ type ErrExternalLoginUserNotExist struct {
LoginSourceID int64 LoginSourceID int64
} }
// IsErrExternalLoginUserNotExist checks if an error is a ExternalLoginUserNotExist.
func IsErrExternalLoginUserNotExist(err error) bool {
_, ok := err.(ErrExternalLoginUserNotExist)
return ok
}
func (err ErrExternalLoginUserNotExist) Error() string { func (err ErrExternalLoginUserNotExist) Error() string {
return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID) return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID)
} }
-6
View File
@@ -50,12 +50,6 @@ func (err ErrUserSettingIsNotExist) Unwrap() error {
return util.ErrNotExist return util.ErrNotExist
} }
// IsErrUserSettingIsNotExist return true if err is ErrSettingIsNotExist
func IsErrUserSettingIsNotExist(err error) bool {
_, ok := err.(ErrUserSettingIsNotExist)
return ok
}
// genSettingCacheKey returns the cache key for some configuration // genSettingCacheKey returns the cache key for some configuration
func genSettingCacheKey(userID int64, key string) string { func genSettingCacheKey(userID int64, key string) string {
return fmt.Sprintf("user_%d.setting.%s", userID, key) return fmt.Sprintf("user_%d.setting.%s", userID, key)
+13 -7
View File
@@ -7,6 +7,7 @@ package user
import ( import (
"context" "context"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"html/template" "html/template"
"mime" "mime"
@@ -1013,17 +1014,22 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
return users, err return users, err
} }
// GetPossibleUserByID returns the user if id > 0 or returns system user if id < 0 // GetPossibleUserByID returns the possible user and its ID. If the user doesn't exist, it returns Ghost user
func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) { func GetPossibleUserByID(ctx context.Context, id int64) (_ int64, u *User, err error) {
if id < 0 { if id < 0 {
if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok { if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok {
return newFunc(), nil u = newFunc()
} }
return nil, ErrUserNotExist{UID: id}
} else if id == 0 {
return nil, ErrUserNotExist{}
} }
return GetUserByID(ctx, id) if u == nil {
u, err = GetUserByID(ctx, id)
if errors.Is(err, util.ErrNotExist) {
u = NewGhostUser()
} else if err != nil {
return 0, nil, err
}
}
return u.ID, u, nil
} }
// GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0 // GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0
+8 -4
View File
@@ -11,8 +11,9 @@ import (
) )
func TestSystemUser(t *testing.T) { func TestSystemUser(t *testing.T) {
u, err := GetPossibleUserByID(t.Context(), -1) uid, u, err := GetPossibleUserByID(t.Context(), -1)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, int64(-1), uid)
assert.Equal(t, "Ghost", u.Name) assert.Equal(t, "Ghost", u.Name)
assert.Equal(t, "ghost", u.LowerName) assert.Equal(t, "ghost", u.LowerName)
assert.True(t, u.IsGhost()) assert.True(t, u.IsGhost())
@@ -21,8 +22,9 @@ func TestSystemUser(t *testing.T) {
require.NotNil(t, u) require.NotNil(t, u)
assert.Equal(t, "Ghost", u.Name) assert.Equal(t, "Ghost", u.Name)
u, err = GetPossibleUserByID(t.Context(), -2) uid, u, err = GetPossibleUserByID(t.Context(), -2)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, int64(-2), uid)
assert.Equal(t, "gitea-actions", u.Name) assert.Equal(t, "gitea-actions", u.Name)
assert.Equal(t, "gitea-actions", u.LowerName) assert.Equal(t, "gitea-actions", u.LowerName)
assert.True(t, u.IsGiteaActions()) assert.True(t, u.IsGiteaActions())
@@ -31,6 +33,8 @@ func TestSystemUser(t *testing.T) {
require.NotNil(t, u) require.NotNil(t, u)
assert.Equal(t, "Gitea Actions", u.FullName) assert.Equal(t, "Gitea Actions", u.FullName)
_, err = GetPossibleUserByID(t.Context(), -3) uid, u, err = GetPossibleUserByID(t.Context(), 999999)
require.Error(t, err) require.NoError(t, err)
assert.Equal(t, int64(-1), uid)
assert.Equal(t, "Ghost", u.Name)
} }
+1
View File
@@ -15,5 +15,6 @@ func TestMain(m *testing.M) {
"webhook.yml", "webhook.yml",
"hook_task.yml", "hook_task.yml",
}, },
SetUp: prepareWebhookTestData,
}) })
} }
+14 -14
View File
@@ -10,28 +10,28 @@ import (
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestListSystemWebhookOptions(t *testing.T) { func TestListSystemWebhookOptions(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hookSystem := unittest.AssertExistsAndLoadBean(t, &Webhook{URL: "https://www.example.com/system"})
hookDefault := unittest.AssertExistsAndLoadBean(t, &Webhook{URL: "https://www.example.com/default"})
opts := ListSystemWebhookOptions{IsSystem: optional.None[bool]()} opts := ListSystemWebhookOptions{IsSystem: optional.None[bool]()}
hooks, _, err := GetGlobalWebhooks(t.Context(), &opts) hooks, _, err := GetGlobalWebhooks(t.Context(), &opts)
assert.NoError(t, err) require.NoError(t, err)
if assert.Len(t, hooks, 2) { require.Len(t, hooks, 2)
assert.Equal(t, int64(5), hooks[0].ID) assert.Equal(t, hookSystem.ID, hooks[0].ID)
assert.Equal(t, int64(6), hooks[1].ID) assert.Equal(t, hookDefault.ID, hooks[1].ID)
}
opts.IsSystem = optional.Some(true) opts.IsSystem = optional.Some(true)
hooks, _, err = GetGlobalWebhooks(t.Context(), &opts) hooks, _, err = GetGlobalWebhooks(t.Context(), &opts)
assert.NoError(t, err) require.NoError(t, err)
if assert.Len(t, hooks, 1) { require.Len(t, hooks, 1)
assert.Equal(t, int64(5), hooks[0].ID) assert.Equal(t, hookSystem.ID, hooks[0].ID)
}
opts.IsSystem = optional.Some(false) opts.IsSystem = optional.Some(false)
hooks, _, err = GetGlobalWebhooks(t.Context(), &opts) hooks, _, err = GetGlobalWebhooks(t.Context(), &opts)
assert.NoError(t, err) require.NoError(t, err)
if assert.Len(t, hooks, 1) { require.Len(t, hooks, 1)
assert.Equal(t, int64(6), hooks[0].ID) assert.Equal(t, hookDefault.ID, hooks[0].ID)
}
} }
+151 -92
View File
@@ -4,6 +4,7 @@
package webhook package webhook
import ( import (
"context"
"testing" "testing"
"time" "time"
@@ -14,40 +15,105 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/builder"
) )
func TestHookContentType_Name(t *testing.T) { func prepareWebhookTestData() error {
assert.Equal(t, "json", ContentTypeJSON.Name()) if err := unittest.PrepareTestDatabase(); err != nil {
assert.Equal(t, "form", ContentTypeForm.Name()) return err
}
var hooks []*Webhook
hooks = append(hooks, &Webhook{
RepoID: 1,
URL: "https://www.example.com/url1",
ContentType: ContentTypeJSON,
Events: `{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}`,
IsActive: true,
})
hooks = append(hooks, &Webhook{
RepoID: 1,
URL: "https://www.example.com/url2",
ContentType: ContentTypeJSON,
Events: `{}`,
IsActive: false,
})
hooks = append(hooks, &Webhook{
OwnerID: 3,
RepoID: 3,
URL: "https://www.example.com/url3",
ContentType: ContentTypeJSON,
Events: `{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}`,
IsActive: true,
})
hooks = append(hooks, &Webhook{
OwnerID: 3,
RepoID: 3,
URL: "https://www.example.com/url3",
ContentType: ContentTypeJSON,
Events: `{}`,
})
hooks = append(hooks, &Webhook{
RepoID: 2,
URL: "https://www.example.com/url4",
ContentType: ContentTypeJSON,
Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`,
IsActive: true,
})
hooks = append(hooks, &Webhook{
URL: "https://www.example.com/system",
ContentType: ContentTypeJSON,
Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`,
IsSystemWebhook: true,
})
hooks = append(hooks, &Webhook{
URL: "https://www.example.com/default",
ContentType: ContentTypeJSON,
Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`,
})
ctx := context.Background()
if err := db.TruncateBeans(ctx, &Webhook{}); err != nil {
return err
}
if err := db.Insert(ctx, hooks); err != nil {
return err
}
hook, _, _ := db.Get[Webhook](ctx, builder.Eq{"repo_id": 1, "is_active": true})
var tasks []*HookTask
tasks = append(tasks, &HookTask{HookID: hook.ID, UUID: uuid.New().String()})
tasks = append(tasks, &HookTask{HookID: hook.ID, UUID: uuid.New().String()})
tasks = append(tasks, &HookTask{HookID: hook.ID, UUID: uuid.New().String()})
if err := db.TruncateBeans(ctx, &HookTask{}); err != nil {
return err
}
return db.Insert(ctx, tasks)
} }
func TestIsValidHookContentType(t *testing.T) { func TestWebHookContentType(t *testing.T) {
assert.Equal(t, "json", ContentTypeJSON.Name())
assert.Equal(t, "form", ContentTypeForm.Name())
assert.True(t, IsValidHookContentType("json")) assert.True(t, IsValidHookContentType("json"))
assert.True(t, IsValidHookContentType("form")) assert.True(t, IsValidHookContentType("form"))
assert.False(t, IsValidHookContentType("invalid")) assert.False(t, IsValidHookContentType("invalid"))
} }
func TestWebhook_History(t *testing.T) { func TestWebhook_History(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true})
webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1}) tasks, err := hook.History(t.Context(), 0)
tasks, err := webhook.History(t.Context(), 0) require.NoError(t, err)
assert.NoError(t, err) require.Len(t, tasks, 3)
if assert.Len(t, tasks, 3) {
assert.Equal(t, int64(3), tasks[0].ID)
assert.Equal(t, int64(2), tasks[1].ID)
assert.Equal(t, int64(1), tasks[2].ID)
}
webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2}) hook = unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, Events: "{}"})
tasks, err = webhook.History(t.Context(), 0) tasks, err = hook.History(t.Context(), 0)
assert.NoError(t, err) assert.NoError(t, err)
assert.Empty(t, tasks) assert.Empty(t, tasks)
} }
func TestWebhook_UpdateEvent(t *testing.T) { func TestWebhook_UpdateEvent(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true})
webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1})
hookEvent := &webhook_module.HookEvent{ hookEvent := &webhook_module.HookEvent{
PushOnly: true, PushOnly: true,
SendEverything: false, SendEverything: false,
@@ -100,10 +166,10 @@ func TestCreateWebhook(t *testing.T) {
} }
func TestGetWebhookByRepoID(t *testing.T) { func TestGetWebhookByRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true})
hook, err := GetWebhookByRepoID(t.Context(), 1, 1) loaded, err := GetWebhookByRepoID(t.Context(), 1, hook.ID)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, int64(1), hook.ID) assert.Equal(t, hook.ID, loaded.ID)
_, err = GetWebhookByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) _, err = GetWebhookByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
assert.Error(t, err) assert.Error(t, err)
@@ -111,10 +177,10 @@ func TestGetWebhookByRepoID(t *testing.T) {
} }
func TestGetWebhookByOwnerID(t *testing.T) { func TestGetWebhookByOwnerID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3})
hook, err := GetWebhookByOwnerID(t.Context(), 3, 3) loaded, err := GetWebhookByOwnerID(t.Context(), 3, hook.ID)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, int64(3), hook.ID) require.Equal(t, hook.ID, loaded.ID)
_, err = GetWebhookByOwnerID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) _, err = GetWebhookByOwnerID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
assert.Error(t, err) assert.Error(t, err)
@@ -122,48 +188,45 @@ func TestGetWebhookByOwnerID(t *testing.T) {
} }
func TestGetActiveWebhooksByRepoID(t *testing.T) { func TestGetActiveWebhooksByRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true})
hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)}) hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)})
assert.NoError(t, err) require.NoError(t, err)
if assert.Len(t, hooks, 1) { require.Len(t, hooks, 1)
assert.Equal(t, int64(1), hooks[0].ID) assert.Equal(t, hook.ID, hooks[0].ID)
assert.True(t, hooks[0].IsActive) assert.True(t, hooks[0].IsActive)
}
} }
func TestGetWebhooksByRepoID(t *testing.T) { func TestGetWebhooksByRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{RepoID: 1}) hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{RepoID: 1})
assert.NoError(t, err) require.NoError(t, err)
if assert.Len(t, hooks, 2) { require.Len(t, hooks, 2)
assert.Equal(t, int64(1), hooks[0].ID) assert.Equal(t, int64(1), hooks[0].RepoID)
assert.Equal(t, int64(2), hooks[1].ID) assert.True(t, hooks[0].IsActive)
} assert.Equal(t, int64(1), hooks[1].RepoID)
assert.False(t, hooks[1].IsActive)
} }
func TestGetActiveWebhooksByOwnerID(t *testing.T) { func TestGetActiveWebhooksByOwnerID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)}) hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)})
assert.NoError(t, err) require.NoError(t, err)
if assert.Len(t, hooks, 1) { require.Len(t, hooks, 1)
assert.Equal(t, int64(3), hooks[0].ID) assert.Equal(t, int64(3), hooks[0].OwnerID)
assert.True(t, hooks[0].IsActive) assert.True(t, hooks[0].IsActive)
}
} }
func TestGetWebhooksByOwnerID(t *testing.T) { func TestGetWebhooksByOwnerID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{OwnerID: 3}) hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{OwnerID: 3})
assert.NoError(t, err) require.NoError(t, err)
if assert.Len(t, hooks, 1) { require.Len(t, hooks, 2)
assert.Equal(t, int64(3), hooks[0].ID) assert.Equal(t, int64(3), hooks[0].OwnerID)
assert.True(t, hooks[0].IsActive) assert.True(t, hooks[0].IsActive)
} assert.Equal(t, int64(3), hooks[1].OwnerID)
assert.False(t, hooks[1].IsActive)
} }
func TestUpdateWebhook(t *testing.T) { func TestUpdateWebhook(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, Events: `{}`})
hook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2}) require.False(t, hook.IsActive)
hook.IsActive = true hook.IsActive = true
hook.ContentType = ContentTypeForm hook.ContentType = ContentTypeForm
unittest.AssertNotExistsBean(t, hook) unittest.AssertNotExistsBean(t, hook)
@@ -172,48 +235,36 @@ func TestUpdateWebhook(t *testing.T) {
} }
func TestDeleteWebhookByRepoID(t *testing.T) { func TestDeleteWebhookByRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, Events: `{}`})
unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2, RepoID: 1}) assert.NoError(t, DeleteWebhookByRepoID(t.Context(), 1, hook.ID))
assert.NoError(t, DeleteWebhookByRepoID(t.Context(), 1, 2)) unittest.AssertNotExistsBean(t, &Webhook{ID: hook.ID, RepoID: 1})
unittest.AssertNotExistsBean(t, &Webhook{ID: 2, RepoID: 1})
err := DeleteWebhookByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) err := DeleteWebhookByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
assert.Error(t, err)
assert.True(t, IsErrWebhookNotExist(err)) assert.True(t, IsErrWebhookNotExist(err))
} }
func TestDeleteWebhookByOwnerID(t *testing.T) { func TestDeleteWebhookByOwnerID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3, Events: `{}`})
unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OwnerID: 3}) assert.NoError(t, DeleteWebhookByOwnerID(t.Context(), 3, hook.ID))
assert.NoError(t, DeleteWebhookByOwnerID(t.Context(), 3, 3)) unittest.AssertNotExistsBean(t, &Webhook{ID: hook.ID, OwnerID: 3})
unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OwnerID: 3})
err := DeleteWebhookByOwnerID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) err := DeleteWebhookByOwnerID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)
assert.Error(t, err)
assert.True(t, IsErrWebhookNotExist(err)) assert.True(t, IsErrWebhookNotExist(err))
} }
func TestHookTasks(t *testing.T) { func TestHookTasks(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true})
hookTasks, err := HookTasks(t.Context(), 1, 1) hookTasks, err := HookTasks(t.Context(), hook.ID, 1)
assert.NoError(t, err) assert.NoError(t, err)
if assert.Len(t, hookTasks, 3) { assert.Len(t, hookTasks, 3)
assert.Equal(t, int64(3), hookTasks[0].ID)
assert.Equal(t, int64(2), hookTasks[1].ID)
assert.Equal(t, int64(1), hookTasks[2].ID)
}
hookTasks, err = HookTasks(t.Context(), unittest.NonexistentID, 1) hookTasks, err = HookTasks(t.Context(), unittest.NonexistentID, 1)
assert.NoError(t, err) assert.NoError(t, err)
assert.Empty(t, hookTasks) assert.Empty(t, hookTasks)
} }
func TestCreateHookTask(t *testing.T) { func TestCreateHookTask(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3, IsActive: true})
hookTask := &HookTask{ hookTask := &HookTask{HookID: hook.ID, PayloadVersion: 2}
HookID: 3,
PayloadVersion: 2,
}
unittest.AssertNotExistsBean(t, hookTask) unittest.AssertNotExistsBean(t, hookTask)
_, err := CreateHookTask(t.Context(), hookTask) _, err := CreateHookTask(t.Context(), hookTask)
assert.NoError(t, err) assert.NoError(t, err)
@@ -221,20 +272,23 @@ func TestCreateHookTask(t *testing.T) {
} }
func TestUpdateHookTask(t *testing.T) { func TestUpdateHookTask(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3, IsActive: true})
hookTask := &HookTask{HookID: hook.ID, PayloadVersion: 2}
_, err := CreateHookTask(t.Context(), hookTask)
assert.NoError(t, err)
hook := unittest.AssertExistsAndLoadBean(t, &HookTask{ID: 1}) hookTask.PayloadContent = "new payload content"
hook.PayloadContent = "new payload content" hookTask.IsDelivered = true
hook.IsDelivered = true unittest.AssertNotExistsBean(t, hookTask)
unittest.AssertNotExistsBean(t, hook) assert.NoError(t, UpdateHookTask(t.Context(), hookTask))
assert.NoError(t, UpdateHookTask(t.Context(), hook)) unittest.AssertExistsAndLoadBean(t, hookTask)
unittest.AssertExistsAndLoadBean(t, hook)
} }
func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup1", ContentType: ContentTypeJSON, Events: `{"push_only":true}`}
require.NoError(t, db.Insert(t.Context(), hook))
hookTask := &HookTask{ hookTask := &HookTask{
HookID: 3, HookID: hook.ID,
IsDelivered: true, IsDelivered: true,
Delivered: timeutil.TimeStampNanoNow(), Delivered: timeutil.TimeStampNanoNow(),
PayloadVersion: 2, PayloadVersion: 2,
@@ -249,9 +303,10 @@ func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
} }
func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup2", ContentType: ContentTypeJSON, Events: `{"push_only":true}`}
require.NoError(t, db.Insert(t.Context(), hook))
hookTask := &HookTask{ hookTask := &HookTask{
HookID: 4, HookID: hook.ID,
IsDelivered: false, IsDelivered: false,
PayloadVersion: 2, PayloadVersion: 2,
} }
@@ -265,9 +320,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
} }
func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup3", ContentType: ContentTypeJSON, Events: `{"push_only":true}`}
require.NoError(t, db.Insert(t.Context(), hook))
hookTask := &HookTask{ hookTask := &HookTask{
HookID: 4, HookID: hook.ID,
IsDelivered: true, IsDelivered: true,
Delivered: timeutil.TimeStampNanoNow(), Delivered: timeutil.TimeStampNanoNow(),
PayloadVersion: 2, PayloadVersion: 2,
@@ -282,9 +338,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
} }
func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup4", ContentType: ContentTypeJSON, Events: `{"push_only":true}`}
require.NoError(t, db.Insert(t.Context(), hook))
hookTask := &HookTask{ hookTask := &HookTask{
HookID: 3, HookID: hook.ID,
IsDelivered: true, IsDelivered: true,
Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()), Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()),
PayloadVersion: 2, PayloadVersion: 2,
@@ -299,9 +356,10 @@ func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
} }
func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup5", ContentType: ContentTypeJSON, Events: `{"push_only":true}`}
require.NoError(t, db.Insert(t.Context(), hook))
hookTask := &HookTask{ hookTask := &HookTask{
HookID: 4, HookID: hook.ID,
IsDelivered: false, IsDelivered: false,
PayloadVersion: 2, PayloadVersion: 2,
} }
@@ -315,9 +373,10 @@ func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
} }
func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup6", ContentType: ContentTypeJSON, Events: `{"push_only":true}`}
require.NoError(t, db.Insert(t.Context(), hook))
hookTask := &HookTask{ hookTask := &HookTask{
HookID: 4, HookID: hook.ID,
IsDelivered: true, IsDelivered: true,
Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()), Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()),
PayloadVersion: 2, PayloadVersion: 2,
-6
View File
@@ -83,12 +83,6 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
return ret, nil return ret, nil
} }
func WithJobResults(results map[string]string) ParseOption {
return func(c *parseContext) {
c.jobResults = results
}
}
func WithGitContext(context *model.GithubContext) ParseOption { func WithGitContext(context *model.GithubContext) ParseOption {
return func(c *parseContext) { return func(c *parseContext) {
c.gitContext = context c.gitContext = context
+2 -1
View File
@@ -4,6 +4,7 @@
package dump package dump
import ( import (
"archive/zip"
"context" "context"
"errors" "errors"
"fmt" "fmt"
@@ -85,7 +86,7 @@ func NewDumper(ctx context.Context, format string, output io.Writer) (*Dumper, e
var comp archives.ArchiverAsync var comp archives.ArchiverAsync
switch format { switch format {
case "zip": case "zip":
comp = archives.Zip{} comp = archives.Zip{Compression: zip.Deflate}
case "tar": case "tar":
comp = archives.Tar{} comp = archives.Tar{}
case "tar.sz": case "tar.sz":
-7
View File
@@ -7,7 +7,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"strings"
"time" "time"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
@@ -110,9 +109,3 @@ func (e *Event) WriteTo(w io.Writer) (int64, error) {
return sum, err return sum, err
} }
func (e *Event) String() string {
buf := new(strings.Builder)
_, _ = e.WriteTo(buf)
return buf.String()
}
+2 -2
View File
@@ -192,13 +192,13 @@ func RunGitTests(m interface{ Run() int }) {
func runGitTests(m interface{ Run() int }) int { func runGitTests(m interface{ Run() int }) int {
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home") gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
if err != nil { if err != nil {
testlogger.Panicf("unable to create temp dir: %s", err.Error()) return testlogger.MainErrorf("unable to create temp dir: %v", err)
} }
defer cleanup() defer cleanup()
setting.Git.HomePath = gitHomePath setting.Git.HomePath = gitHomePath
if err = InitFull(); err != nil { if err = InitFull(); err != nil {
testlogger.Panicf("failed to call Init: %s", err.Error()) return testlogger.MainErrorf("failed to call Init: %v", err)
} }
return m.Run() return m.Run()
} }
+1 -1
View File
@@ -24,7 +24,7 @@ func testMain(m *testing.M) int {
// "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package // "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home") gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
if err != nil { if err != nil {
testlogger.Panicf("failed to create temp dir: %v", err) return testlogger.MainErrorf("failed to create temp dir: %v", err)
} }
defer cleanup() defer cleanup()
+8
View File
@@ -56,6 +56,14 @@ func StderrHasPrefix(err error, prefix string) bool {
return strings.HasPrefix(stderr, prefix) return strings.HasPrefix(stderr, prefix)
} }
func StderrContains(err error, sub string) bool {
stderr, ok := ErrorAsStderr(err)
if !ok {
return false
}
return strings.Contains(stderr, sub)
}
func IsErrorExitCode(err error, code int) bool { func IsErrorExitCode(err error, code int) bool {
var exitError *exec.ExitError var exitError *exec.ExitError
if errors.As(err, &exitError) { if errors.As(err, &exitError) {
-17
View File
@@ -44,23 +44,6 @@ func CommitsCount(ctx context.Context, repo Repository, opts CommitsCountOptions
return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
} }
// CommitsCountBetween return numbers of commits between two commits
func CommitsCountBetween(ctx context.Context, repo Repository, start, end string) (int64, error) {
count, err := CommitsCount(ctx, repo, CommitsCountOptions{
Revision: []string{start + ".." + end},
})
if err != nil && strings.Contains(err.Error(), "no merge base") {
// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
// previously it would return the results of git rev-list before last so let's try that...
return CommitsCount(ctx, repo, CommitsCountOptions{
Revision: []string{start, end},
})
}
return count, err
}
// FileCommitsCount return the number of files at a revision // FileCommitsCount return the number of files at a revision
func FileCommitsCount(ctx context.Context, repo Repository, revision, file string) (int64, error) { func FileCommitsCount(ctx context.Context, repo Repository, revision, file string) (int64, error) {
return CommitsCount(ctx, repo, return CommitsCount(ctx, repo,
-10
View File
@@ -5,21 +5,11 @@ package gitrepo
import ( import (
"context" "context"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/globallock" "code.gitea.io/gitea/modules/globallock"
) )
func GitConfigGet(ctx context.Context, repo Repository, key string) (string, error) {
result, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("config", "--get").
AddDynamicArguments(key))
if err != nil {
return "", err
}
return strings.TrimSpace(result), nil
}
func getRepoConfigLockKey(repoStoragePath string) string { func getRepoConfigLockKey(repoStoragePath string) string {
return "repo-config:" + repoStoragePath return "repo-config:" + repoStoragePath
} }
-5
View File
@@ -78,11 +78,6 @@ func (hl *HostMatchList) AppendBuiltin(builtin string) {
hl.builtins = append(hl.builtins, builtin) hl.builtins = append(hl.builtins, builtin)
} }
// AppendPattern appends more pattern to match
func (hl *HostMatchList) AppendPattern(pattern string) {
hl.patterns = append(hl.patterns, pattern)
}
// IsEmpty checks if the checklist is empty // IsEmpty checks if the checklist is empty
func (hl *HostMatchList) IsEmpty() bool { func (hl *HostMatchList) IsEmpty() bool {
return hl == nil || (len(hl.builtins) == 0 && len(hl.patterns) == 0 && len(hl.ipNets) == 0) return hl == nil || (len(hl.builtins) == 0 && len(hl.patterns) == 0 && len(hl.ipNets) == 0)
+2 -2
View File
@@ -317,7 +317,7 @@ func TestRender_email(t *testing.T) {
func TestRender_emoji(t *testing.T) { func TestRender_emoji(t *testing.T) {
setting.AppURL = markup.TestAppURL setting.AppURL = markup.TestAppURL
setting.StaticURLPrefix = markup.TestAppURL setting.StaticURLPrefix = strings.TrimSuffix(markup.TestAppURL, "/")
test := func(input, expected string) { test := func(input, expected string) {
expected = strings.ReplaceAll(expected, "&", "&amp;") expected = strings.ReplaceAll(expected, "&", "&amp;")
@@ -500,7 +500,7 @@ func Test_ParseClusterFuzz(t *testing.T) {
} }
func TestPostProcess(t *testing.T) { func TestPostProcess(t *testing.T) {
setting.StaticURLPrefix = markup.TestAppURL // can't run standalone setting.StaticURLPrefix = strings.TrimSuffix(markup.TestAppURL, "/") // can't run standalone
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
test := func(input, expected string) { test := func(input, expected string) {
-4
View File
@@ -74,10 +74,6 @@ func (r *GlodmarkRender) Convert(source []byte, writer io.Writer, opts ...parser
return r.goldmarkMarkdown.Convert(source, writer, opts...) return r.goldmarkMarkdown.Convert(source, writer, opts...)
} }
func (r *GlodmarkRender) Renderer() renderer.Renderer {
return r.goldmarkMarkdown.Renderer()
}
func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) { func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
if entering { if entering {
languageBytes, _ := c.Language() languageBytes, _ := c.Language()
Loaded 100 of 714 files, more files were not shown because too many files have changed in this diff. Show more