diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f8e5972af13..4863391f394 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Gitea DevContainer", - "image": "mcr.microsoft.com/devcontainers/go:1.25-trixie", + "image": "mcr.microsoft.com/devcontainers/go:1.26-trixie", "containerEnv": { // override "local" from packaged version "GOTOOLCHAIN": "auto" diff --git a/.github/actions/go-cache/action.yml b/.github/actions/go-cache/action.yml new file mode 100644 index 00000000000..04d4bac3673 --- /dev/null +++ b/.github/actions/go-cache/action.yml @@ -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 }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index be33b8975fc..00000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: 2 - -updates: - - package-ecosystem: github-actions - labels: [modifies/dependencies] - directory: / - schedule: - interval: daily - cooldown: - default-days: 5 diff --git a/.github/workflows/cache-seeder.yml b/.github/workflows/cache-seeder.yml new file mode 100644 index 00000000000..d0801a10783 --- /dev/null +++ b/.github/workflows/cache-seeder.yml @@ -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 }} diff --git a/.github/workflows/cron-flake-updater.yml b/.github/workflows/cron-flake-updater.yml deleted file mode 100644 index c9a1f22a2ae..00000000000 --- a/.github/workflows/cron-flake-updater.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index ee1c3e0c750..edb6f2e1576 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -12,15 +12,15 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod check-latest: true - run: make generate-gitignore timeout-minutes: 40 - name: push translations to repo - uses: appleboy/git-push-action@v1.2.0 + uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0 with: author_email: "teabot@gitea.io" author_name: GiteaBot diff --git a/.github/workflows/cron-renovate.yml b/.github/workflows/cron-renovate.yml new file mode 100644 index 00000000000..edeefc26ad8 --- /dev/null +++ b/.github/workflows/cron-renovate.yml @@ -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"]' diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml index 56a30fb5ba6..17f29d4e0c5 100644 --- a/.github/workflows/cron-translations.yml +++ b/.github/workflows/cron-translations.yml @@ -12,8 +12,8 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v6 - - uses: crowdin/github-action@v2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 with: upload_sources: true upload_translations: false @@ -29,7 +29,7 @@ jobs: - name: update locales run: ./build/update-locales.sh - name: push translations to repo - uses: appleboy/git-push-action@v1.2.0 + uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0 with: author_email: "teabot@gitea.io" author_name: GiteaBot diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index b1d6fbe9d8e..5fd43e6cef1 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -42,8 +42,8 @@ jobs: json: ${{ steps.changes.outputs.json }} e2e: ${{ steps.changes.outputs.e2e }} steps: - - uses: actions/checkout@v6 - - uses: dorny/paths-filter@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 id: changes with: filters: | diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index b4f5002082c..b057962a21b 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -20,11 +20,16 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - 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: lint-backend + lint-cache: "true" - run: make deps-backend deps-tools - run: make lint-backend env: @@ -37,11 +42,11 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: astral-sh/setup-uv@v8.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - run: uv python install 3.14 - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 cache: pnpm @@ -57,8 +62,8 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: astral-sh/setup-uv@v8.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - run: uv python install 3.14 - run: make deps-py - run: make lint-yaml @@ -70,11 +75,13 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - run: make deps-frontend - run: make lint-json @@ -85,9 +92,9 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 cache: pnpm @@ -102,8 +109,8 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod check-latest: true @@ -116,11 +123,16 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - 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: lint-go-windows + lint-cache: "true" - run: make deps-backend deps-tools - run: make lint-go-windows env: @@ -135,11 +147,16 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - 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: lint-go-gogit + lint-cache: "true" - run: make deps-backend deps-tools - run: make lint-go env: @@ -152,11 +169,16 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - 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: checks-backend + build-cache: "false" - run: make deps-backend deps-tools - run: make --always-make checks-backend # ensure the "go-licenses" make target runs @@ -167,9 +189,9 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 cache: pnpm @@ -187,11 +209,15 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - 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: backend # no frontend build here as backend should be able to build # even without any frontend files - run: make deps-backend @@ -221,9 +247,9 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 cache: pnpm @@ -238,8 +264,8 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod check-latest: true diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index d168c2ecc5f..d49fc33dadd 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -42,11 +42,15 @@ jobs: ports: - "9000:9000" steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - 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: pgsql - 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: make deps-backend @@ -60,7 +64,6 @@ jobs: timeout-minutes: 50 env: TAGS: bindata gogit - RACE_ENABLED: true TEST_TAGS: gogit TEST_LDAP: 1 @@ -71,11 +74,15 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - 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: sqlite - run: make deps-backend - run: GOEXPERIMENT='' make backend env: @@ -130,11 +137,16 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - 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: unit + build-cache-rotate: "true" - 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: make deps-backend @@ -142,16 +154,18 @@ jobs: env: TAGS: bindata - name: unit-tests - run: make unit-test-coverage test-check + run: make test-backend test-check env: TAGS: bindata RACE_ENABLED: true + GOTESTFLAGS: -timeout=20m GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} - name: unit-tests-gogit - run: GOEXPERIMENT='' make unit-test-coverage test-check + run: GOEXPERIMENT='' make test-backend test-check env: TAGS: bindata gogit RACE_ENABLED: true + GOTESTFLAGS: -timeout=20m GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} test-mysql: @@ -185,11 +199,15 @@ jobs: - "587:587" - "993:993" steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - 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: mysql - 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: make deps-backend @@ -203,7 +221,6 @@ jobs: run: make test-mysql env: TAGS: bindata - RACE_ENABLED: true TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" test-mssql: @@ -226,11 +243,15 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - 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: mssql - 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: make deps-backend diff --git a/.github/workflows/pull-docker-dryrun.yml b/.github/workflows/pull-docker-dryrun.yml index 201825ccbaa..e0c0fff815d 100644 --- a/.github/workflows/pull-docker-dryrun.yml +++ b/.github/workflows/pull-docker-dryrun.yml @@ -20,18 +20,18 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: docker/setup-qemu-action@v4 - - uses: docker/setup-buildx-action@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build regular container image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 push: false cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful - name: Build rootless container image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . push: false diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index fde36383b93..afa95870227 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -20,13 +20,18 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 + cache: false + - 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: node-version: 24 cache: pnpm diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml index d05483e56ca..f9e2e5e07b7 100644 --- a/.github/workflows/pull-labeler.yml +++ b/.github/workflows/pull-labeler.yml @@ -15,6 +15,6 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/labeler@v6 + - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: sync-labels: true diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index eaebccd7fbe..a5fa452ef36 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -14,16 +14,16 @@ jobs: permissions: contents: read 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 tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v6 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 cache: pnpm @@ -35,7 +35,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify - name: import gpg key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v7 + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -52,7 +52,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v6 + uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -67,18 +67,18 @@ jobs: contents: read packages: write # to publish to ghcr.io 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 tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v4 - - uses: docker/setup-buildx-action@v4 + - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Get cleaned branch name id: clean_name run: | 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" - - uses: docker/metadata-action@v6 + - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 id: meta with: images: |- @@ -88,7 +88,7 @@ jobs: type=raw,value=${{ steps.clean_name.outputs.branch }} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@v6 + - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 id: meta_rootless with: images: |- @@ -102,18 +102,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular docker image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . 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-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max - name: build rootless docker image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index 248fa532eeb..2e0f2dd5c0b 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -15,16 +15,16 @@ jobs: permissions: contents: read 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 tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v6 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 cache: pnpm @@ -36,7 +36,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify - name: import gpg key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v7 + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -53,7 +53,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v6 + uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -62,7 +62,7 @@ jobs: run: | aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress - 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: gh-cli-version: 2.39.1 - name: create github release @@ -77,13 +77,13 @@ jobs: contents: read packages: write # to publish to ghcr.io 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 tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v4 - - uses: docker/setup-buildx-action@v4 - - uses: docker/metadata-action@v6 + - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 id: meta with: images: |- @@ -96,7 +96,7 @@ jobs: type=semver,pattern={{version}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@v6 + - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 id: meta_rootless with: images: |- @@ -112,18 +112,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular container image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -131,7 +131,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index 1e84ae1739f..2e7a9f5f54c 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -18,16 +18,16 @@ jobs: contents: read packages: write # to publish to ghcr.io 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 tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v6 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 cache: pnpm @@ -39,7 +39,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify - name: import gpg key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v7 + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -56,7 +56,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v6 + uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -65,7 +65,7 @@ jobs: run: | aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress - 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: gh-cli-version: 2.39.1 - name: create github release @@ -80,13 +80,13 @@ jobs: contents: read packages: write # to publish to ghcr.io 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 tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v4 - - uses: docker/setup-buildx-action@v4 - - uses: docker/metadata-action@v6 + - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 id: meta with: images: |- @@ -103,7 +103,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@v6 + - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 id: meta_rootless with: images: |- @@ -124,18 +124,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular container image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -143,7 +143,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 diff --git a/AGENTS.md b/AGENTS.md index 6c7e50fea4f..fd87f432b71 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,9 @@ - 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 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 ` +- Run single playwright e2e test files with `GITEA_TEST_E2E_FLAGS='' make test-e2e` - Add the current year into the copyright header of new `.go` 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 05c56dc84fc..c3b6b94269b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 * BREAKING diff --git a/Makefile b/Makefile index 0e4d68bffe0..ae053a8368e 100644 --- a/Makefile +++ b/Makefile @@ -12,15 +12,15 @@ COMMA := , XGO_VERSION := go-1.25.x -AIR_PACKAGE ?= github.com/air-verse/air@v1 -EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 -GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 -MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 +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 # renovate: datasource=go +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 # renovate: datasource=go +MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 # renovate: datasource=go XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest -GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 -ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.11 +GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go +ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.11 # renovate: datasource=go DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest @@ -518,7 +518,8 @@ test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test .PHONY: playwright 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 test-e2e: playwright $(EXECUTABLE_E2E) diff --git a/cmd/admin_user_change_password_test.go b/cmd/admin_user_change_password_test.go index 902632f3e49..cf497517f74 100644 --- a/cmd/admin_user_change_password_test.go +++ b/cmd/admin_user_change_password_test.go @@ -4,6 +4,7 @@ package cmd import ( + "io" "testing" "code.gitea.io/gitea/models/db" @@ -82,7 +83,9 @@ func TestChangePasswordCommand(t *testing.T) { for _, tc := range testCases { 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.Contains(t, err.Error(), tc.expectedErr) }) diff --git a/cmd/cert_test.go b/cmd/cert_test.go index 4242d8915b3..c5775e52048 100644 --- a/cmd/cert_test.go +++ b/cmd/cert_test.go @@ -4,6 +4,7 @@ package cmd import ( + "io" "path/filepath" "testing" @@ -107,6 +108,7 @@ func TestCertCommandFailures(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { app := cmdCert() + app.Writer, app.ErrWriter = io.Discard, io.Discard tempDir := t.TempDir() certFile := filepath.Join(tempDir, "cert.pem") diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go deleted file mode 100644 index a36d05c76e0..00000000000 --- a/cmd/cmd_test.go +++ /dev/null @@ -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) -} diff --git a/cmd/cmdtest/cmd_test.go b/cmd/cmdtest/cmd_test.go new file mode 100644 index 00000000000..4ff854f8abf --- /dev/null +++ b/cmd/cmdtest/cmd_test.go @@ -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"]) +} diff --git a/cmd/cmd.go b/cmd/helper.go similarity index 98% rename from cmd/cmd.go rename to cmd/helper.go index 25e90a16950..9d70b057015 100644 --- a/cmd/cmd.go +++ b/cmd/helper.go @@ -124,7 +124,7 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cl if setting.InstallLock { // 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. - 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") } level := defaultLevel diff --git a/cmd/main.go b/cmd/main.go index a6b89a6fada..27d8cba2e90 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -48,7 +48,7 @@ DEFAULT CONFIGURATION: } } -func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) { +func PrepareSubcommandWithGlobalFlags(originCmd *cli.Command) { originBefore := originCmd.Before originCmd.Before = func(ctxOrig context.Context, cmd *cli.Command) (ctx context.Context, err error) { ctx = ctxOrig @@ -145,7 +145,7 @@ func NewMainApp(appVer AppVersion) *cli.Command { app.Before = PrepareConsoleLoggerLevel(log.INFO) for i := range subCmdWithConfig { - prepareSubcommandWithGlobalFlags(subCmdWithConfig[i]) + PrepareSubcommandWithGlobalFlags(subCmdWithConfig[i]) } app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdStandalone...) diff --git a/cmd/main_test.go b/cmd/main_test.go index b1f6bb3ba9b..f367bf12e42 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -5,17 +5,9 @@ package cmd import ( "context" - "errors" - "fmt" - "io" - "path/filepath" - "strings" "testing" "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" @@ -25,209 +17,28 @@ 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 := NewMainApp(AppVersion{}) - testCmd.Name = util.IfZero(testCmd.Name, "test-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 := 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 +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 + }, }, - }) - 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) - } - }) + }, + } + assert.NoError(t, cmd.Run(t.Context(), args)) + assert.True(t, called) } -} - -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"]) + test(t, []string{"./gitea"}, "", true) + test(t, []string{"./gitea", "test"}, "", true) + test(t, []string{"./gitea", "other"}, "other", false) } diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index ef276e4da58..97af5fa5fbd 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -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. ;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 +;; +;; 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://gitea.com/gitea/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 ;; in a repo is used to find Actions workflow files ;WORKFLOW_DIRS = .gitea/workflows,.github/workflows +;; Maximum number of attempts a single workflow run can have. Default value is 50. +;MAX_RERUN_ATTEMPTS = 50 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/flake.lock b/flake.lock index 2130399c1dd..839eaed572d 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1776169885, - "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", + "lastModified": 1776877367, + "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", + "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index d1aac0db900..d7577bfbf07 100644 --- a/go.mod +++ b/go.mod @@ -27,8 +27,8 @@ require ( github.com/PuerkitoBio/goquery v1.12.0 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0 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/service/codecommit v1.33.12 + github.com/aws/aws-sdk-go-v2/credentials v1.19.15 + github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.13 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blevesearch/bleve/v2 v2.5.7 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/chi-middleware/proxy v1.1.1 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/dustin/go-humanize v1.0.1 github.com/editorconfig/editorconfig-core-go/v2 v2.6.4 @@ -49,14 +49,14 @@ require ( github.com/gliderlabs/ssh v0.3.8 github.com/go-chi/chi/v5 v5.2.5 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-git/go-billy/v5 v5.8.0 github.com/go-git/go-git/v5 v5.18.0 github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-redsync/redsync/v4 v4.16.0 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/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 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/cascadia v1.3.3 // 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/internal/configsources v1.4.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect - github.com/aws/smithy-go v1.24.2 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.6 // 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.22 // indirect + github.com/aws/smithy-go v1.25.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // 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/spf13/afero v1.15.0 // 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/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect diff --git a/go.sum b/go.sum index 547c61d826c..0b65e6305ff 100644 --- a/go.sum +++ b/go.sum @@ -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/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/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= -github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= -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.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w= -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.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= -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.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= -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.12/go.mod h1:voO3LP/dZ4CTERiNWCz3DFLbK/8hbfeC1OJkLW+sang= -github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= -github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg= +github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ= +github.com/aws/aws-sdk-go-v2/credentials v1.19.15 h1:fyvgWTszojq8hEnMi8PPBTvZdTtEVmAVyo+NFLHBhH4= +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.22 h1:GmLa5Kw1ESqtFpXsx5MmC84QWa/ZrLZvlJGa2y+4kcQ= +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.22 h1:dY4kWZiSaXIzxnKlj17nHnBcXXBfac6UlsAx2qL6XrU= +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.13 h1:IIW5QmNI9PrnDTBCPa75HcD0g+hoD/a+d388dbIAkEM= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.13/go.mod h1:B1FUSufCQp3d8VUzob1EY+AdSo2OuEWNEY6GQff7EHA= +github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U= +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/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 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.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.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= -github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= @@ -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/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-co-op/gocron/v2 v2.20.0 h1:9IMrnnVSWjfSh3E54gWmWCHbloQJLh6f9+nwyKfLNpc= -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 h1:e1nt9AEFglarRH9/9y9q0V5sblwxlknpHPjttEajrwQ= +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/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= 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-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= 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.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4= +github.com/go-webauthn/webauthn v0.16.5 h1:x+vADHlaiIjta23kGhtwyCIlB5mayKx6SBlpwQ5NF9A= +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/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk= 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/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= 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.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= +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/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= diff --git a/models/actions/artifact.go b/models/actions/artifact.go index ffadc79661a..f0effdeecaf 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -12,6 +12,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -61,7 +62,8 @@ const ( // ActionArtifact is a file that is stored in the artifact storage. type ActionArtifact struct { 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 RepoID int64 `xorm:"index"` OwnerID int64 @@ -80,9 +82,9 @@ type ActionArtifact struct { // * "application/pdf", "text/html", etc.: real content type of the artifact ContentEncodingOrType string `xorm:"content_encoding"` - ArtifactPath string `xorm:"index unique(runid_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 - Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete + ArtifactPath string `xorm:"index unique(runid_attempt_name_path)"` // The path to 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 CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` 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 { 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) { artifact := &ActionArtifact{ ArtifactName: artifactName, ArtifactPath: artifactPath, RunID: t.Job.RunID, + RunAttemptID: t.Job.RunAttemptID, RunnerID: t.RunnerID, RepoID: t.RepoID, OwnerID: t.OwnerID, @@ -122,9 +125,9 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa 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 - 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 { return nil, err } else if !has { @@ -144,6 +147,7 @@ type FindArtifactsOptions struct { db.ListOptions RepoID int64 RunID int64 + RunAttemptID optional.Option[int64] // use optional to allow filtering by zero (legacy artifacts have run_attempt_id=0) ArtifactName string Status int FinalizedArtifactsV4 bool @@ -163,6 +167,9 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond { if opts.RunID > 0 { 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 != "" { cond = cond.And(builder.Eq{"artifact_name": opts.ArtifactName}) } @@ -186,11 +193,12 @@ type ActionArtifactMeta struct { ExpiredUnix timeutil.TimeStamp } -// ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run -func ListUploadedArtifactsMeta(ctx context.Context, repoID, runID int64) ([]*ActionArtifactMeta, error) { +// ListUploadedArtifactsMetaByRunAttempt returns uploaded artifacts meta scoped to a specific run and attempt. +// 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) 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"). Select("artifact_name, sum(file_size) as file_size, max(status) as status, max(expired_unix) as expired_unix"). Find(&arts) @@ -217,12 +225,29 @@ func SetArtifactExpired(ctx context.Context, artifactID int64) error { return err } -// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it -func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error { - _, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion}) +// SetArtifactNeedDeleteByID sets an artifact to need-delete by ID, cron job will delete it. +func SetArtifactNeedDeleteByID(ctx context.Context, artifactID int64) error { + _, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion}) 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 func SetArtifactDeleted(ctx context.Context, artifactID int64) error { _, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusDeleted}) diff --git a/models/actions/run.go b/models/actions/run.go index bce356c0e2b..b4f4d001715 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -30,7 +30,7 @@ import ( type ActionRun struct { ID int64 Title string - RepoID int64 `xorm:"unique(repo_index) index(repo_concurrency)"` + RepoID int64 `xorm:"unique(repo_index)"` Repo *repo_model.Repository `xorm:"-"` OwnerID int64 `xorm:"index"` WorkflowID string `xorm:"index"` // the name of workflow file @@ -50,15 +50,20 @@ type ActionRun struct { Status Status `xorm:"index"` Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed RawConcurrency string // raw concurrency - ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` - ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` - // Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0 + + // Started and Stopped are identical to the latest attempt after ActionRunAttempt was introduced. + // When a rerun creates a new latest attempt, they are reset until the new attempt starts and stops. Started timeutil.TimeStamp 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 - 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() { @@ -115,7 +120,7 @@ func (run *ActionRun) RefTooltip() string { } // 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 { return nil } @@ -129,11 +134,10 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error { } 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 { return err } - run.TriggerUser = u } return nil @@ -160,6 +164,31 @@ func (run *ActionRun) Duration() time.Duration { 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) { if run.Event == webhook_module.HookEventPush { var payload api.PushPayload @@ -406,14 +435,11 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { type ActionRunIndex db.ResourceIndex -func GetConcurrentRunsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRun, []*ActionRunJob, error) { - runs, err := db.Find[ActionRun](ctx, &FindRunOptions{ - RepoID: repoID, - ConcurrencyGroup: concurrencyGroup, - Status: status, - }) +// GetConcurrentRunAttemptsAndJobs returns run attempts and jobs in the same concurrency group by statuses. +func GetConcurrentRunAttemptsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRunAttempt, []*ActionRunJob, error) { + attempts, err := FindConcurrentRunAttempts(ctx, repoID, concurrencyGroup, status) 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{ @@ -425,36 +451,34 @@ func GetConcurrentRunsAndJobs(ctx context.Context, repoID int64, concurrencyGrou 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) { - if actionRun.ConcurrencyGroup == "" { +func CancelPreviousJobsByRunConcurrency(ctx context.Context, attempt *ActionRunAttempt) ([]*ActionRunJob, error) { + if attempt.ConcurrencyGroup == "" { return nil, nil } var jobsToCancel []*ActionRunJob statusFindOption := []Status{StatusWaiting, StatusBlocked} - if actionRun.ConcurrencyCancel { + if attempt.ConcurrencyCancel { 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 { return nil, fmt.Errorf("find concurrent runs and jobs: %w", err) } jobsToCancel = append(jobsToCancel, jobs...) // cancel runs in the same concurrency group - for _, run := range runs { - if run.ID == actionRun.ID { + for _, concurrentAttempt := range attempts { + if concurrentAttempt.RunID == attempt.RunID { continue } - jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{ - RunID: run.ID, - }) + jobs, err := GetRunJobsByRunAndAttemptID(ctx, concurrentAttempt.RunID, concurrentAttempt.ID) 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...) } diff --git a/models/actions/run_attempt.go b/models/actions/run_attempt.go new file mode 100644 index 00000000000..8ef2ddf00a3 --- /dev/null +++ b/models/actions/run_attempt.go @@ -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") +} diff --git a/models/actions/run_attempt_list.go b/models/actions/run_attempt_list.go new file mode 100644 index 00000000000..77a5b8f15c9 --- /dev/null +++ b/models/actions/run_attempt_list.go @@ -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) +} diff --git a/models/actions/run_job.go b/models/actions/run_job.go index d1e5d1e9380..09213299978 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -34,7 +34,10 @@ type ActionRunJob struct { CommitSHA string `xorm:"index"` IsForkPullRequest bool 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 // 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 Needs []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 @@ -61,6 +67,14 @@ type ActionRunJob struct { // It is JSON-encoded repo_model.ActionsTokenPermissions and may be empty if not specified. 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 Stopped timeutil.TimeStamp 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) } +func (job *ActionRunJob) EffectiveTaskID() int64 { + if job.TaskID > 0 { + return job.TaskID + } + return job.SourceTaskID +} + func (job *ActionRunJob) LoadRun(ctx context.Context) error { if job.Run == nil { run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID) @@ -152,9 +173,50 @@ func GetRunJobByRunAndID(ctx context.Context, runID, jobID int64) (*ActionRunJob 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 - 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 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. - // So we need load the run and its jobs before updating the run. - run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID) - if err != nil { - return 0, err - } - jobs, err := GetRunJobsByRunID(ctx, 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) + // Other goroutines may aggregate the status of the attempt/run and update it too. + // So we need to load the current jobs before updating the aggregate state. + if job.RunAttemptID > 0 { + attempt, err := GetRunAttemptByRepoAndID(ctx, job.RepoID, job.RunAttemptID) + if err != nil { + return 0, err + } + jobs, err := GetRunJobsByRunAndAttemptID(ctx, job.RunID, job.RunAttemptID) + if err != nil { + return 0, err + } + attempt.Status = AggregateJobStatus(jobs) + if attempt.Started.IsZero() && attempt.Status.IsRunning() { + attempt.Started = timeutil.TimeStampNow() + } + if attempt.Stopped.IsZero() && attempt.Status.IsDone() { + attempt.Stopped = timeutil.TimeStampNow() + } + 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 { 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 { 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...) // cancel runs in the same concurrency group - for _, run := range runs { - jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{ - RunID: run.ID, - }) + for _, attempt := range attempts { + if attempt.ID == job.RunAttemptID { + continue + } + jobs, err := GetRunJobsByRunAndAttemptID(ctx, attempt.RunID, attempt.ID) 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...) } diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index 10f76d3641b..e06b6beb9ec 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "xorm.io/builder" @@ -70,6 +71,7 @@ func (jobs ActionJobList) LoadAttributes(ctx context.Context, withRepo bool) err type FindRunJobOptions struct { db.ListOptions RunID int64 + RunAttemptID optional.Option[int64] // use optional to allow filtering by zero (legacy jobs have run_attempt_id=0) RepoID int64 OwnerID int64 CommitSHA string @@ -83,6 +85,9 @@ func (opts FindRunJobOptions) ToConds() builder.Cond { if opts.RunID > 0 { 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 { cond = cond.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID}) } diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 2628c4712f5..82dc97f3e5a 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -7,7 +7,6 @@ import ( "context" "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" "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 { userIDs := runs.GetUserIDs() users := make(map[int64]*user_model.User, len(userIDs)) @@ -50,18 +43,6 @@ func (runs RunList) LoadTriggerUser(ctx context.Context) error { 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 { db.ListOptions RepoID int64 @@ -102,12 +83,6 @@ func (opts FindRunOptions) ToConds() builder.Cond { if 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 } diff --git a/models/actions/schedule_list.go b/models/actions/schedule_list.go index 5361b94801a..6b5cae94fe9 100644 --- a/models/actions/schedule_list.go +++ b/models/actions/schedule_list.go @@ -4,62 +4,13 @@ package actions import ( - "context" - "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" ) 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 { db.ListOptions RepoID int64 diff --git a/models/actions/task.go b/models/actions/task.go index 28928c2bc6f..016f91a7bb3 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -272,7 +272,6 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask } now := timeutil.TimeStampNow() - job.Attempt++ job.Started = now job.Status = StatusRunning diff --git a/models/activities/action.go b/models/activities/action.go index 8e589eda88d..4ffdca842a4 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -186,15 +186,7 @@ func (a *Action) LoadActUser(ctx context.Context) { if a.ActUser != nil { return } - var err error - 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) - } + a.ActUserID, a.ActUser, _ = user_model.GetPossibleUserByID(ctx, a.ActUserID) } func (a *Action) LoadRepo(ctx context.Context) error { diff --git a/models/activities/action_list.go b/models/activities/action_list.go index 29ff2fdf7a2..5b07a8e080a 100644 --- a/models/activities/action_list.go +++ b/models/activities/action_list.go @@ -282,9 +282,3 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err 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{}) -} diff --git a/models/asymkey/error.go b/models/asymkey/error.go index b7656245791..5df7beb8cd7 100644 --- a/models/asymkey/error.go +++ b/models/asymkey/error.go @@ -192,28 +192,6 @@ func (err ErrGPGKeyIDAlreadyUsed) Unwrap() error { 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. type ErrKeyAccessDenied struct { UserID int64 diff --git a/models/asymkey/ssh_key_deploy.go b/models/asymkey/ssh_key_deploy.go index 4ab84eabcf6..ea3d93e8c81 100644 --- a/models/asymkey/ssh_key_deploy.go +++ b/models/asymkey/ssh_key_deploy.go @@ -105,14 +105,6 @@ func addDeployKey(ctx context.Context, keyID, repoID int64, name, fingerprint st 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. func AddDeployKey(ctx context.Context, repoID int64, name, content string, readOnly bool) (*DeployKey, error) { fingerprint, err := CalcFingerprint(content) diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index 846c386a20c..d5a5e2af8e8 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -217,7 +217,7 @@ func (app *OAuth2Application) GetGrantByUserID(ctx context.Context, userID int64 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) { grant := &OAuth2Grant{ 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 { ID int64 `xorm:"pk autoincr"` UserID int64 `xorm:"INDEX unique(user_application)"` @@ -627,7 +627,7 @@ func GetActiveOAuth2SourceByAuthName(ctx context.Context, name string) (*Source, } 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 diff --git a/models/auth/oauth2_test.go b/models/auth/oauth2_test.go index 88ae065652c..d72e1cb1d52 100644 --- a/models/auth/oauth2_test.go +++ b/models/auth/oauth2_test.go @@ -12,19 +12,30 @@ import ( "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestOAuth2AuthorizationCodeValidity(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func TestOAuth2AuthorizationCode(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) t.Run("GenerateSetsValidUntil", func(t *testing.T) { grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1}) expectedValidUntil := timeutil.TimeStamp(time.Now().Unix() + 600) 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.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())) + + 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) { @@ -34,13 +45,14 @@ func TestOAuth2AuthorizationCodeValidity(t *testing.T) { assert.True(t, code.IsExpired()) }) - t.Run("InvalidateTwice", func(t *testing.T) { - code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode") - assert.NoError(t, err) - if assert.NotNil(t, code) { - assert.NoError(t, code.Invalidate(t.Context())) - assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated) - } + t.Run("Invalidate", func(t *testing.T) { + grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1}) + code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "") + require.NoError(t, err) + require.NotNil(t, code) + 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 -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) { // test plain 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()) } -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) { assert.Equal(t, "oauth2_authorization_code", new(auth_model.OAuth2AuthorizationCode).TableName()) } diff --git a/models/auth/webauthn.go b/models/auth/webauthn.go index 6d8b5429579..7bd79ed3f52 100644 --- a/models/auth/webauthn.go +++ b/models/auth/webauthn.go @@ -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{}) 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 -} diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index b7d1201189f..1df02e48090 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -89,7 +89,7 @@ ref: "refs/heads/test" commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" event: "push" - trigger_event: "push" + trigger_event: "schedule" is_fork_pull_request: 0 status: 1 started: 1683636528 diff --git a/models/fixtures/hook_task.yml b/models/fixtures/hook_task.yml index e19eeb03687..01918b35eeb 100644 --- a/models/fixtures/hook_task.yml +++ b/models/fixtures/hook_task.yml @@ -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 diff --git a/models/fixtures/oauth2_application.yml b/models/fixtures/oauth2_application.yml index 5b3b00b16e8..3426ccffac4 100644 --- a/models/fixtures/oauth2_application.yml +++ b/models/fixtures/oauth2_application.yml @@ -4,7 +4,7 @@ name: "Test" client_id: "da7da3ba-9a13-4167-856f-3899de0b0138" 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 updated_unix: 1546869730 confidential_client: true diff --git a/models/fixtures/oauth2_authorization_code.yml b/models/fixtures/oauth2_authorization_code.yml index 64d8b175077..01918b35eeb 100644 --- a/models/fixtures/oauth2_authorization_code.yml +++ b/models/fixtures/oauth2_authorization_code.yml @@ -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 diff --git a/models/fixtures/webhook.yml b/models/fixtures/webhook.yml index f372aaaecb4..01918b35eeb 100644 --- a/models/fixtures/webhook.yml +++ b/models/fixtures/webhook.yml @@ -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 diff --git a/models/git/branch_list.go b/models/git/branch_list.go index 25e84526d29..1445f3a5a02 100644 --- a/models/git/branch_list.go +++ b/models/git/branch_list.go @@ -7,7 +7,6 @@ import ( "context" "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" "code.gitea.io/gitea/modules/optional" @@ -60,24 +59,6 @@ func (branches BranchList) LoadPusher(ctx context.Context) error { 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 { db.ListOptions RepoID int64 diff --git a/models/issues/comment.go b/models/issues/comment.go index 84a7150b9f7..acfc07ff220 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -400,16 +400,7 @@ func (c *Comment) LoadPoster(ctx context.Context) (err error) { if c.Poster != nil { return nil } - - 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) - } - } + c.PosterID, c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID) return err } diff --git a/models/issues/dependency.go b/models/issues/dependency.go index 0eaa47e3593..db8054e1618 100644 --- a/models/issues/dependency.go +++ b/models/issues/dependency.go @@ -89,12 +89,6 @@ type ErrUnknownDependencyType struct { Type DependencyType } -// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType -func IsErrUnknownDependencyType(err error) bool { - _, ok := err.(ErrUnknownDependencyType) - return ok -} - func (err ErrUnknownDependencyType) Error() string { return fmt.Sprintf("unknown dependency type [type: %d]", err.Type) } diff --git a/models/issues/issue.go b/models/issues/issue.go index 838d41a3005..fe5433fbb20 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -48,21 +48,6 @@ func (err ErrIssueNotExist) Unwrap() error { 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") // Issue represents an issue or pull request of repository. @@ -190,17 +175,10 @@ func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool { // LoadPoster loads poster func (issue *Issue) LoadPoster(ctx context.Context) (err error) { - if issue.Poster == nil && issue.PosterID != 0 { - issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID) - 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 - } + if issue.Poster != nil { + return nil } + issue.PosterID, issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID) return err } diff --git a/models/issues/issue_pin.go b/models/issues/issue_pin.go index ae6195b05dd..753c96ed180 100644 --- a/models/issues/issue_pin.go +++ b/models/issues/issue_pin.go @@ -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) { var pins []*IssuePin if err := db.GetEngine(ctx).Where("repo_id = ? AND is_pull = ?", repoID, isPull).Find(&pins); err != nil { diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 01a3eb9a2af..c58a2f319dd 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -93,12 +93,6 @@ type ErrIssueIsOpen struct { 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 { 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, Attachments: uuids, }); err != nil { - if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { return err } return fmt.Errorf("newIssue: %w", err) diff --git a/models/issues/issue_watch.go b/models/issues/issue_watch.go index 560be17eb62..f384e086e56 100644 --- a/models/issues/issue_watch.go +++ b/models/issues/issue_watch.go @@ -67,7 +67,7 @@ func GetIssueWatch(ctx context.Context, userID, issueID int64) (iw *IssueWatch, 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 func CheckIssueWatch(ctx context.Context, user *user_model.User, issue *Issue) (bool, error) { iw, exist, err := GetIssueWatch(ctx, user.ID, issue.ID) diff --git a/models/issues/pull.go b/models/issues/pull.go index 1b883f29810..4c8d02a990e 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -437,8 +437,8 @@ func (pr *PullRequest) IsChecking() bool { return pr.Status == PullRequestStatusChecking } -// CanAutoMerge returns true if this pull request can be merged automatically. -func (pr *PullRequest) CanAutoMerge() bool { +// IsStatusMergeable returns true if this pull request is mergeable to its base +func (pr *PullRequest) IsStatusMergeable() bool { return pr.Status == PullRequestStatusMergeable } @@ -475,7 +475,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss LabelIDs: labelIDs, Attachments: uuids, }); err != nil { - if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { return err } return fmt.Errorf("newIssue: %w", err) diff --git a/models/issues/review.go b/models/issues/review.go index af2600876ed..78ef0d20c23 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -67,7 +67,7 @@ func (err ErrNotValidReviewRequest) Unwrap() error { 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{} // 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 { return err } - 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 - } + r.ReviewerID, r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID) 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 // 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) { - if doer == nil || issue == nil { - return false, errors.New("issue or doer is nil") + if doer == nil { + return false, nil } if err = issue.LoadRepo(ctx); err != nil { diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index 17ea951b5a6..7482829f1f2 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -75,7 +75,7 @@ func deleteDB() error { } 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 { 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)) @@ -91,7 +91,7 @@ func deleteDB() error { defer schrows.Close() if !schrows.Next() { - // Create and setup a DB schema + // Create and set up a DB schema _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema) if err != nil { return err @@ -134,7 +134,8 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu ourSkip := 2 ourSkip += skip 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 { 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 { 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 { 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 { - 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() - setting.AppDataPath = tmpDataPath + setting.MockBuiltinPaths(tempWorkPath, "", "") + setting.SetupGiteaTestEnv() - unittest.InitSettingsForTesting() 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.InitLoggersForTest() diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index db74ff78d50..c3a8f08b5d7 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/models/migrations/v1_24" "code.gitea.io/gitea/models/migrations/v1_25" "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_7" "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(329, "Add unique constraint for user badge", v1_26.AddUniqueIndexForUserBadge), 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 } diff --git a/models/migrations/v1_27/main_test.go b/models/migrations/v1_27/main_test.go new file mode 100644 index 00000000000..e269e3df9a8 --- /dev/null +++ b/models/migrations/v1_27/main_test.go @@ -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) +} diff --git a/models/migrations/v1_27/v331.go b/models/migrations/v1_27/v331.go new file mode 100644 index 00000000000..204b7b661e9 --- /dev/null +++ b/models/migrations/v1_27/v331.go @@ -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)) +} diff --git a/models/migrations/v1_27/v331_test.go b/models/migrations/v1_27/v331_test.go new file mode 100644 index 00000000000..45f467cf9bc --- /dev/null +++ b/models/migrations/v1_27/v331_test.go @@ -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 +} diff --git a/models/organization/org_user.go b/models/organization/org_user.go index 69cd9609446..627c1c2edf0 100644 --- a/models/organization/org_user.go +++ b/models/organization/org_user.go @@ -8,10 +8,7 @@ import ( "fmt" "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" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "xorm.io/builder" @@ -129,49 +126,6 @@ func IsUserOrgOwner(ctx context.Context, users user_model.UserList, orgID int64) 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) { if len(users) == 0 { return nil, nil //nolint:nilnil // return nil when there are no users diff --git a/models/organization/team_unit.go b/models/organization/team_unit.go index c6ec6b39b2c..b5237c2c587 100644 --- a/models/organization/team_unit.go +++ b/models/organization/team_unit.go @@ -28,19 +28,3 @@ func (t *TeamUnit) Unit() unit.Unit { func getUnitsByTeamID(ctx context.Context, teamID int64) (units []*TeamUnit, err error) { 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 - }) -} diff --git a/models/organization/team_user.go b/models/organization/team_user.go index d6d0a5054dd..d24a4c51263 100644 --- a/models/organization/team_user.go +++ b/models/organization/team_user.go @@ -36,14 +36,6 @@ type SearchMembersOptions struct { 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. func GetTeamMembers(ctx context.Context, opts *SearchMembersOptions) ([]*user_model.User, error) { var members []*user_model.User diff --git a/models/project/column.go b/models/project/column.go index 997e82ddf90..9c9abb4599d 100644 --- a/models/project/column.go +++ b/models/project/column.go @@ -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) { columns := make([]*Column, 0, 5) if len(columnsIDs) == 0 { diff --git a/models/pull/automerge.go b/models/pull/automerge.go index 7f940a98492..d32dc847d2d 100644 --- a/models/pull/automerge.go +++ b/models/pull/automerge.go @@ -5,14 +5,12 @@ package pull import ( "context" - "errors" "fmt" "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/timeutil" - "code.gitea.io/gitea/modules/util" ) // 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 } - doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID) - if errors.Is(err, util.ErrNotExist) { - doer, err = user_model.NewGhostUser(), nil - } - if err != nil { - return false, nil, err - } - - scheduledPRM.Doer = doer - return true, scheduledPRM, nil + scheduledPRM.DoerID, scheduledPRM.Doer, err = user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID) + return true, scheduledPRM, err } // DeleteScheduledAutoMerge delete a scheduled pull request diff --git a/models/repo/star.go b/models/repo/star.go index bc865f8373f..e1672623c82 100644 --- a/models/repo/star.go +++ b/models/repo/star.go @@ -11,7 +11,7 @@ import ( "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 { ID int64 `xorm:"pk autoincr"` UID int64 `xorm:"UNIQUE(s)"` diff --git a/models/repo/topic.go b/models/repo/topic.go index 6d5209d8210..14017da2d0b 100644 --- a/models/repo/topic.go +++ b/models/repo/topic.go @@ -44,12 +44,6 @@ type ErrTopicNotExist struct { Name string } -// IsErrTopicNotExist checks if an error is an ErrTopicNotExist. -func IsErrTopicNotExist(err error) bool { - _, ok := err.(ErrTopicNotExist) - return ok -} - // Error implements error interface func (err ErrTopicNotExist) Error() string { return fmt.Sprintf("topic is not exist [name: %s]", err.Name) diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index e15a64b01e8..28ae83a095e 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -8,6 +8,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" 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) } -// GetRepoAssignees returns all users that have write access and can be assigned to issues -// of the repository, +// GetRepoAssignees returns all users that have write access and can be assigned to issues or pull-requests of the repository, func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.User, err error) { if err = repo.LoadOwner(ctx); err != nil { return nil, err @@ -114,15 +114,9 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us uniqueUserIDs.AddMultiple(userIDs...) if repo.Owner.IsOrganization() { - 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"). - 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 { + // issues and pull requests both need "assignee list" + additionalUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeIssues, unit.TypePullRequests) + if err != nil { return nil, err } uniqueUserIDs.AddMultiple(additionalUserIDs...) diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go index cd8a0f1a1f7..cd45db61d08 100644 --- a/models/repo/user_repo_test.go +++ b/models/repo/user_repo_test.go @@ -6,7 +6,12 @@ package repo_test import ( "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" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -14,9 +19,14 @@ import ( "github.com/stretchr/testify/require" ) -func TestRepoAssignees(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func TestUserRepo(t *testing.T) { + 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}) users, err := repo_model.GetRepoAssignees(t.Context(), repo2) assert.NoError(t, err) @@ -39,9 +49,29 @@ func TestRepoAssignees(t *testing.T) { } } -func TestGetIssuePostersWithSearch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testRepoAssigneesNoTeamUnit(t *testing.T) { + 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}) users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER") diff --git a/models/system/notice.go b/models/system/notice.go index f39188f8fb3..4b919dffc92 100644 --- a/models/system/notice.go +++ b/models/system/notice.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" ) // NoticeType describes the notice type @@ -60,18 +59,6 @@ func CreateRepositoryNotice(desc string, args ...any) error { 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 // creates a system notice when error occurs. func RemoveStorageWithNotice(ctx context.Context, bucket storage.ObjectStorage, title, path string) { diff --git a/models/unittest/mock_http.go b/models/unittest/mock_http.go new file mode 100644 index 00000000000..dd05b263ede --- /dev/null +++ b/models/unittest/mock_http.go @@ -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 +} diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 63c9a3a9994..bd832348e7c 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -13,10 +13,8 @@ import ( "code.gitea.io/gitea/models/db" "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/git" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting/config" "code.gitea.io/gitea/modules/storage" @@ -29,37 +27,6 @@ import ( "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 type TestOptions struct { FixtureFiles []string @@ -75,11 +42,20 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) { func mainTest(m *testing.M, testOptsArg ...*TestOptions) int { 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() fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles} 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/" @@ -91,59 +67,28 @@ func mainTest(m *testing.M, testOptsArg ...*TestOptions) int { setting.SSH.Domain = "try.gitea.io" setting.Database.Type = "sqlite3" 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.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" config.SetDynGetter(system.NewDatabaseDynKeyGetter()) 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 { - 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 { - testlogger.Panicf("util.SyncDirs: %v\n", err) + return testlogger.MainErrorf("util.SyncDirs: %v", err) } 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 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 err := testOpts.TearDown(); err != nil { - testlogger.Panicf("tear down failed: %v\n", err) + return testlogger.MainErrorf("tear down failed: %v", err) } } return exitStatus diff --git a/models/user/badge.go b/models/user/badge.go index fbba8659261..a4a465a9d54 100644 --- a/models/user/badge.go +++ b/models/user/badge.go @@ -64,7 +64,7 @@ type GetBadgeUsersOptions struct { func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, int64, error) { sess := db.GetEngine(ctx). 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"). 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 type SearchBadgeOptions struct { db.ListOptions @@ -258,16 +252,3 @@ func (opts *SearchBadgeOptions) ToOrders() string { func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) { 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 -} diff --git a/models/user/block.go b/models/user/block.go index f4afd47d0f7..03f984a8fdf 100644 --- a/models/user/block.go +++ b/models/user/block.go @@ -90,7 +90,7 @@ func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, er return nil, err } 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 } diff --git a/models/user/email_address.go b/models/user/email_address.go index aa483d5f005..670d417f9e6 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -147,11 +147,6 @@ func InsertEmailAddress(ctx context.Context, email *EmailAddress) (*EmailAddress 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 func ValidateEmail(email string) error { if err := validateEmailBasic(email); err != nil { diff --git a/models/user/error.go b/models/user/error.go index cbf19998d10..5a956a2afe1 100644 --- a/models/user/error.go +++ b/models/user/error.go @@ -71,27 +71,6 @@ func (err ErrUserProhibitLogin) Unwrap() error { 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. type ErrUserIsNotLocal struct { UID int64 diff --git a/models/user/external_login_user.go b/models/user/external_login_user.go index 0e764efb9fe..636a2007ee8 100644 --- a/models/user/external_login_user.go +++ b/models/user/external_login_user.go @@ -21,12 +21,6 @@ type ErrExternalLoginUserAlreadyExist struct { 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 { 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 } -// IsErrExternalLoginUserNotExist checks if an error is a ExternalLoginUserNotExist. -func IsErrExternalLoginUserNotExist(err error) bool { - _, ok := err.(ErrExternalLoginUserNotExist) - return ok -} - func (err ErrExternalLoginUserNotExist) Error() string { return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID) } diff --git a/models/user/setting.go b/models/user/setting.go index a16fc86e55e..67b45208fcf 100644 --- a/models/user/setting.go +++ b/models/user/setting.go @@ -50,12 +50,6 @@ func (err ErrUserSettingIsNotExist) Unwrap() error { 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 func genSettingCacheKey(userID int64, key string) string { return fmt.Sprintf("user_%d.setting.%s", userID, key) diff --git a/models/user/user.go b/models/user/user.go index c9dc59b5430..69b97e9b478 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -7,6 +7,7 @@ package user import ( "context" "encoding/hex" + "errors" "fmt" "html/template" "mime" @@ -1013,17 +1014,22 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) { return users, err } -// GetPossibleUserByID returns the user if id > 0 or returns system user if id < 0 -func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) { +// 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) (_ int64, u *User, err error) { if id < 0 { 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 diff --git a/models/user/user_system_test.go b/models/user/user_system_test.go index 70a900378f3..3ae9c6e3665 100644 --- a/models/user/user_system_test.go +++ b/models/user/user_system_test.go @@ -11,8 +11,9 @@ import ( ) func TestSystemUser(t *testing.T) { - u, err := GetPossibleUserByID(t.Context(), -1) + uid, u, err := GetPossibleUserByID(t.Context(), -1) require.NoError(t, err) + assert.Equal(t, int64(-1), uid) assert.Equal(t, "Ghost", u.Name) assert.Equal(t, "ghost", u.LowerName) assert.True(t, u.IsGhost()) @@ -21,8 +22,9 @@ func TestSystemUser(t *testing.T) { require.NotNil(t, u) assert.Equal(t, "Ghost", u.Name) - u, err = GetPossibleUserByID(t.Context(), -2) + uid, u, err = GetPossibleUserByID(t.Context(), -2) require.NoError(t, err) + assert.Equal(t, int64(-2), uid) assert.Equal(t, "gitea-actions", u.Name) assert.Equal(t, "gitea-actions", u.LowerName) assert.True(t, u.IsGiteaActions()) @@ -31,6 +33,8 @@ func TestSystemUser(t *testing.T) { require.NotNil(t, u) assert.Equal(t, "Gitea Actions", u.FullName) - _, err = GetPossibleUserByID(t.Context(), -3) - require.Error(t, err) + uid, u, err = GetPossibleUserByID(t.Context(), 999999) + require.NoError(t, err) + assert.Equal(t, int64(-1), uid) + assert.Equal(t, "Ghost", u.Name) } diff --git a/models/webhook/main_test.go b/models/webhook/main_test.go index f19465d5053..5f2d5081a1a 100644 --- a/models/webhook/main_test.go +++ b/models/webhook/main_test.go @@ -15,5 +15,6 @@ func TestMain(m *testing.M) { "webhook.yml", "hook_task.yml", }, + SetUp: prepareWebhookTestData, }) } diff --git a/models/webhook/webhook_system_test.go b/models/webhook/webhook_system_test.go index d0013c6873f..9e954d1e377 100644 --- a/models/webhook/webhook_system_test.go +++ b/models/webhook/webhook_system_test.go @@ -10,28 +10,28 @@ import ( "code.gitea.io/gitea/modules/optional" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) 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]()} hooks, _, err := GetGlobalWebhooks(t.Context(), &opts) - assert.NoError(t, err) - if assert.Len(t, hooks, 2) { - assert.Equal(t, int64(5), hooks[0].ID) - assert.Equal(t, int64(6), hooks[1].ID) - } + require.NoError(t, err) + require.Len(t, hooks, 2) + assert.Equal(t, hookSystem.ID, hooks[0].ID) + assert.Equal(t, hookDefault.ID, hooks[1].ID) + opts.IsSystem = optional.Some(true) hooks, _, err = GetGlobalWebhooks(t.Context(), &opts) - assert.NoError(t, err) - if assert.Len(t, hooks, 1) { - assert.Equal(t, int64(5), hooks[0].ID) - } + require.NoError(t, err) + require.Len(t, hooks, 1) + assert.Equal(t, hookSystem.ID, hooks[0].ID) opts.IsSystem = optional.Some(false) hooks, _, err = GetGlobalWebhooks(t.Context(), &opts) - assert.NoError(t, err) - if assert.Len(t, hooks, 1) { - assert.Equal(t, int64(6), hooks[0].ID) - } + require.NoError(t, err) + require.Len(t, hooks, 1) + assert.Equal(t, hookDefault.ID, hooks[0].ID) } diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index 71f50017c51..073af91de21 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -4,6 +4,7 @@ package webhook import ( + "context" "testing" "time" @@ -14,40 +15,105 @@ import ( "code.gitea.io/gitea/modules/timeutil" webhook_module "code.gitea.io/gitea/modules/webhook" + "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "xorm.io/builder" ) -func TestHookContentType_Name(t *testing.T) { - assert.Equal(t, "json", ContentTypeJSON.Name()) - assert.Equal(t, "form", ContentTypeForm.Name()) +func prepareWebhookTestData() error { + if err := unittest.PrepareTestDatabase(); err != nil { + 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("form")) assert.False(t, IsValidHookContentType("invalid")) } func TestWebhook_History(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1}) - tasks, err := webhook.History(t.Context(), 0) - assert.NoError(t, err) - 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) - } + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true}) + tasks, err := hook.History(t.Context(), 0) + require.NoError(t, err) + require.Len(t, tasks, 3) - webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2}) - tasks, err = webhook.History(t.Context(), 0) + hook = unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, Events: "{}"}) + tasks, err = hook.History(t.Context(), 0) assert.NoError(t, err) assert.Empty(t, tasks) } func TestWebhook_UpdateEvent(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1}) + webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true}) hookEvent := &webhook_module.HookEvent{ PushOnly: true, SendEverything: false, @@ -100,10 +166,10 @@ func TestCreateWebhook(t *testing.T) { } func TestGetWebhookByRepoID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - hook, err := GetWebhookByRepoID(t.Context(), 1, 1) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true}) + loaded, err := GetWebhookByRepoID(t.Context(), 1, hook.ID) 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) assert.Error(t, err) @@ -111,10 +177,10 @@ func TestGetWebhookByRepoID(t *testing.T) { } func TestGetWebhookByOwnerID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - hook, err := GetWebhookByOwnerID(t.Context(), 3, 3) - assert.NoError(t, err) - assert.Equal(t, int64(3), hook.ID) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3}) + loaded, err := GetWebhookByOwnerID(t.Context(), 3, hook.ID) + require.NoError(t, err) + require.Equal(t, hook.ID, loaded.ID) _, err = GetWebhookByOwnerID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) assert.Error(t, err) @@ -122,48 +188,45 @@ func TestGetWebhookByOwnerID(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)}) - assert.NoError(t, err) - if assert.Len(t, hooks, 1) { - assert.Equal(t, int64(1), hooks[0].ID) - assert.True(t, hooks[0].IsActive) - } + require.NoError(t, err) + require.Len(t, hooks, 1) + assert.Equal(t, hook.ID, hooks[0].ID) + assert.True(t, hooks[0].IsActive) } func TestGetWebhooksByRepoID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{RepoID: 1}) - assert.NoError(t, err) - if assert.Len(t, hooks, 2) { - assert.Equal(t, int64(1), hooks[0].ID) - assert.Equal(t, int64(2), hooks[1].ID) - } + require.NoError(t, err) + require.Len(t, hooks, 2) + assert.Equal(t, int64(1), hooks[0].RepoID) + 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) { - assert.NoError(t, unittest.PrepareTestDatabase()) hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)}) - assert.NoError(t, err) - if assert.Len(t, hooks, 1) { - assert.Equal(t, int64(3), hooks[0].ID) - assert.True(t, hooks[0].IsActive) - } + require.NoError(t, err) + require.Len(t, hooks, 1) + assert.Equal(t, int64(3), hooks[0].OwnerID) + assert.True(t, hooks[0].IsActive) } func TestGetWebhooksByOwnerID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{OwnerID: 3}) - assert.NoError(t, err) - if assert.Len(t, hooks, 1) { - assert.Equal(t, int64(3), hooks[0].ID) - assert.True(t, hooks[0].IsActive) - } + require.NoError(t, err) + require.Len(t, hooks, 2) + assert.Equal(t, int64(3), hooks[0].OwnerID) + 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) { - assert.NoError(t, unittest.PrepareTestDatabase()) - hook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2}) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, Events: `{}`}) + require.False(t, hook.IsActive) hook.IsActive = true hook.ContentType = ContentTypeForm unittest.AssertNotExistsBean(t, hook) @@ -172,48 +235,36 @@ func TestUpdateWebhook(t *testing.T) { } func TestDeleteWebhookByRepoID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2, RepoID: 1}) - assert.NoError(t, DeleteWebhookByRepoID(t.Context(), 1, 2)) - unittest.AssertNotExistsBean(t, &Webhook{ID: 2, RepoID: 1}) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, Events: `{}`}) + assert.NoError(t, DeleteWebhookByRepoID(t.Context(), 1, hook.ID)) + unittest.AssertNotExistsBean(t, &Webhook{ID: hook.ID, RepoID: 1}) err := DeleteWebhookByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) - assert.Error(t, err) assert.True(t, IsErrWebhookNotExist(err)) } func TestDeleteWebhookByOwnerID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OwnerID: 3}) - assert.NoError(t, DeleteWebhookByOwnerID(t.Context(), 3, 3)) - unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OwnerID: 3}) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3, Events: `{}`}) + assert.NoError(t, DeleteWebhookByOwnerID(t.Context(), 3, hook.ID)) + unittest.AssertNotExistsBean(t, &Webhook{ID: hook.ID, OwnerID: 3}) err := DeleteWebhookByOwnerID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) - assert.Error(t, err) assert.True(t, IsErrWebhookNotExist(err)) } func TestHookTasks(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - hookTasks, err := HookTasks(t.Context(), 1, 1) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true}) + hookTasks, err := HookTasks(t.Context(), hook.ID, 1) assert.NoError(t, err) - if 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) - } - + assert.Len(t, hookTasks, 3) hookTasks, err = HookTasks(t.Context(), unittest.NonexistentID, 1) assert.NoError(t, err) assert.Empty(t, hookTasks) } func TestCreateHookTask(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - hookTask := &HookTask{ - HookID: 3, - PayloadVersion: 2, - } + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3, IsActive: true}) + hookTask := &HookTask{HookID: hook.ID, PayloadVersion: 2} unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(t.Context(), hookTask) assert.NoError(t, err) @@ -221,20 +272,23 @@ func TestCreateHookTask(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}) - hook.PayloadContent = "new payload content" - hook.IsDelivered = true - unittest.AssertNotExistsBean(t, hook) - assert.NoError(t, UpdateHookTask(t.Context(), hook)) - unittest.AssertExistsAndLoadBean(t, hook) + hookTask.PayloadContent = "new payload content" + hookTask.IsDelivered = true + unittest.AssertNotExistsBean(t, hookTask) + assert.NoError(t, UpdateHookTask(t.Context(), hookTask)) + unittest.AssertExistsAndLoadBean(t, hookTask) } 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{ - HookID: 3, + HookID: hook.ID, IsDelivered: true, Delivered: timeutil.TimeStampNanoNow(), PayloadVersion: 2, @@ -249,9 +303,10 @@ func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(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{ - HookID: 4, + HookID: hook.ID, IsDelivered: false, PayloadVersion: 2, } @@ -265,9 +320,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(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{ - HookID: 4, + HookID: hook.ID, IsDelivered: true, Delivered: timeutil.TimeStampNanoNow(), PayloadVersion: 2, @@ -282,9 +338,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(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{ - HookID: 3, + HookID: hook.ID, IsDelivered: true, Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()), PayloadVersion: 2, @@ -299,9 +356,10 @@ func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(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{ - HookID: 4, + HookID: hook.ID, IsDelivered: false, PayloadVersion: 2, } @@ -315,9 +373,10 @@ func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(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{ - HookID: 4, + HookID: hook.ID, IsDelivered: true, Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()), PayloadVersion: 2, diff --git a/modules/actions/jobparser/jobparser.go b/modules/actions/jobparser/jobparser.go index 1d4c4c1756a..76f229a54b3 100644 --- a/modules/actions/jobparser/jobparser.go +++ b/modules/actions/jobparser/jobparser.go @@ -83,12 +83,6 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) { return ret, nil } -func WithJobResults(results map[string]string) ParseOption { - return func(c *parseContext) { - c.jobResults = results - } -} - func WithGitContext(context *model.GithubContext) ParseOption { return func(c *parseContext) { c.gitContext = context diff --git a/modules/dump/dumper.go b/modules/dump/dumper.go index 02829d6a1ed..2f160707041 100644 --- a/modules/dump/dumper.go +++ b/modules/dump/dumper.go @@ -4,6 +4,7 @@ package dump import ( + "archive/zip" "context" "errors" "fmt" @@ -85,7 +86,7 @@ func NewDumper(ctx context.Context, format string, output io.Writer) (*Dumper, e var comp archives.ArchiverAsync switch format { case "zip": - comp = archives.Zip{} + comp = archives.Zip{Compression: zip.Deflate} case "tar": comp = archives.Tar{} case "tar.sz": diff --git a/modules/eventsource/event.go b/modules/eventsource/event.go index ebcca509034..c72cc466a03 100644 --- a/modules/eventsource/event.go +++ b/modules/eventsource/event.go @@ -7,7 +7,6 @@ import ( "bytes" "fmt" "io" - "strings" "time" "code.gitea.io/gitea/modules/json" @@ -110,9 +109,3 @@ func (e *Event) WriteTo(w io.Writer) (int64, error) { return sum, err } - -func (e *Event) String() string { - buf := new(strings.Builder) - _, _ = e.WriteTo(buf) - return buf.String() -} diff --git a/modules/git/git.go b/modules/git/git.go index 2df83f9843a..69eb07d1f0b 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -192,13 +192,13 @@ func RunGitTests(m interface{ Run() int }) { func runGitTests(m interface{ Run() int }) int { gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home") 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() setting.Git.HomePath = gitHomePath 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() } diff --git a/modules/git/gitcmd/command_test.go b/modules/git/gitcmd/command_test.go index 6e4214d9953..19ec02b8088 100644 --- a/modules/git/gitcmd/command_test.go +++ b/modules/git/gitcmd/command_test.go @@ -24,7 +24,7 @@ func testMain(m *testing.M) int { // "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home") if err != nil { - testlogger.Panicf("failed to create temp dir: %v", err) + return testlogger.MainErrorf("failed to create temp dir: %v", err) } defer cleanup() diff --git a/modules/git/gitcmd/error.go b/modules/git/gitcmd/error.go index b674068c400..436f4e18ae4 100644 --- a/modules/git/gitcmd/error.go +++ b/modules/git/gitcmd/error.go @@ -56,6 +56,14 @@ func StderrHasPrefix(err error, prefix string) bool { 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 { var exitError *exec.ExitError if errors.As(err, &exitError) { diff --git a/modules/gitrepo/commit.go b/modules/gitrepo/commit.go index 0ab17862fee..0a8cb0544cb 100644 --- a/modules/gitrepo/commit.go +++ b/modules/gitrepo/commit.go @@ -44,23 +44,6 @@ func CommitsCount(ctx context.Context, repo Repository, opts CommitsCountOptions 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 func FileCommitsCount(ctx context.Context, repo Repository, revision, file string) (int64, error) { return CommitsCount(ctx, repo, diff --git a/modules/gitrepo/config.go b/modules/gitrepo/config.go index 9be3ef94aeb..4940f1c0788 100644 --- a/modules/gitrepo/config.go +++ b/modules/gitrepo/config.go @@ -5,21 +5,11 @@ package gitrepo import ( "context" - "strings" "code.gitea.io/gitea/modules/git/gitcmd" "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 { return "repo-config:" + repoStoragePath } diff --git a/modules/hostmatcher/hostmatcher.go b/modules/hostmatcher/hostmatcher.go index 15c63714222..044dba679a1 100644 --- a/modules/hostmatcher/hostmatcher.go +++ b/modules/hostmatcher/hostmatcher.go @@ -78,11 +78,6 @@ func (hl *HostMatchList) AppendBuiltin(builtin string) { 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 func (hl *HostMatchList) IsEmpty() bool { return hl == nil || (len(hl.builtins) == 0 && len(hl.patterns) == 0 && len(hl.ipNets) == 0) diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index e62747c7241..4fa9466d19a 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -317,7 +317,7 @@ func TestRender_email(t *testing.T) { func TestRender_emoji(t *testing.T) { setting.AppURL = markup.TestAppURL - setting.StaticURLPrefix = markup.TestAppURL + setting.StaticURLPrefix = strings.TrimSuffix(markup.TestAppURL, "/") test := func(input, expected string) { expected = strings.ReplaceAll(expected, "&", "&") @@ -500,7 +500,7 @@ func Test_ParseClusterFuzz(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)() test := func(input, expected string) { diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index f6a6cb26c6d..1cc75d763d1 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -74,10 +74,6 @@ func (r *GlodmarkRender) Convert(source []byte, writer io.Writer, opts ...parser 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) { if entering { languageBytes, _ := c.Language() diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index 7cfe692390b..e6be460599a 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -87,6 +87,7 @@ func getViteDevProxy() *httputil.ReverseProxy { // the Vite dev server port from the port file written by the viteDevServerPortPlugin. // It is needed because there are container-based development, only Gitea web server's port is exposed. func ViteDevMiddleware(next http.Handler) http.Handler { + markLongPolling := routing.MarkLongPolling() return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { if !isViteDevRequest(req) { next.ServeHTTP(resp, req) @@ -97,8 +98,7 @@ func ViteDevMiddleware(next http.Handler) http.Handler { next.ServeHTTP(resp, req) return } - routing.MarkLongPolling(resp, req) - proxy.ServeHTTP(resp, req) + markLongPolling(proxy).ServeHTTP(resp, req) }) } diff --git a/modules/queue/manager_test.go b/modules/queue/manager_test.go index fda498cc841..f9f9b7310be 100644 --- a/modules/queue/manager_test.go +++ b/modules/queue/manager_test.go @@ -13,11 +13,7 @@ import ( ) func TestManager(t *testing.T) { - oldAppDataPath := setting.AppDataPath setting.AppDataPath = t.TempDir() - defer func() { - setting.AppDataPath = oldAppDataPath - }() newQueueFromConfig := func(name, cfg string) (*WorkerPoolQueue[int], error) { cfgProvider, err := setting.NewConfigProviderFromData(cfg) diff --git a/modules/setting/actions.go b/modules/setting/actions.go index 7a91ecb5930..0d1bdadc8ec 100644 --- a/modules/setting/actions.go +++ b/modules/setting/actions.go @@ -12,6 +12,8 @@ import ( "code.gitea.io/gitea/modules/log" ) +const defaultMaxRerunAttempts = 50 + // Actions settings var ( Actions = struct { @@ -27,11 +29,13 @@ var ( AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"` SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"` WorkflowDirs []string `ini:"WORKFLOW_DIRS"` + MaxRerunAttempts int64 `ini:"MAX_RERUN_ATTEMPTS"` }{ Enabled: true, DefaultActionsURL: defaultActionsURLGitHub, SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"}, WorkflowDirs: []string{".gitea/workflows", ".github/workflows"}, + MaxRerunAttempts: defaultMaxRerunAttempts, } ) @@ -118,6 +122,10 @@ func loadActionsFrom(rootCfg ConfigProvider) error { Actions.EndlessTaskTimeout = sec.Key("ENDLESS_TASK_TIMEOUT").MustDuration(3 * time.Hour) Actions.AbandonedJobTimeout = sec.Key("ABANDONED_JOB_TIMEOUT").MustDuration(24 * time.Hour) + if Actions.MaxRerunAttempts <= 0 { + Actions.MaxRerunAttempts = defaultMaxRerunAttempts + } + if !Actions.LogCompression.IsValid() { return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression) } diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 8e0210aa518..83891387a77 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -99,6 +99,7 @@ var OAuth2 = struct { JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"` MaxTokenLength int DefaultApplications []string + CustomSchemes []string }{ Enabled: true, AccessTokenExpirationTime: 3600, diff --git a/modules/setting/path.go b/modules/setting/path.go index f51457a620e..45c6759a73e 100644 --- a/modules/setting/path.go +++ b/modules/setting/path.go @@ -198,6 +198,12 @@ func InitWorkPathAndCfgProvider(getEnvFn func(name string) string, args ArgWorkP CustomConf = tmpCustomConf.Value } +func MockBuiltinPaths(workPath, customPath, customConf string) func() { + oldApp, oldCustom, oldConf := appWorkPathBuiltin, customPathBuiltin, customConfBuiltin + appWorkPathBuiltin, customPathBuiltin, customConfBuiltin = workPath, customPath, customConf + return func() { appWorkPathBuiltin, customPathBuiltin, customConfBuiltin = oldApp, oldCustom, oldConf } +} + // AppDataTempDir returns a managed temporary directory for the application data. // Using empty sub will get the managed base temp directory, and it's safe to delete it. // Gitea only creates subdirectories under it, but not the APP_TEMP_PATH directory itself. diff --git a/modules/setting/security.go b/modules/setting/security.go index 152bcffd9fa..8b7664baba3 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -16,9 +16,11 @@ import ( // Security settings var Security = struct { // TODO: move more settings to this struct in future - XFrameOptions string + XFrameOptions string + XContentTypeOptions string }{ - XFrameOptions: "SAMEORIGIN", + XFrameOptions: "SAMEORIGIN", + XContentTypeOptions: "nosniff", } var ( @@ -154,6 +156,8 @@ func loadSecurityFrom(rootCfg ConfigProvider) { Security.XFrameOptions = rootCfg.Section("cors").Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions) } + Security.XContentTypeOptions = sec.Key("X_CONTENT_TYPE_OPTIONS").MustString(Security.XContentTypeOptions) + twoFactorAuth := sec.Key("TWO_FACTOR_AUTH").String() switch twoFactorAuth { case "": diff --git a/modules/setting/server.go b/modules/setting/server.go index 1085e052a3e..dc58e43c435 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -4,7 +4,6 @@ package setting import ( - "encoding/base64" "net" "net/url" "os" @@ -13,7 +12,6 @@ import ( "strings" "time" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" ) @@ -112,72 +110,9 @@ var ( StartupTimeout time.Duration PerWriteTimeout = 30 * time.Second PerWritePerKbTimeout = 10 * time.Second - StaticURLPrefix string - AbsoluteAssetURL string - - ManifestData string + StaticURLPrefix string // no trailing slash, defaults to AppSubURL, the URL can be relative or absolute ) -// MakeManifestData generates web app manifest JSON -func MakeManifestData(appName, appURL, absoluteAssetURL string) []byte { - type manifestIcon struct { - Src string `json:"src"` - Type string `json:"type"` - Sizes string `json:"sizes"` - } - - type manifestJSON struct { - Name string `json:"name"` - ShortName string `json:"short_name"` - StartURL string `json:"start_url"` - Icons []manifestIcon `json:"icons"` - } - - bytes, err := json.Marshal(&manifestJSON{ - Name: appName, - ShortName: appName, - StartURL: appURL, - Icons: []manifestIcon{ - { - Src: absoluteAssetURL + "/assets/img/logo.png", - Type: "image/png", - Sizes: "512x512", - }, - { - Src: absoluteAssetURL + "/assets/img/logo.svg", - Type: "image/svg+xml", - Sizes: "512x512", - }, - }, - }) - if err != nil { - log.Error("unable to marshal manifest JSON. Error: %v", err) - return make([]byte, 0) - } - - return bytes -} - -// MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash -func MakeAbsoluteAssetURL(appURL *url.URL, staticURLPrefix string) string { - parsedPrefix, err := url.Parse(strings.TrimSuffix(staticURLPrefix, "/")) - if err != nil { - log.Fatal("Unable to parse STATIC_URL_PREFIX: %v", err) - } - - if err == nil && parsedPrefix.Hostname() == "" { - if staticURLPrefix == "" { - return strings.TrimSuffix(appURL.String(), "/") - } - - // StaticURLPrefix is just a path - appHostURL := &url.URL{Scheme: appURL.Scheme, Host: appURL.Host} - return appHostURL.String() + "/" + strings.Trim(staticURLPrefix, "/") - } - - return strings.TrimSuffix(staticURLPrefix, "/") -} - func loadServerFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("server") AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea") @@ -313,10 +248,6 @@ func loadServerFrom(rootCfg ConfigProvider) { Domain = urlHostname } - AbsoluteAssetURL = MakeAbsoluteAssetURL(appURL, StaticURLPrefix) - manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL) - ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes) - var defaultLocalURL string switch Protocol { case HTTPUnix: diff --git a/modules/setting/setting_test.go b/modules/setting/setting_test.go deleted file mode 100644 index 13575f52a6e..00000000000 --- a/modules/setting/setting_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package setting - -import ( - "net/url" - "testing" - - "code.gitea.io/gitea/modules/json" - - "github.com/stretchr/testify/assert" -) - -func TestMakeAbsoluteAssetURL(t *testing.T) { - appURL1, _ := url.Parse("https://localhost:1234") - appURL2, _ := url.Parse("https://localhost:1234/") - appURLSub1, _ := url.Parse("https://localhost:1234/foo") - appURLSub2, _ := url.Parse("https://localhost:1234/foo/") - - // static URL is an absolute URL, so should be used - assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL(appURL1, "https://localhost:2345")) - assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL(appURL1, "https://localhost:2345/")) - - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL1, "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL2, "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL1, "/foo/")) - - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub1, "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub2, "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub1, "/foo/")) - - assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub1, "/bar")) - assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub2, "/bar")) - assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub1, "/bar/")) -} - -func TestMakeManifestData(t *testing.T) { - jsonBytes := MakeManifestData(`Example App '\"`, "https://example.com", "https://example.com/foo/bar") - assert.True(t, json.Valid(jsonBytes)) -} diff --git a/modules/setting/testenv.go b/modules/setting/testenv.go index 853521c328a..d8663d07e24 100644 --- a/modules/setting/testenv.go +++ b/modules/setting/testenv.go @@ -10,10 +10,12 @@ import ( "runtime" "strings" + "code.gitea.io/gitea/modules/auth/password/hash" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" ) -var giteaTestSourceRoot *string +var giteaTestSourceRoot *string // intentionally use a pointer to make sure the uninitialized access panics func GetGiteaTestSourceRoot() string { return *giteaTestSourceRoot @@ -25,48 +27,93 @@ func SetupGiteaTestEnv() { } IsInTesting = true - giteaRoot := os.Getenv("GITEA_TEST_ROOT") - if giteaRoot == "" { - _, filename, _, _ := runtime.Caller(0) - giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename))) - fixturesDir := filepath.Join(giteaRoot, "models", "fixtures") - if _, err := os.Stat(fixturesDir); err != nil { - panic("in gitea source code directory, fixtures directory not found: " + fixturesDir) + + 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. + // * If the "log.Fatal" is abused in tests, should fix. + panic(fmt.Errorf("non-zero exit code during testing: %d", code)) + } + os.Exit(0) + } + + initGiteaRoot := func() string { + giteaRoot := os.Getenv("GITEA_TEST_ROOT") + if giteaRoot == "" { + _, filename, _, _ := runtime.Caller(0) + giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename))) + fixturesDir := filepath.Join(giteaRoot, "models", "fixtures") + if _, err := os.Stat(fixturesDir); err != nil { + panic("in gitea source code directory, fixtures directory not found: " + fixturesDir) + } + } + giteaTestSourceRoot = &giteaRoot + return giteaRoot + } + giteaRoot := initGiteaRoot() + + initGiteaPaths := func() { + // need to load assets (options, public) from the source code directory for testing + StaticRootPath = giteaRoot + // during testing, the AppPath must point to the pre-built Gitea binary in the source root + // it needs to be called by git hooks + AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(IsWindows, ".exe", "") + } + + initGiteaConf := func() string { + // giteaConf (GITEA_CONF) must be relative because it is used in the git hooks as "$GITEA_ROOT/$GITEA_CONF" + giteaConf := os.Getenv("GITEA_TEST_CONF") + if giteaConf == "" { + // if no GITEA_TEST_CONF, then it is in unit test, use a temp (non-existing / empty) config file + // do not really use such config file, the test can run concurrently, using the same config file will cause data-race between tests + giteaConf = "custom/conf/app-test-tmp.ini" + customConfBuiltin = filepath.Join(AppWorkPath, giteaConf) + CustomConf = customConfBuiltin + _ = os.Remove(CustomConf) + } else { + // CustomConf must be absolute path to make tests pass. + // At the moment, GITEA_TEST_CONF is always in Gitea's source root + CustomConf = filepath.Join(giteaRoot, giteaConf) + } + return giteaConf + } + + cleanUpEnv := func() { + // also unset unnecessary env vars for testing (only keep "GITEA_TEST_*" ones) + UnsetUnnecessaryEnvVars() + for _, env := range os.Environ() { + if strings.HasPrefix(env, "GIT_") || (strings.HasPrefix(env, "GITEA_") && !strings.HasPrefix(env, "GITEA_TEST_")) { + k, _, _ := strings.Cut(env, "=") + _ = os.Unsetenv(k) + } } } - appWorkPathBuiltin = giteaRoot - AppWorkPath = giteaRoot - AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(IsWindows, ".exe", "") - StaticRootPath = giteaRoot // need to load assets (options, public) from the source code directory for testing + initWorkPathAndConfig := func() { + // init paths and config system for testing + getTestEnv := func(key string) string { return "" } + InitWorkPathAndCommonConfig(getTestEnv, ArgWorkPathAndCustomConf{CustomConf: CustomConf}) - // giteaConf (GITEA_CONF) must be relative because it is used in the git hooks as "$GITEA_ROOT/$GITEA_CONF" - giteaConf := os.Getenv("GITEA_TEST_CONF") - if giteaConf == "" { - // By default, use sqlite.ini for testing, then IDE like GoLand can start the test process with debugger. - // It's easier for developers to debug bugs step by step with a debugger. - // Notice: when doing "ssh push", Gitea executes sub processes, debugger won't work for the sub processes. - giteaConf = "tests/sqlite.ini" - _, _ = fmt.Fprintf(os.Stderr, "Environment variable GITEA_TEST_CONF not set - defaulting to %s\n", giteaConf) - if !EnableSQLite3 { - _, _ = fmt.Fprintf(os.Stderr, "sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n") - os.Exit(1) + if err := 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) + PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") } - // CustomConf must be absolute path to make tests pass, - CustomConf = filepath.Join(AppWorkPath, giteaConf) - // also unset unnecessary env vars for testing (only keep "GITEA_TEST_*" ones) - UnsetUnnecessaryEnvVars() - for _, env := range os.Environ() { - if strings.HasPrefix(env, "GIT_") || (strings.HasPrefix(env, "GITEA_") && !strings.HasPrefix(env, "GITEA_TEST_")) { - k, _, _ := strings.Cut(env, "=") - _ = os.Unsetenv(k) - } + initGiteaPaths() + giteaConf := initGiteaConf() + cleanUpEnv() + initWorkPathAndConfig() + + if RepoRootPath == "" || AppDataPath == "" { + panic("SetupGiteaTestEnv failed, paths are not initialized") } // TODO: some git repo hooks (test fixtures) still use these env variables, need to be refactored in the future _ = os.Setenv("GITEA_ROOT", giteaRoot) _ = os.Setenv("GITEA_CONF", giteaConf) // test fixture git hooks use "$GITEA_ROOT/$GITEA_CONF" in their scripts - giteaTestSourceRoot = &giteaRoot } diff --git a/modules/storage/storage.go b/modules/storage/storage.go index e19c421ba82..1271440e5a8 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -21,25 +21,6 @@ import ( // ErrURLNotSupported represents url is not supported var ErrURLNotSupported = errors.New("url method not supported") -// ErrInvalidConfiguration is called when there is invalid configuration for a storage -type ErrInvalidConfiguration struct { - cfg any - err error -} - -func (err ErrInvalidConfiguration) Error() string { - if err.err != nil { - return fmt.Sprintf("Invalid Configuration Argument: %v: Error: %v", err.cfg, err.err) - } - return fmt.Sprintf("Invalid Configuration Argument: %v", err.cfg) -} - -// IsErrInvalidConfiguration checks if an error is an ErrInvalidConfiguration -func IsErrInvalidConfiguration(err error) bool { - _, ok := err.(ErrInvalidConfiguration) - return ok -} - type Type = setting.StorageType // NewStorageFunc is a function that creates a storage diff --git a/modules/structs/notifications.go b/modules/structs/notifications.go index d7aa0783dc2..b94e02aee35 100644 --- a/modules/structs/notifications.go +++ b/modules/structs/notifications.go @@ -68,12 +68,12 @@ const ( type NotifySubjectType string const ( - // NotifySubjectIssue an issue is subject of an notification + // NotifySubjectIssue a issue is subject of an notification NotifySubjectIssue NotifySubjectType = "Issue" - // NotifySubjectPull an pull is subject of an notification + // NotifySubjectPull a pull is subject of an notification NotifySubjectPull NotifySubjectType = "Pull" - // NotifySubjectCommit an commit is subject of an notification + // NotifySubjectCommit a commit is subject of an notification NotifySubjectCommit NotifySubjectType = "Commit" - // NotifySubjectRepository an repository is subject of an notification + // NotifySubjectRepository a repository is subject of an notification NotifySubjectRepository NotifySubjectType = "Repository" ) diff --git a/modules/structs/pull_review.go b/modules/structs/pull_review.go index de0677efabd..82c86b5b4ac 100644 --- a/modules/structs/pull_review.go +++ b/modules/structs/pull_review.go @@ -94,6 +94,11 @@ type CreatePullReviewComment struct { NewLineNum int64 `json:"new_position"` } +// CreatePullReviewCommentReplyOptions are options to reply to a pull request review comment +type CreatePullReviewCommentReplyOptions struct { + Body string `json:"body" binding:"Required"` +} + // SubmitPullReviewOptions are options to submit a pending pull request review type SubmitPullReviewOptions struct { Event ReviewStateType `json:"event"` diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 92ca9bcccef..4592c18ed69 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -105,12 +105,18 @@ type ActionArtifact struct { // ActionWorkflowRun represents a WorkflowRun type ActionWorkflowRun struct { - ID int64 `json:"id"` - URL string `json:"url"` - HTMLURL string `json:"html_url"` - DisplayTitle string `json:"display_title"` - Path string `json:"path"` - Event string `json:"event"` + ID int64 `json:"id"` + URL string `json:"url"` + // PreviousAttemptURL is the API URL of the previous attempt of this run, e.g. ".../actions/runs/{run_id}/attempts/{attempt-1}". + // It is set only when the current attempt is > 1 (i.e. a rerun). For the first attempt, or for legacy runs that pre-date ActionRunAttempt, it is null. + PreviousAttemptURL *string `json:"previous_attempt_url"` + HTMLURL string `json:"html_url"` + DisplayTitle string `json:"display_title"` + Path string `json:"path"` + Event string `json:"event"` + // RunAttempt is 1-based for runs created after ActionRunAttempt was introduced. + // A value of 0 is a legacy-only sentinel for runs created before attempts existed + // and indicates no corresponding /attempts/{n} resource is available. RunAttempt int64 `json:"run_attempt"` RunNumber int64 `json:"run_number"` RepositoryID int64 `json:"repository_id,omitempty"` diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index cf1db324768..c21c20efff9 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -5,6 +5,7 @@ package templates import ( "html/template" + "net/url" "strings" "testing" @@ -169,9 +170,21 @@ func TestQueryBuild(t *testing.T) { }) } +const queryNonASCII = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" // all non-letter & non-number chars + func TestQueryEscape(t *testing.T) { // this test is a reference for "urlQueryEscape" in JS - in := "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" // all non-letter & non-number chars - expected := "%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~" - assert.Equal(t, expected, string(queryEscape(in))) + // Special case for space encoding: + // * RFC 3986: Uniform Resource Identifier (URI): %20 + // * WHATWG HTML: application/x-www-form-urlencoded: + + // * JavaScript: encodeURIComponent() uses "%20". URLSearchParams uses "+" + // * Golang: QueryEscape uses "+" + expected := "+%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~" + assert.Equal(t, expected, url.QueryEscape(queryNonASCII)) +} + +func TestPathEscape(t *testing.T) { + // this test is a reference for "pathEscape" in JS + expected := "%20%21%22%23$%25&%27%28%29%2A+%2C-.%2F:%3B%3C=%3E%3F@%5B%5C%5D%5E_%60%7B%7C%7D~" + assert.Equal(t, expected, url.PathEscape(queryNonASCII)) } diff --git a/modules/test/utils.go b/modules/test/utils.go index a55deeb2e35..331dc4a959b 100644 --- a/modules/test/utils.go +++ b/modules/test/utils.go @@ -47,7 +47,7 @@ func ParseJSONError(buf []byte) (ret struct { } func ParseJSONRedirect(buf []byte) (ret struct { - Redirect string `json:"redirect"` + Redirect *string `json:"redirect"` }, ) { _ = json.Unmarshal(buf, &ret) diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index 217121f604b..151ca397032 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -118,7 +118,7 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() { deferHasRun := false t.Cleanup(func() { if !deferHasRun { - stdoutPrintf("!!! %s defer function hasn't been run but Cleanup is called, usually caused by panic\n", t.Name()) + stdoutPrintf("!!! %s: defer function hasn't been run but Cleanup is called, usually caused by panic\n", t.Name()) } }) stdoutPrintf("=== %s (%s:%d)\n", log.NewColoredValue(t.Name()), strings.TrimPrefix(filename, prefix), line) @@ -173,7 +173,8 @@ func Init() { log.RegisterEventWriter("test", newTestLoggerWriter) } -func Panicf(format string, args ...any) { - // don't call os.Exit, otherwise the "defer" functions won't be executed - panic(fmt.Sprintf(format, args...)) +// MainErrorf is used to report an error from TestMain and return a non-zero value to indicate the failure +func MainErrorf(msg string, a ...any) int { + _, _ = fmt.Fprintf(os.Stderr, msg+"\n", a...) + return 1 } diff --git a/modules/util/util.go b/modules/util/util.go index ec118aaf0de..04d0fb584d5 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -281,11 +281,13 @@ func EnumValue[T comparable](val EnumConst[T]) (ret T, valid bool) { return enums[0], false } -func ReserveLineBreakForTextarea(input string) string { +func NormalizeStringEOL(input string) string { // Since the content is from a form which is a textarea, the line endings are \r\n. // It's a standard behavior of HTML. - // But we want to store them as \n like what GitHub does. - // And users are unlikely to really need to keep the \r. + // But in most cases, we only want "\n" for EOL + // * Text files: use "\n" by default because "\r\n" sometimes doesn't work in POSIX + // * Actions values: store them as "\n" like what GitHub does. + // And users are unlikely to really need the "\r". // Other than this, we should respect the original content, even leading or trailing spaces. - return strings.ReplaceAll(input, "\r\n", "\n") + return UnsafeBytesToString(NormalizeEOL(UnsafeStringToBytes(input))) } diff --git a/modules/util/util_test.go b/modules/util/util_test.go index ec8b738e543..7dbb14e374b 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -167,9 +167,9 @@ func TestToTitleCase(t *testing.T) { assert.Equal(t, `Foo Bar Baz`, ToTitleCase(`FOO BAR BAZ`)) } -func TestReserveLineBreakForTextarea(t *testing.T) { - assert.Equal(t, "test\ndata", ReserveLineBreakForTextarea("test\r\ndata")) - assert.Equal(t, "test\ndata\n", ReserveLineBreakForTextarea("test\r\ndata\r\n")) +func TestNormalizeStringEOL(t *testing.T) { + assert.Equal(t, "test\ndata", NormalizeStringEOL("test\r\ndata")) + assert.Equal(t, " test\ndata\n ", NormalizeStringEOL(" test\rdata\r ")) } func TestOptionalArg(t *testing.T) { @@ -184,3 +184,10 @@ func TestOptionalArg(t *testing.T) { assert.Equal(t, 42, bar(nil)) assert.Equal(t, 100, bar(nil, 100)) } + +func TestPathEscapeSegments(t *testing.T) { + assert.Equal(t, "a", PathEscapeSegments("a")) + assert.Equal(t, "a/b", PathEscapeSegments("a/b")) + assert.Equal(t, "a/b%20c", PathEscapeSegments("a/b c")) + assert.Equal(t, "a/b+c", PathEscapeSegments("a/b+c")) +} diff --git a/modules/validation/binding.go b/modules/validation/binding.go index 1a830ed2ebe..c9de1be96c1 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/glob" "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/util" "gitea.com/go-chi/binding" ) @@ -51,7 +50,6 @@ func (j jsonProvider) NewEncoder(writer io.Writer) binding.JSONEncoder { func AddBindingRules() { binding.JSONProvider = jsonProvider{} addGitRefNameBindingRule() - addValidURLListBindingRule() addValidURLBindingRule() addValidSiteURLBindingRule() addGlobPatternRule() @@ -80,33 +78,6 @@ func addGitRefNameBindingRule() { }) } -func addValidURLListBindingRule() { - // URL validation rule - binding.AddRule(&binding.Rule{ - IsMatch: func(rule string) bool { - return rule == "ValidUrlList" - }, - IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) { - str := fmt.Sprintf("%v", val) - if len(str) == 0 { - errs.Add([]string{name}, binding.ERR_URL, "Url") - return false, errs - } - - ok := true - urls := util.SplitTrimSpace(str, "\n") - for _, u := range urls { - if !IsValidURL(u) { - ok = false - errs.Add([]string{name}, binding.ERR_URL, u) - } - } - - return ok, errs - }, - }) -} - func addValidURLBindingRule() { // URL validation rule binding.AddRule(&binding.Rule{ diff --git a/modules/validation/binding_test.go b/modules/validation/binding_test.go index 0cd328f312a..d30eb1bbb1c 100644 --- a/modules/validation/binding_test.go +++ b/modules/validation/binding_test.go @@ -27,7 +27,6 @@ type ( TestForm struct { BranchName string `form:"BranchName" binding:"GitRefName"` URL string `form:"ValidUrl" binding:"ValidUrl"` - URLs string `form:"ValidUrls" binding:"ValidUrlList"` GlobPattern string `form:"GlobPattern" binding:"GlobPattern"` RegexPattern string `form:"RegexPattern" binding:"RegexPattern"` } diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index ee05de74bdf..7695529beba 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -4,7 +4,6 @@ package validation import ( - "net" "net/url" "regexp" "slices" @@ -33,10 +32,6 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct { } }) -func isLoopbackIP(ip string) bool { - return net.ParseIP(ip).IsLoopback() -} - // IsValidURL checks if URL is valid func IsValidURL(uri string) bool { if u, err := url.ParseRequestURI(uri); err != nil || @@ -85,36 +80,9 @@ func IsEmailDomainListed(globs []glob.Glob, email string) bool { return false } -// IsAPIURL checks if URL is current Gitea instance API URL -func IsAPIURL(uri string) bool { - return strings.HasPrefix(strings.ToLower(uri), strings.ToLower(setting.AppURL+"api")) -} - -// IsValidExternalURL checks if URL is valid external URL -func IsValidExternalURL(uri string) bool { - if !IsValidURL(uri) || IsAPIURL(uri) { - return false - } - - u, err := url.ParseRequestURI(uri) - if err != nil { - return false - } - - // Currently check only if not loopback IP is provided to keep compatibility - if isLoopbackIP(u.Hostname()) || strings.ToLower(u.Hostname()) == "localhost" { - return false - } - - // TODO: Later it should be added to allow local network IP addresses - // only if allowed by special setting - - return true -} - // IsValidExternalTrackerURLFormat checks if URL matches required syntax for external trackers func IsValidExternalTrackerURLFormat(uri string) bool { - if !IsValidExternalURL(uri) { + if !IsValidURL(uri) { return false } vars := globalVars() diff --git a/modules/validation/helpers_test.go b/modules/validation/helpers_test.go index 75f73b97fca..a3c3acb16c4 100644 --- a/modules/validation/helpers_test.go +++ b/modules/validation/helpers_test.go @@ -6,9 +6,6 @@ package validation import ( "testing" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/test" - "github.com/stretchr/testify/assert" ) @@ -47,51 +44,7 @@ func Test_IsValidURL(t *testing.T) { } } -func Test_IsValidExternalURL(t *testing.T) { - defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")() - - cases := []struct { - description string - url string - valid bool - }{ - { - description: "Current instance URL", - url: "https://try.gitea.io/test", - valid: true, - }, - { - description: "Loopback IPv4 URL", - url: "http://127.0.1.1:5678/", - valid: false, - }, - { - description: "Current instance API URL", - url: "https://try.gitea.io/api/v1/user/follow", - valid: false, - }, - { - description: "Local network URL", - url: "http://192.168.1.2/api/v1/user/follow", - valid: true, - }, - { - description: "Local URL", - url: "http://LOCALHOST:1234/whatever", - valid: false, - }, - } - - for _, testCase := range cases { - t.Run(testCase.description, func(t *testing.T) { - assert.Equal(t, testCase.valid, IsValidExternalURL(testCase.url)) - }) - } -} - func Test_IsValidExternalTrackerURLFormat(t *testing.T) { - defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")() - cases := []struct { description string url string @@ -105,7 +58,7 @@ func Test_IsValidExternalTrackerURLFormat(t *testing.T) { { description: "Local external tracker URL with all placeholders", url: "https://127.0.0.1/{user}/{repo}/issues/{index}", - valid: false, + valid: true, }, { description: "External tracker URL with typo placeholder", diff --git a/modules/validation/validurllist_test.go b/modules/validation/validurllist_test.go deleted file mode 100644 index cccc570a1a8..00000000000 --- a/modules/validation/validurllist_test.go +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package validation - -import ( - "testing" - - "gitea.com/go-chi/binding" -) - -func Test_ValidURLListValidation(t *testing.T) { - AddBindingRules() - - // This is a copy of all the URL tests cases, plus additional ones to - // account for multiple URLs - urlListValidationTestCases := []validationTestCase{ - { - description: "Empty URL", - data: TestForm{ - URLs: "", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "URL without port", - data: TestForm{ - URLs: "http://test.lan/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "URL with port", - data: TestForm{ - URLs: "http://test.lan:3000/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "URL with IPv6 address without port", - data: TestForm{ - URLs: "http://[::1]/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "URL with IPv6 address with port", - data: TestForm{ - URLs: "http://[::1]:3000/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "Invalid URL", - data: TestForm{ - URLs: "http//test.lan/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "http//test.lan/", - }, - }, - }, - { - description: "Invalid schema", - data: TestForm{ - URLs: "ftp://test.lan/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "ftp://test.lan/", - }, - }, - }, - { - description: "Invalid port", - data: TestForm{ - URLs: "http://test.lan:3x4/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "http://test.lan:3x4/", - }, - }, - }, - { - description: "Invalid port with IPv6 address", - data: TestForm{ - URLs: "http://[::1]:3x4/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "http://[::1]:3x4/", - }, - }, - }, - { - description: "Multi URLs", - data: TestForm{ - URLs: "http://test.lan:3000/\nhttp://test.local/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "Multi URLs with newline", - data: TestForm{ - URLs: "http://test.lan:3000/\nhttp://test.local/\n", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "List with invalid entry", - data: TestForm{ - URLs: "http://test.lan:3000/\nhttp://[::1]:3x4/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "http://[::1]:3x4/", - }, - }, - }, - { - description: "List with two invalid entries", - data: TestForm{ - URLs: "ftp://test.lan:3000/\nhttp://[::1]:3x4/\n", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "ftp://test.lan:3000/", - }, - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "http://[::1]:3x4/", - }, - }, - }, - } - - for _, testCase := range urlListValidationTestCases { - t.Run(testCase.description, func(t *testing.T) { - performValidationTest(t, testCase) - }) - } -} diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 05047ad3bdb..988beb47c52 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -78,82 +78,97 @@ func GetInclude(field reflect.StructField) string { return getRuleBody(field, "Include(") } -// Validate validate +func ReportValidationError(errs binding.Errors, data map[string]any, fieldName, classification, errorMsg string) binding.Errors { + errs.Add([]string{fieldName}, classification, errorMsg) + + data["HasError"] = true + data["ErrorMsg"] = fieldName + ": " + errorMsg + data["Err_"+fieldName] = true + // there is already a reported validation error, so no need to generate default error messages in Validate() + data["HasErrorFormValidation"] = true + return errs +} + func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Locale) binding.Errors { - if errs.Len() == 0 { + // try to restore the form's values as much as possible, + // especially for RenderWithErrDeprecated to re-render the form with errors + AssignForm(f, data) + + if errs.Len() == 0 || data["HasErrorFormValidation"] == true { return errs } + // if HasError=true, then must set default error message + // because still a lot of places use `ctx.Data["ErrorMsg"].(string)` even if the error fields can't be found data["HasError"] = true - // If the field with name errs[0].FieldNames[0] is not found in form - // somehow, some code later on will panic on Data["ErrorMsg"].(string). - // So initialize it to some default. - data["ErrorMsg"] = l.Tr("form.unknown_error") - AssignForm(f, data) + data["ErrorMsg"] = l.TrString("form.unknown_error") typ := reflect.TypeOf(f) - if typ.Kind() == reflect.Ptr { typ = typ.Elem() } - if field, ok := typ.FieldByName(errs[0].FieldNames[0]); ok { - fieldName := field.Tag.Get("form") - if fieldName != "-" { - data["Err_"+field.Name] = true - - trName := field.Tag.Get("locale") - if len(trName) == 0 { - trName = l.TrString("form." + field.Name) - } else { - trName = l.TrString(trName) - } - - switch errs[0].Classification { - case binding.ERR_REQUIRED: - data["ErrorMsg"] = trName + l.TrString("form.require_error") - case binding.ERR_ALPHA_DASH: - data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error") - case binding.ERR_ALPHA_DASH_DOT: - data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error") - case validation.ErrGitRefName: - data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error") - case binding.ERR_SIZE: - data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field)) - case binding.ERR_MIN_SIZE: - data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field)) - case binding.ERR_MAX_SIZE: - data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field)) - case binding.ERR_EMAIL: - data["ErrorMsg"] = trName + l.TrString("form.email_error") - case binding.ERR_URL: - data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message) - case binding.ERR_INCLUDE: - data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field)) - case validation.ErrGlobPattern: - data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message) - case validation.ErrRegexPattern: - data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message) - case validation.ErrUsername: - data["ErrorMsg"] = trName + l.TrString("form.username_error") - case validation.ErrInvalidGroupTeamMap: - data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message) - case validation.ErrInvalidBadgeSlug: - data["ErrorMsg"] = trName + l.TrString("form.invalid_slug_error") - default: - msg := errs[0].Classification - if msg != "" && errs[0].Message != "" { - msg += ": " - } - - msg += errs[0].Message - if msg == "" { - msg = l.TrString("form.unknown_error") - } - data["ErrorMsg"] = trName + ": " + msg - } - return errs - } + field, fieldExists := typ.FieldByName(errs[0].FieldNames[0]) + if !fieldExists { + return errs } + + if field.Tag.Get("form") == "-" { + return errs + } + + data["Err_"+field.Name] = true + + trName := field.Tag.Get("locale") + if len(trName) == 0 { + trName = l.TrString("form." + field.Name) + } else { + trName = l.TrString(trName) + } + + switch errs[0].Classification { + case binding.ERR_REQUIRED: + data["ErrorMsg"] = trName + l.TrString("form.require_error") + case binding.ERR_ALPHA_DASH: + data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error") + case binding.ERR_ALPHA_DASH_DOT: + data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error") + case validation.ErrGitRefName: + data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error") + case binding.ERR_SIZE: + data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field)) + case binding.ERR_MIN_SIZE: + data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field)) + case binding.ERR_MAX_SIZE: + data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field)) + case binding.ERR_EMAIL: + data["ErrorMsg"] = trName + l.TrString("form.email_error") + case binding.ERR_URL: + data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message) + case binding.ERR_INCLUDE: + data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field)) + case validation.ErrGlobPattern: + data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message) + case validation.ErrRegexPattern: + data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message) + case validation.ErrUsername: + data["ErrorMsg"] = trName + l.TrString("form.username_error") + case validation.ErrInvalidGroupTeamMap: + data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message) + case validation.ErrInvalidBadgeSlug: + data["ErrorMsg"] = trName + l.TrString("form.invalid_slug_error") + default: + msg := errs[0].Classification + if msg != "" && errs[0].Message != "" { + msg += ": " + } + + msg += errs[0].Message + if msg == "" { + msg = l.TrString("form.unknown_error") + } + data["ErrorMsg"] = trName + ": " + msg + } + return errs } diff --git a/modules/web/middleware/locale.go b/modules/web/middleware/locale.go index 34a16f04e7f..fc396f08081 100644 --- a/modules/web/middleware/locale.go +++ b/modules/web/middleware/locale.go @@ -51,9 +51,3 @@ func Locale(resp http.ResponseWriter, req *http.Request) translation.Locale { func SetLocaleCookie(resp http.ResponseWriter, lang string, maxAge int) { SetSiteCookie(resp, "lang", lang, maxAge) } - -// DeleteLocaleCookie convenience function to delete the locale cookie consistently -// Setting the lang cookie will trigger the middleware to reset the language to previous state. -func DeleteLocaleCookie(resp http.ResponseWriter) { - SetSiteCookie(resp, "lang", "", -1) -} diff --git a/modules/web/router.go b/modules/web/router.go index 5ef18e96795..f4575399b9d 100644 --- a/modules/web/router.go +++ b/modules/web/router.go @@ -13,18 +13,12 @@ import ( "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/modules/web/types" "gitea.com/go-chi/binding" "github.com/go-chi/chi/v5" ) -// PreMiddlewareProvider is a special middleware provider which will be executed -// before other middlewares on the same "routing" level (AfterRouting/Group/Methods/Any, but not BeforeRouting). -// A route can do something (e.g.: set middleware options) at the place where it is declared, -// and the code will be executed before other middlewares which are added before the declaration. -// Use cases: mark a route with some meta info, set some options for middlewares, etc. -type PreMiddlewareProvider func(next http.Handler) http.Handler - // Bind binding an obj to a handler's context data func Bind[T any](_ T) http.HandlerFunc { return func(resp http.ResponseWriter, req *http.Request) { @@ -112,7 +106,7 @@ func isNilOrFuncNil(v any) bool { func wrapMiddlewareAppendPre(all []middlewareProvider, middlewares []any) []middlewareProvider { for _, m := range middlewares { - if h, ok := m.(PreMiddlewareProvider); ok && h != nil { + if h, ok := m.(types.PreMiddlewareProvider); ok && h != nil { all = append(all, toHandlerProvider(middlewareProvider(h))) } } @@ -121,7 +115,7 @@ func wrapMiddlewareAppendPre(all []middlewareProvider, middlewares []any) []midd func wrapMiddlewareAppendNormal(all []middlewareProvider, middlewares []any) []middlewareProvider { for _, m := range middlewares { - if _, ok := m.(PreMiddlewareProvider); !ok && !isNilOrFuncNil(m) { + if _, ok := m.(types.PreMiddlewareProvider); !ok && !isNilOrFuncNil(m) { all = append(all, toHandlerProvider(m)) } } diff --git a/modules/web/router_test.go b/modules/web/router_test.go index d424f072e95..c1c314f85e1 100644 --- a/modules/web/router_test.go +++ b/modules/web/router_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web/types" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" @@ -312,12 +313,12 @@ func TestPreMiddlewareProvider(t *testing.T) { root := NewRouter() root.BeforeRouting(h("before-root")) root.AfterRouting(h("root")) - root.Get("/a/1", h("mid"), PreMiddlewareProvider(p("pre-root")), h("end1")) + root.Get("/a/1", h("mid"), types.PreMiddlewareProvider(p("pre-root")), h("end1")) sub := NewRouter() sub.BeforeRouting(h("before-sub")) sub.AfterRouting(h("sub")) - sub.Get("/2", h("mid"), PreMiddlewareProvider(p("pre-sub")), h("end2")) + sub.Get("/2", h("mid"), types.PreMiddlewareProvider(p("pre-sub")), h("end2")) sub.NotFound(h("not-found")) root.Mount("/a", sub) diff --git a/modules/web/routing/context.go b/modules/web/routing/context.go index e302507bf27..7799a24e948 100644 --- a/modules/web/routing/context.go +++ b/modules/web/routing/context.go @@ -10,12 +10,18 @@ import ( "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/reqctx" + "code.gitea.io/gitea/modules/web/types" ) type contextKeyType struct{} var contextKey contextKeyType +func getRequestRecord(ctx context.Context) *requestRecord { + record, _ := ctx.Value(contextKey).(*requestRecord) + return record +} + // RecordFuncInfo records a func info into context func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) { end = func() {} @@ -24,7 +30,7 @@ func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) { traceSpan, end = gtprof.GetTracer().StartInContext(reqCtx, "http.func") traceSpan.SetAttributeString("func", funcInfo.shortName) } - if record, ok := ctx.Value(contextKey).(*requestRecord); ok { + if record := getRequestRecord(ctx); record != nil { record.lock.Lock() record.funcInfo = funcInfo record.lock.Unlock() @@ -32,22 +38,39 @@ func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) { return end } -// MarkLongPolling marks the request is a long-polling request, and the logger may output different message for it -func MarkLongPolling(resp http.ResponseWriter, req *http.Request) { - record, ok := req.Context().Value(contextKey).(*requestRecord) - if !ok { - return +func GetRequestRecordInfo(reqCtx context.Context) (ret struct { + HasRecord bool + IsLongPolling bool +}, +) { + record := getRequestRecord(reqCtx) + if record == nil { + return ret } + ret.HasRecord = true + record.lock.RLock() + ret.IsLongPolling = record.isLongPolling + record.lock.RUnlock() + return ret +} - record.lock.Lock() - record.isLongPolling = true - record.logLevel = log.TRACE - record.lock.Unlock() +// MarkLongPolling marks the request is a long-polling request, and the logger may output different message for it +func MarkLongPolling() types.PreMiddlewareProvider { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + record := getRequestRecord(req.Context()) // it must exist + record.lock.Lock() + record.isLongPolling = true + record.logLevel = log.TRACE + record.lock.Unlock() + next.ServeHTTP(w, req) + }) + } } func MarkLogLevelTrace(resp http.ResponseWriter, req *http.Request) { - record, ok := req.Context().Value(contextKey).(*requestRecord) - if !ok { + record := getRequestRecord(req.Context()) + if record == nil { return } @@ -58,8 +81,8 @@ func MarkLogLevelTrace(resp http.ResponseWriter, req *http.Request) { // UpdatePanicError updates a context's error info, a panic may be recovered by other middlewares, but we still need to know that. func UpdatePanicError(ctx context.Context, err error) { - record, ok := ctx.Value(contextKey).(*requestRecord) - if !ok { + record := getRequestRecord(ctx) + if record == nil { return } diff --git a/modules/web/types/premiddleware.go b/modules/web/types/premiddleware.go new file mode 100644 index 00000000000..275b55b8c69 --- /dev/null +++ b/modules/web/types/premiddleware.go @@ -0,0 +1,13 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package types + +import "net/http" + +// PreMiddlewareProvider is a special middleware provider which will be executed +// before other middlewares on the same "routing" level (AfterRouting/Group/Methods/Any, but not BeforeRouting). +// A route can do something (e.g.: set middleware options) at the place where it is declared, +// and the code will be executed before other middlewares which are added before the declaration. +// Use cases: mark a route with some meta info, set some options for middlewares, etc. +type PreMiddlewareProvider func(next http.Handler) http.Handler diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 13d197babd3..6281ff8f547 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -639,14 +639,8 @@ "user.block.unblock.failure": "Failed to unblock user: %s", "user.block.blocked": "You have blocked this user.", "user.block.title": "Block a user", - "user.block.info": "Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.", - "user.block.info_1": "Blocking a user prevents the following actions on your account and your repositories:", - "user.block.info_2": "following your account", - "user.block.info_3": "send you notifications by @mentioning your username", - "user.block.info_4": "inviting you as a collaborator to their repositories", - "user.block.info_5": "starring, forking or watching on repositories", - "user.block.info_6": "opening and commenting on issues or pull requests", - "user.block.info_7": "reacting to your comments in issues or pull requests", + "user.block.info": "Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues.", + "user.block.info.docs": "Learn more about blocking a user.", "user.block.user_to_block": "User to block", "user.block.note": "Note", "user.block.note.title": "Optional note:", @@ -1856,6 +1850,7 @@ "repo.pulls.merge_manually": "Manually merged", "repo.pulls.merge_commit_id": "The merge commit ID", "repo.pulls.require_signed_wont_sign": "The branch requires signed commits but this merge will not be signed", + "repo.pulls.require_signed_head_commits_unverified": "The branch requires signed commits but one or more commits on this pull request are not verified", "repo.pulls.invalid_merge_option": "You cannot use this merge option for this pull request.", "repo.pulls.merge_conflict": "Merge Failed: There was a conflict while merging. Hint: Try a different strategy.", "repo.pulls.merge_conflict_summary": "Error Message", @@ -3777,9 +3772,11 @@ "actions.runs.delete.description": "Are you sure you want to permanently delete this workflow run? This action cannot be undone.", "actions.runs.not_done": "This workflow run is not done.", "actions.runs.view_workflow_file": "View workflow file", - "actions.runs.workflow_graph": "Workflow Graph", "actions.runs.summary": "Summary", "actions.runs.all_jobs": "All jobs", + "actions.runs.attempt": "Attempt", + "actions.runs.latest": "Latest", + "actions.runs.latest_attempt": "Latest attempt", "actions.runs.triggered_via": "Triggered via %s", "actions.runs.total_duration": "Total duration:", "actions.workflow.disable": "Disable Workflow", diff --git a/options/locale/locale_ko-KR.json b/options/locale/locale_ko-KR.json index e4ea00f31c7..4576b8bf824 100644 --- a/options/locale/locale_ko-KR.json +++ b/options/locale/locale_ko-KR.json @@ -122,6 +122,7 @@ "unpin": "고정 해제", "artifacts": "아티팩트", "expired": "만료됨", + "artifact_expires_at": "%s에 만료됨", "confirm_delete_artifact": "아티팩트 '%s'를 삭제하시겠습니까?", "archived": "아카이빙됨", "concept_system_global": "글로벌", @@ -173,6 +174,8 @@ "search.org_kind": "조직 검색…", "search.team_kind": "팀 검색…", "search.code_kind": "코드 검색…", + "search.code_empty": "코드 검색 시작.", + "search.code_empty_description": "코드 전체에서 검색할 키워드를 입력하세요.", "search.code_search_unavailable": "현재 코드 검색이 불가능합니다. 사이트 운영자에게 문의하세요.", "search.code_search_by_git_grep": "현재 코드 검색 결과는 \"git grep\"을 통해 제공됩니다. 사이트 운영자가 리포지토리 인덱서를 활성화하면 더 나은 결과를 얻을 수 있습니다.", "search.package_kind": "패키지 검색…", @@ -213,11 +216,15 @@ "editor.buttons.switch_to_legacy.tooltip": "기존 편집기를 대신 사용", "editor.buttons.enable_monospace_font": "고정폭 글꼴 활성화", "editor.buttons.disable_monospace_font": "고정폭 글꼴 비활성화", + "editor.code_editor.command_palette": "명령 팔레트", + "editor.code_editor.find": "검색", + "editor.code_editor.placeholder": "파일 내용을 여기에 입력", "filter.string.asc": "A–Z", "filter.string.desc": "Z–A", "error.occurred": "오류가 발생했습니다", "error.report_message": "Gitea의 버그라고 생각되면, GitHub에서 해당하는 이슈를 검색하거나 새 이슈를 등록해 주시길 바랍니다.", "error.not_found": "대상을 찾을 수 없습니다.", + "error.permission_denied": "권한 거부됨.", "error.network_error": "네트워크 오류", "startpage.app_desc": "편리한 설치형 Git 서비스", "startpage.install": "쉬운 설치", @@ -264,7 +271,7 @@ "install.lfs_path": "Git LFS 루트 경로", "install.lfs_path_helper": "Git LFS에 저장된 파일들은 이 디렉토리에 저장됩니다. LFS를 사용하지 않는다면 빈칸으로 남겨주세요.", "install.run_user": "실행 사용자명", - "install.run_user_helper": "Gitea 를 실행할 시스템 사용자명을 넣으세요. 이 사용자는 리포지토리 루트 경로에 대한 권한이 있어야 합니다.", + "install.run_user_helper": "Gitea 를 실행할 운영체제 사용자명을 넣으세요. 이 사용자는 리포지토리 루트 경로에 대한 권한이 있어야 합니다.", "install.domain": "서버 도메인", "install.domain_helper": "서버의 도메인 또는 호스트 주소.", "install.ssh_port": "SSH 서버 포트", @@ -311,7 +318,6 @@ "install.invalid_db_table": "데이터베이스 테이블 '%s' 이 유효하지 않습니다: %v", "install.invalid_repo_path": "리포지토리의 경로가 올바르지 않습니다: %v", "install.invalid_app_data_path": "앱 데이터 경로가 올바르지 않습니다.: %v", - "install.run_user_not_match": "실행 사용자명이 현재 사용자명과 다릅니다.: %s -> %s", "install.internal_token_failed": "내부 토큰의 생성에 실패했습니다: %v", "install.secret_key_failed": "비밀 키 생성에 실패했습니다: %v", "install.save_config_failed": "설정을 저장할 수 없습니다: %v", @@ -634,13 +640,7 @@ "user.block.blocked": "해당 사용자를 차단했습니다.", "user.block.title": "사용자 차단", "user.block.info": "사용자를 차단하면 해당 사용자가 리포지토리에서 풀 리퀘스트나 이슈를 생성하거나 댓글을 작성하는 등의 활동들 할 수 없게 됩니다. 사용자 차단에 대해 자세히 알아보세요.", - "user.block.info_1": "사용자를 차단하면 계정과 리포지토리에서 다음 작업이 차단됩니다:", - "user.block.info_2": "당신의 계정 팔로우", - "user.block.info_3": "사용자 아이디를 @멘션하여 알림 보내기", - "user.block.info_4": "리포지토리의 공동작업자로 초대", - "user.block.info_5": "리포지토리에 대해 별표, 포크 또는 구독", - "user.block.info_6": "이슈나 풀 리퀘스트 생성 및 댓글 작성", - "user.block.info_7": "이슈나 풀 리퀘스트에 있는 당신의 댓글에 대한 반응", + "user.block.info.docs": "사용자 차단에 대해 자세히 알아보기.", "user.block.user_to_block": "차단할 사용자", "user.block.note": "노트", "user.block.note.title": "노트 (선택 사항):", @@ -969,7 +969,6 @@ "repo.visibility_description": "소유자 또는 권한이 있는 조직 멤버만 볼 수 있습니다.", "repo.visibility_helper": "비공개 리포지토리로 만들기", "repo.visibility_helper_forced": "사이트 운영자가 새 리포지토리에 대해 비공개로만 생성되도록 하였습니다.", - "repo.visibility_fork_helper": "(이를 변경하면 모든 포크가 영향을 받게 됩니다.)", "repo.clone_helper": "클로닝에 도움이 필요하면 Help에 방문하세요.", "repo.fork_repo": "리포지토리 포크", "repo.fork_from": "원본 프로젝트 :", @@ -1041,6 +1040,7 @@ "repo.forks": "포크", "repo.stars": "별점", "repo.reactions_more": "그리고 %d 더", + "repo.reactions": "리액션", "repo.unit_disabled": "사이트 운영자가 이 리포지토리 섹션을 비활성화했습니다.", "repo.language_other": "기타", "repo.adopt_search": "편입되지 않은 리포지토리를 찾을 사용자명을 입력하세요... (모두 찾으려면 비워두세요)", @@ -1061,8 +1061,8 @@ "repo.transfer.accept_desc": "\"%s\" 으로 이전", "repo.transfer.reject": "이전 거부", "repo.transfer.reject_desc": "\"%s\" 으로의 이전 취소", - "repo.transfer.no_permission_to_accept": "이 이전을 수락할 권한이 없습니다.", - "repo.transfer.no_permission_to_reject": "이 이전을 거부할 권한이 없습니다.", + "repo.transfer.is_transferring": "이전 중…", + "repo.transfer.is_transferring_prompt": "리포지토리가 %s로 이전 중 입니다", "repo.desc.private": "비공개", "repo.desc.public": "공개", "repo.desc.public_access": "공개 액세스", @@ -1213,7 +1213,7 @@ "repo.ambiguous_runes_description": "이 파일에는 다른 문자와 혼동될 수 있는 유니코드 문자가 포함되어 있습니다. 이것이 의도적인 것이라고 판단되면, 이 경고를 무시해도 됩니다. Escape 버튼을 눌러 보이지 않는 문자를 표시할 수 있습니다.", "repo.invisible_runes_line": "이 라인에는 보이지 않는 유니코드 문자가 있습니다", "repo.ambiguous_runes_line": "이 라인에는 모호한 유니코드 문자가 있습니다", - "repo.ambiguous_character": "%[1]c [U+%04[1]X]는 %[2]c [U+%04[2]X]와 혼동될 수 있습니다", + "repo.ambiguous_character": "%[1]s 는 %[2]s 와 혼돈될 수 있습니다", "repo.escape_control_characters": "이스케이프", "repo.unescape_control_characters": "이스케이프 해제", "repo.file_copy_permalink": "Permalink 복사", @@ -1354,10 +1354,13 @@ "repo.projects.desc": "프로젝트내의 이슈 및 풀을 관리합니다.", "repo.projects.description": "설명 (선택 사항)", "repo.projects.description_placeholder": "설명", + "repo.projects.empty": "아직 프로젝트가 없습니다.", + "repo.projects.empty_description": "이슈와 풀 리퀘스트를 관리하기 위한 프로젝트를 생성하세요.", "repo.projects.create": "프로젝트 생성", "repo.projects.title": "제목", "repo.projects.new": "새 프로젝트", "repo.projects.new_subheader": "작업을 한곳에서 조율, 추적 및 갱신하여 프로젝트를 투명하고 일정에 맞게 진행되도록 합니다.", + "repo.projects.no_results": "검색과 일치하는 프로젝트 없음.", "repo.projects.create_success": "프로젝트 \"%s\"가 생성되었습니다.", "repo.projects.deletion": "프로젝트 삭제", "repo.projects.deletion_desc": "프로젝트를 삭제하여 모든 관련 이슈에서 제거합니다. 계속하시겠습니까?", @@ -1399,11 +1402,12 @@ "repo.issues.new": "새로운 이슈", "repo.issues.new.title_empty": "제목은 비워둘 수 없습니다.", "repo.issues.new.labels": "레이블", - "repo.issues.new.no_label": "레이블 없음", + "repo.issues.new.no_labels": "라벨 없음", "repo.issues.new.clear_labels": "레이블 초기화", "repo.issues.new.projects": "프로젝트", "repo.issues.new.clear_projects": "프로젝트 초기화", "repo.issues.new.no_projects": "프로젝트 없음", + "repo.issues.new.no_column": "열 없음", "repo.issues.new.open_projects": "열린 프로젝트", "repo.issues.new.closed_projects": "닫힌 프로젝트", "repo.issues.new.no_items": "항목 없음", @@ -1529,6 +1533,7 @@ "repo.issues.context.edit": "수정하기", "repo.issues.context.delete": "삭제", "repo.issues.no_content": "제공된 설명 없음.", + "repo.issues.comment_no_content": "댓글이 없습니다.", "repo.issues.close": "이슈 닫기", "repo.issues.comment_pull_merged_at": "커밋 %[1]s 을 %[2]s %[3]s에 머지됨", "repo.issues.comment_manually_pull_merged_at": "커밋 %[1]s을 %[2] %[3]s에 수동 머지됨", @@ -1845,6 +1850,7 @@ "repo.pulls.merge_manually": "수동 머지됨", "repo.pulls.merge_commit_id": "머지 커밋 ID", "repo.pulls.require_signed_wont_sign": "브랜치에는 서명된 커밋이 필요하지만 이 머지는 서명되지 않습니다", + "repo.pulls.require_signed_head_commits_unverified": "이 브랜치는 서명된 커밋을 요구하지만, 현재 풀 리퀘스트에 포함된 하나 이상의 커밋이 검증되지 않았습니다", "repo.pulls.invalid_merge_option": "이 풀 리퀘스트에서 설정한 머지 옵션을 사용하실 수 없습니다.", "repo.pulls.merge_conflict": "머지 실패: 머지 중에 충돌이 발생했습니다. 힌트: 다른 전략을 시도하십시오.", "repo.pulls.merge_conflict_summary": "오류 메시지", @@ -2174,7 +2180,8 @@ "repo.settings.transfer_abort_invalid": "존재하지 않는 저장소 이전을 취소할 수 없습니다.", "repo.settings.transfer_abort_success": "리포지토리를 %s로 이전이 성공적으로 취소되었습니다.", "repo.settings.transfer_desc": "이 리포지토리를 사용자 또는 당신이 운영자 권한을 가지고 있는 조직으로 이전합니다.", - "repo.settings.transfer_form_title": "확인을 위해 저장소명을 입력:", + "repo.settings.enter_repo_name_to_confirm": "확인을 위해 리포지토리 이름을 입력:", + "repo.settings.enter_repo_full_name_to_confirm": "확인을 위해 리포지토리 이름 (소유자/이름)을 입력:", "repo.settings.transfer_in_progress": "현재 진행 중인 이전이 있습니다. 이 저장소를 다른 사용자에게 이전하려면 먼저 취소하십시오.", "repo.settings.transfer_notices_1": "- 개별 사용자에게 리포지토리를 이전하면 리포지토리에 대한 액세스 권한을 잃게 됩니다.", "repo.settings.transfer_notices_2": "- 당신이 (공동)소유하고 있는 조직으로 리포지토리를 이전하면 리포지토리에 대한 액세스 권한을 유지합니다.", @@ -2250,13 +2257,14 @@ "repo.settings.webhook.delivery.success": "이벤트가 배송 큐에 추가되었습니다. 배송 이력에 표시되기까지 몇 초가 걸릴 수 있습니다.", "repo.settings.githooks_desc": "Git Hook은 Git 자체에 의해 동작합니다. 아래에서 hook 파일을 편집하여 커스텀 동작을 설정할 수 있습니다.", "repo.settings.githook_edit_desc": "후크가 비활성인 경우 샘플 콘텐츠가 표시됩니다. 내용을 빈 값으로 두면 이 훅은 비활성화됩니다.", - "repo.settings.githook_name": "Hook 이름", - "repo.settings.githook_content": "Hook 내용", "repo.settings.update_githook": "Hook 업데이트", "repo.settings.add_webhook_desc": "Gitea는 지정된 콘텐츠 유형과 함께 POST 요청을 대상 URL로 보냅니다. 자세한 내용은 웹훅 가이드에서 확인하세요.", "repo.settings.payload_url": "대상 URL", "repo.settings.http_method": "HTTP 메서드", "repo.settings.content_type": "POST Content Type", + "repo.settings.webhook.name": "웹훅 이름", + "repo.settings.webhook.name_helper": "이 웹후크에 친근한 이름을 지정하세요 (옵션)", + "repo.settings.webhook.name_empty": "이름없는 웹혹", "repo.settings.secret": "비밀", "repo.settings.webhook_secret_desc": "웹훅 서버가 비밀 사용을 지원하는 경우, 웹훅 매뉴얼에 따라 비밀을 여기에 채울 수 있습니다.", "repo.settings.slack_username": "사용자 이름", @@ -2477,7 +2485,10 @@ "repo.settings.visibility.private.text": "공개 수준을 비공개로 변경하면 허용된 멤버에게만 리포지토리가가 표시되며, 기존 포크, 구독자, 별점 관계가 삭제 될 수 있습니다.", "repo.settings.visibility.private.bullet_title": "공개 수준을 비공개로 변경하면:", "repo.settings.visibility.private.bullet_one": "허용된 멤버에게만 리포지토리가 표시됩니다.", - "repo.settings.visibility.private.bullet_two": "포크, 구독자, 별점와의 관계를 제거할 수 있습니다.", + "repo.settings.visibility.private.bullet_two": "포크, 팔로워, 별점와의 관계를 제거할 수 있습니다.", + "repo.settings.visibility.private.stats_stars": "이 리포지토리가 받은 %d 별점을 잃어버릴 수 있습니다.", + "repo.settings.visibility.private.stats_watchers": "이 리포지토리를 구독중인 %d 사람을 잃게될 수 있습니다.", + "repo.settings.visibility.private.stats_forks": "이 리포지토리는 %d 개의 연결된 포크가 있습니다.", "repo.settings.visibility.public.button": "공개로 설정", "repo.settings.visibility.public.text": "공개 수준을 공개로 변경하면 리포지토리가 누구에게나 표시됩니다.", "repo.settings.visibility.public.bullet_title": "공개범위를 공개로 변경하면:", @@ -2713,6 +2724,8 @@ "org.members": "멤버", "org.teams": "팀", "org.code": "코드", + "org.repos.empty": "리포지토리 없음.", + "org.repos.empty_description": "조직과 코드를 공유하기 위한 저장소를 생성하세요.", "org.lower_members": "회원", "org.lower_repositories": "리포지토리", "org.create_new_team": "새 팀", @@ -2805,7 +2818,10 @@ "org.teams.no_desc": "이 팀은 설명이 없습니다.", "org.teams.settings": "설정", "org.teams.owners_permission_desc": "소유자는 모든 리포지토리에 대한 전체 액세스 권한을 가지며 조직에 대한 운영자 액세스 권한을 가집니다.", + "org.teams.owners_permission_suggestion": "구성원별로 세분화된 접근 권한을 부여하기 위해 새로운 팀을 생성할 수 있습니다.", "org.teams.members": "팀 구성원", + "org.teams.manage_team_member": "팀과 멤버를 관리", + "org.teams.manage_team_member_prompt": "회원은 팀을 통해 관리됩니다. 사용자를 팀에 추가하여 이 조직에 초대하세요.", "org.teams.update_settings": "설정 업데이트", "org.teams.delete_team": "팀 삭제", "org.teams.add_team_member": "팀 구성원 추가", @@ -2846,6 +2862,8 @@ "org.worktime.date_range_end": "종료 날짜", "org.worktime.query": "쿼리", "org.worktime.time": "시간", + "org.worktime.empty": "작업 항목 데이터가 아직 없음.", + "org.worktime.empty_description": "추적 시간을 확인하려면 날짜 범위를 조정하세요.", "org.worktime.by_repositories": "리포지토리 별", "org.worktime.by_milestones": "마일스톤 별", "org.worktime.by_members": "멤버 별", @@ -3160,6 +3178,8 @@ "admin.auths.oauth2_required_claim_name_helper": "이 이름을 설정하여 이 소스에서의 로그인을 이 이름으로 된 클레임을 가진 사용자로 제한합니다", "admin.auths.oauth2_required_claim_value": "Claim Value가 필수적으로 요구됨", "admin.auths.oauth2_required_claim_value_helper": "이 값을 설정하여 이 소스에서의 로그인을 이 이름과 값을 가진 클레임을 가진 사용자로 제한합니다", + "admin.auths.open_id_connect_external_id_claim": "외부 ID Claim 이름 (옵션)", + "admin.auths.open_id_connect_external_id_claim_helper": "사용자의 외부 ID로 사용할 클레임 이름입니다. 기본값은 \"sub\"입니다. Azure AD / Entra ID의 경우, Azure AD V2 공급자에서 마이그레이션할 때 연속성을 유지하려면 이 값을 \"oid\"로 설정하십시오. 참고: \"oid\" 클레임을 사용하려면 위의 Scopes 필드에 \"profile\" 스코프가 포함되어야 합니다.", "admin.auths.oauth2_group_claim_name": "이 소스에 대한 그룹명을 제공하는 클레임명. (선택 사항)", "admin.auths.oauth2_full_name_claim_name": "풀 네임 클레임 이름. (선택사항 — 설정하는 경우 사용자의 풀 네임이 이 클레임과 항상 동기화 됩니다)", "admin.auths.oauth2_ssh_public_key_claim_name": "SSH 공개 키 클레임 이름", @@ -3212,10 +3232,8 @@ "admin.config.server_config": "서버 설정", "admin.config.app_name": "사이트 제목", "admin.config.app_ver": "Gitea 버전", - "admin.config.app_url": "Gitea의 기본 URL", "admin.config.custom_conf": "설정 파일 경로", "admin.config.custom_file_root_path": "커스텀 파일 루트 경로", - "admin.config.domain": "서버 도메인", "admin.config.disable_router_log": "라우터 로그 비활성화", "admin.config.run_user": "실행 사용자명", "admin.config.run_mode": "실행 모드", @@ -3496,6 +3514,7 @@ "packages.dependencies": "의존성", "packages.keywords": "키워드", "packages.details": "상세 정보", + "packages.name": "패키지 이름", "packages.details.author": "작성자", "packages.details.project_site": "프로젝트 사이트", "packages.details.repository_site": "리포지토리 사이트", @@ -3591,6 +3610,18 @@ "packages.swift.registry": "명령줄로 이 레지스트리를 설정:", "packages.swift.install": "Package.swift 파일에 패키지를 추가:", "packages.swift.install2": "그리고 다음 명령을 실행:", + "packages.terraform.install": "HTTP 백엔드를 사용하도록 상태를 설정", + "packages.terraform.install2": "그리고 다음 명령을 실행:", + "packages.terraform.lock_status": "잠금 상태", + "packages.terraform.locked_by": "%s에 의해 잠김", + "packages.terraform.unlocked": "잠금 해제됨", + "packages.terraform.lock": "잠금", + "packages.terraform.unlock": "잠금 해제", + "packages.terraform.lock.success": "Terraform 상태가 성공적으로 잠김.", + "packages.terraform.unlock.success": "Terraform 상태가 성공적으로 잠금 해제됨.", + "packages.terraform.lock.error.already_locked": "Terraform 상태가 이미 잠김.", + "packages.terraform.delete.locked": "Terraform 상태가 잠김 상태로 삭제할 수 없습니다.", + "packages.terraform.delete.latest": "최신 버전의 Terraform 상태는 삭제할 수 없습니다.", "packages.vagrant.install": "Vagrant 박스를 추가하려면 다음 명령을 실행:", "packages.settings.link": "이 패키지를 리포지토리에 연결", "packages.settings.link.description": "패키지를 리포지토리에 연결하면 리포지토리의 패키지 목록에 표시됩니다.", @@ -3604,8 +3635,13 @@ "packages.settings.delete": "패키지 삭제", "packages.settings.delete.description": "패키지 삭제는 영구적이며 복구가 불가능합니다.", "packages.settings.delete.notice": "%s (%s) 삭제 하려고 합니다. 이 작업은 되돌릴 수 없습니다. 정말로 삭제하시겠습니까?", + "packages.settings.delete.notice.package": "%s와 해당 파일의 모든 버전을 삭제하려고 합니다. 이 작업은 되돌릴 수 없습니다. 정말 삭제하시겠습니까?", "packages.settings.delete.success": "패키지가 삭제되었습니다.", + "packages.settings.delete.version.success": "패키지 버전이 삭제되었습니다.", "packages.settings.delete.error": "패키지 삭제에 실패함.", + "packages.settings.delete.version": "버전 삭제", + "packages.settings.delete.confirm": "확인을 위해 패키지 이름을 입력", + "packages.settings.delete.invalid_package_name": "입력한 패키지의 이름이 올바르지 않습니다.", "packages.owner.settings.cargo.title": "Cargo 레지스트리 인덱스", "packages.owner.settings.cargo.initialize": "인덱스 초기화", "packages.owner.settings.cargo.initialize.description": "Cargo 레지스트리를 사용하려면 특별한 인덱스 Git 리포지토리가 필요합니다. 이 옵션을 사용하면 리포지토리를 (재)생성하고 자동으로 구성합니다.", @@ -3712,6 +3748,8 @@ "actions.runs.workflow_run_count_1": "%d 개 워크플로우 실행", "actions.runs.workflow_run_count_n": "%d 개 워크플로우 실행", "actions.runs.commit": "커밋", + "actions.runs.run_details": "실행 상세정보", + "actions.runs.workflow_file": "워크플로우 파일", "actions.runs.scheduled": "예약됨", "actions.runs.pushed_by": "다음이 푸시함", "actions.runs.invalid_workflow_helper": "워크플로 구성 파일이 유효하지 않습니다. 구성 파일을 확인하세요: %s", @@ -3734,9 +3772,11 @@ "actions.runs.delete.description": "이 워크플로우 실행을 영구적으로 삭제하시겠습니까? 이 동작은 되돌릴 수 없습니다.", "actions.runs.not_done": "이 워크플로우 실행은 완료되지 않았습니다.", "actions.runs.view_workflow_file": "워크플로우 파일 표시", - "actions.runs.workflow_graph": "워크플로우 그래프", "actions.runs.summary": "요약", "actions.runs.all_jobs": "모든 작업", + "actions.runs.attempt": "시도", + "actions.runs.latest": "최신", + "actions.runs.latest_attempt": "최근 시도", "actions.runs.triggered_via": "%s를 통해 트리거됨", "actions.runs.total_duration": "총기간:", "actions.workflow.disable": "워크플로 비활성화", diff --git a/options/locale/locale_zh-CN.json b/options/locale/locale_zh-CN.json index 8bdd8d7faf1..0bab3a0010c 100644 --- a/options/locale/locale_zh-CN.json +++ b/options/locale/locale_zh-CN.json @@ -3,7 +3,7 @@ "dashboard": "首页", "explore_title": "探索", "help": "帮助", - "logo": "徽标", + "logo": "Logo", "sign_in": "登录", "sign_in_with_provider": "使用「%s」登录", "sign_in_or": "或", @@ -18,7 +18,7 @@ "language": "语言选项", "notifications": "通知", "active_stopwatch": "活动时间跟踪器", - "tracked_time_summary": "基于问题列表过滤器的跟踪时间概要", + "tracked_time_summary": "基于工单列表筛选器的跟踪时间概要", "create_new": "创建…", "user_profile_and_more": "个人信息和配置", "signed_in_as": "已登录用户", @@ -81,6 +81,7 @@ "retry": "重试", "rerun": "重新运行", "rerun_all": "重新运行所有任务", + "rerun_failed": "重新运行失败的任务", "save": "保存", "add": "添加", "add_all": "添加所有", @@ -121,6 +122,7 @@ "unpin": "取消置顶", "artifacts": "产物", "expired": "已过期", + "artifact_expires_at": "过期于 %s", "confirm_delete_artifact": "您确定要删除产物「%s」吗?", "archived": "已归档", "concept_system_global": "全局", @@ -168,9 +170,12 @@ "search.exact_tooltip": "仅包含精确匹配搜索词的结果", "search.repo_kind": "搜索仓库…", "search.user_kind": "搜索用户…", + "search.badge_kind": "搜索徽章…", "search.org_kind": "搜索组织…", "search.team_kind": "搜索团队…", "search.code_kind": "搜索代码…", + "search.code_empty": "开始代码搜索。", + "search.code_empty_description": "输入关键字以在整个代码中搜索。", "search.code_search_unavailable": "代码搜索当前不可用。请与网站管理员联系。", "search.code_search_by_git_grep": "当前代码搜索结果由「git grep」提供。如果站点管理员启用仓库索引器,可能会有更好的结果。", "search.package_kind": "搜索软件包…", @@ -207,15 +212,19 @@ "editor.buttons.table.rows": "行数", "editor.buttons.table.cols": "列数", "editor.buttons.mention.tooltip": "提及用户或团队", - "editor.buttons.ref.tooltip": "引用一个问题或合并请求", + "editor.buttons.ref.tooltip": "引用一个工单或合并请求", "editor.buttons.switch_to_legacy.tooltip": "使用旧版编辑器", "editor.buttons.enable_monospace_font": "启用等宽字体", "editor.buttons.disable_monospace_font": "禁用等宽字体", + "editor.code_editor.command_palette": "命令面板", + "editor.code_editor.find": "查找", + "editor.code_editor.placeholder": "在此输入文件内容", "filter.string.asc": "A - Z", "filter.string.desc": "Z - A", "error.occurred": "发生了一个错误", "error.report_message": "如果您确定这是一个 Gitea bug,请在 这里 搜索问题,或在必要时创建一个新工单。", - "error.not_found": "找不到目标。", + "error.not_found": "未找到目标。", + "error.permission_denied": "没有权限。", "error.network_error": "网络错误", "startpage.app_desc": "一款极易搭建的自托管 Git 服务", "startpage.install": "易安装", @@ -262,7 +271,7 @@ "install.lfs_path": "LFS根目录", "install.lfs_path_helper": "存储为Git LFS的文件将被存储在此目录。留空禁用LFS", "install.run_user": "以用户名运行", - "install.run_user_helper": "输入 Gitea 运行的操作系统用户名。请注意,此用户必须具有对仓库根路径的访问权限。", + "install.run_user_helper": "Gitea 运行时所使用的操作系统用户名,它必须对数据路径具有写入权限。该值会自动检测,无法在此处更改。若要使用其他用户,请以该用户重新启动 Gitea。", "install.domain": "服务器域名", "install.domain_helper": "服务器的域名或主机地址。", "install.ssh_port": "SSH 服务端口", @@ -309,7 +318,6 @@ "install.invalid_db_table": "数据库表「%s」无效:%v", "install.invalid_repo_path": "仓库根目录设置无效:%v", "install.invalid_app_data_path": "应用数据路径无效: %v", - "install.run_user_not_match": "运行用户名不是当前的用户名:%s -> %s", "install.internal_token_failed": "生成内部令牌失败:%v", "install.secret_key_failed": "生成密钥失败:%v", "install.save_config_failed": "应用配置保存失败:%v", @@ -542,6 +550,7 @@ "form.glob_pattern_error": "匹配表达式无效:%s.", "form.regex_pattern_error": "正则表达式无效:%s.", "form.username_error": "只能包含字母数字('0-9'、'a-z'、'A-Z')破折号('-')下划线('_')和点('.')。不能以非字母数字字符开头和结尾且不允许连续的非字母数字字符。", + "form.invalid_slug_error": " 无效。", "form.invalid_group_team_map_error": "映射无效: %s", "form.unknown_error": "未知错误:", "form.captcha_incorrect": "验证码不正确。", @@ -574,7 +583,7 @@ "form.password_digit_one": "至少一个数字", "form.password_special_one": "至少一个特殊字符(标点符号,括号,引号等)", "form.enterred_invalid_repo_name": "输入的仓库名称不正确", - "form.enterred_invalid_org_name": "您输入的组织名称不正确。", + "form.enterred_invalid_org_name": "输入的组织名称不正确。", "form.enterred_invalid_owner_name": "新的所有者名称无效。", "form.enterred_invalid_password": "输入的密码不正确", "form.unset_password": "登录用户没有设置密码。", @@ -630,14 +639,8 @@ "user.block.unblock.failure": "取消屏蔽用户失败:%s", "user.block.blocked": "您已屏蔽此用户。", "user.block.title": "屏蔽一个用户", - "user.block.info": "屏蔽用户会阻止他们与仓库进行交互,例如打开或评论合并请求或出现问题。了解更多关于屏蔽用户的信息。", - "user.block.info_1": "阻止用户在您的帐户和仓库中进行以下操作:", - "user.block.info_2": "关注您的账号", - "user.block.info_3": "通过 @ 提及您的用户名向您发送通知", - "user.block.info_4": "邀请您作为协作者到他们的仓库中", - "user.block.info_5": "在仓库中点赞、派生或关注", - "user.block.info_6": "打开和评论工单或合并请求", - "user.block.info_7": "在问题或合并请求中对您的评论做出反应", + "user.block.info": "屏蔽用户会阻止他们与仓库进行交互,例如打开或评论合并请求及问题。", + "user.block.info.docs": "了解更多有关屏蔽用户的信息。", "user.block.user_to_block": "要屏蔽的用户", "user.block.note": "备注", "user.block.note.title": "可选备注:", @@ -645,6 +648,7 @@ "user.block.note.edit": "编辑备注", "user.block.list": "已屏蔽用户", "user.block.list.none": "您没有已屏蔽的用户。", + "settings.general": "常规", "settings.profile": "个人信息", "settings.account": "账号", "settings.appearance": "外观", @@ -676,7 +680,7 @@ "settings.update_language": "更新语言", "settings.update_language_not_found": "语言「%s」不可用。", "settings.update_language_success": "语言已更新。", - "settings.update_profile_success": "您的资料信息已经更新", + "settings.update_profile_success": "您的资料信息已更新。", "settings.change_username": "您的用户名已更改。", "settings.change_username_prompt": "注意:更改您的用户名也更改您的帐户 URL。", "settings.change_username_redirect_prompt": "在其他用户使用您的旧用户名注册前,此旧用户名将会重定向到您的新用户名", @@ -753,7 +757,7 @@ "settings.add_email": "新增邮箱地址", "settings.add_openid": "添加 OpenID URI", "settings.add_email_confirmation_sent": "一封确认邮件已经发送至「%s」,请检查您的收件箱并在 %s 内完成确认注册操作。", - "settings.email_primary_not_found": "找不到选定的电子邮件地址。", + "settings.email_primary_not_found": "未找到选定的邮箱地址。", "settings.add_email_success": "新邮箱地址已添加。", "settings.email_preference_set_success": "邮件首选项已成功设置。", "settings.add_openid_success": "新的 OpenID 地址已添加。", @@ -850,7 +854,7 @@ "settings.access_token_deletion_cancel_action": "取消", "settings.access_token_deletion_confirm_action": "刪除", "settings.access_token_deletion_desc": "删除令牌将撤销程序对您账户的访问权限。此操作无法撤消。是否继续?", - "settings.delete_token_success": "令牌已经被删除。使用该令牌的应用将不再能够访问您的账号。", + "settings.delete_token_success": "令牌已删除。使用该令牌的应用将不再能够访问您的账号。", "settings.repo_and_org_access": "仓库和组织访问权限", "settings.permissions_public_only": "仅公开", "settings.permissions_access_all": "全部(公开、私有和受限)", @@ -869,7 +873,7 @@ "settings.oauth2_applications_desc": "OAuth2 应用允许第三方应用程序在此 Gitea 实例中安全验证用户。", "settings.remove_oauth2_application": "删除 OAuth2 应用程序", "settings.remove_oauth2_application_desc": "删除 OAuth2 应用将撤销所有签名的访问令牌。继续吗?", - "settings.remove_oauth2_application_success": "该应用已删除。", + "settings.remove_oauth2_application_success": "应用已删除。", "settings.create_oauth2_application": "创建新的 OAuth2 应用程序", "settings.create_oauth2_application_button": "创建应用", "settings.create_oauth2_application_success": "您已成功创建一个新的 OAuth2 应用。", @@ -893,7 +897,7 @@ "settings.revoke_key": "撤销", "settings.revoke_oauth2_grant": "撤回权限", "settings.revoke_oauth2_grant_description": "确定撤销此三方应用程序的授权,并阻止此应用程序访问您的数据?", - "settings.revoke_oauth2_grant_success": "成功撤销访问权限。", + "settings.revoke_oauth2_grant_success": "访问权限已成功撤销。", "settings.twofa_desc": "为保护您的账号密码安全,您可以使用智能手机或其它设备来接收时间强相关的一次性密码(TOTP)。", "settings.twofa_recovery_tip": "如果您丢失了您的设备,您将能够使用一次性恢复密钥来重新获得对您账户的访问。", "settings.twofa_is_enrolled": "您的账号已启用了两步验证。", @@ -934,7 +938,7 @@ "settings.delete_with_all_comments": "您的帐户年龄小于 %s。为了避免幽灵评论,所有工单 / 合并请求的评论都将与它一起被删除。", "settings.confirm_delete_account": "确认删除帐户", "settings.delete_account_title": "删除当前帐户", - "settings.delete_account_desc": "确实要永久删除此用户帐户吗?", + "settings.delete_account_desc": "确定要永久删除此用户帐户吗?", "settings.email_notifications.enable": "启用邮件通知", "settings.email_notifications.onmention": "仅被提及时通知", "settings.email_notifications.disable": "停用邮件通知", @@ -965,7 +969,6 @@ "repo.visibility_description": "只有组织所有人或拥有权利的组织成员才能看到。", "repo.visibility_helper": "将仓库设为私有", "repo.visibility_helper_forced": "站点管理员强制要求新仓库为私有。", - "repo.visibility_fork_helper": "(修改该值将会影响到所有派生仓库)", "repo.clone_helper": "不知道如何克隆?查看帮助 。", "repo.fork_repo": "派生仓库", "repo.fork_from": "派生自", @@ -1036,7 +1039,8 @@ "repo.stars_remove_warning": "这将清除此仓库的所有点赞数。", "repo.forks": "派生仓库", "repo.stars": "点赞数", - "repo.reactions_more": "再加载 %d", + "repo.reactions_more": "以及另外 %d 人", + "repo.reactions": "回应", "repo.unit_disabled": "站点管理员已禁用此仓库单元。", "repo.language_other": "其它", "repo.adopt_search": "输入用户名以搜索未被收录的仓库…(留空以查找全部)", @@ -1057,8 +1061,8 @@ "repo.transfer.accept_desc": "转移到「%s」", "repo.transfer.reject": "拒绝转移", "repo.transfer.reject_desc": "取消转移到「%s」", - "repo.transfer.no_permission_to_accept": "您没有权限接受此转移。", - "repo.transfer.no_permission_to_reject": "您没有权限拒绝此转移。", + "repo.transfer.is_transferring": "转移中…", + "repo.transfer.is_transferring_prompt": "仓库正在转移至 %s", "repo.desc.private": "私有库", "repo.desc.public": "公开", "repo.desc.public_access": "公开访问", @@ -1209,7 +1213,7 @@ "repo.ambiguous_runes_description": "此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。", "repo.invisible_runes_line": "此行含有不可见的 unicode 字符", "repo.ambiguous_runes_line": "此行有模棱两可的 unicode 字符", - "repo.ambiguous_character": "%[1]c [U+%04[1]X] 容易和 %[2]c [U+%04[2]X] 混淆", + "repo.ambiguous_character": "%[1]s 容易和 %[2]s 混淆", "repo.escape_control_characters": "Escape", "repo.unescape_control_characters": "Unescape", "repo.file_copy_permalink": "复制永久链接", @@ -1252,7 +1256,7 @@ "repo.editor.delete_this_directory": "删除目录", "repo.editor.must_have_write_access": "您必须具有写权限才能对此文件进行修改操作。", "repo.editor.file_delete_success": "文件「%s」已删除。", - "repo.editor.directory_delete_success": "目录「%s」已被删除。", + "repo.editor.directory_delete_success": "目录「%s」已删除。", "repo.editor.delete_directory": "删除目录「%s」", "repo.editor.name_your_file": "命名文件…", "repo.editor.filename_help": "通过键入名称后跟斜线 (\"/\") 来添加目录。通过在输入框的开头键入「退格」来删除目录。", @@ -1350,18 +1354,21 @@ "repo.projects.desc": "在项目看板中管理工单和合并请求。", "repo.projects.description": "描述(可选)", "repo.projects.description_placeholder": "描述", + "repo.projects.empty": "暂无项目。", + "repo.projects.empty_description": "创建一个项目以协调工单和合并请求。", "repo.projects.create": "创建项目", "repo.projects.title": "标题", "repo.projects.new": "创建项目", "repo.projects.new_subheader": "在一个地方协调、跟踪和更新您的工作,让项目保持透明并按计划进行。", + "repo.projects.no_results": "没有匹配您搜索的项目。", "repo.projects.create_success": "项目「%s」创建成功。", "repo.projects.deletion": "删除项目", "repo.projects.deletion_desc": "删除项目会从所有相关的工单中移除它。是否继续?", - "repo.projects.deletion_success": "该项目已删除。", + "repo.projects.deletion_success": "项目已删除。", "repo.projects.edit": "编辑项目", "repo.projects.edit_subheader": "项目用于组织工单和跟踪进展情况。", "repo.projects.modify": "更新项目", - "repo.projects.edit_success": "项目「%s」更新成功。", + "repo.projects.edit_success": "项目「%s」已更新。", "repo.projects.type.none": "无", "repo.projects.type.basic_kanban": "基础看板", "repo.projects.type.bug_triage": "Bug分类看板", @@ -1373,10 +1380,10 @@ "repo.projects.column.new_submit": "创建列", "repo.projects.column.new": "创建列", "repo.projects.column.set_default": "设为默认", - "repo.projects.column.set_default_desc": "设置此列为未分类问题和合并请求的默认值", + "repo.projects.column.set_default_desc": "设置此列为未分类工单和合并请求的默认值", "repo.projects.column.default_column_hint": "添加到此项目的新工单将被添加到此列", "repo.projects.column.delete": "删除列", - "repo.projects.column.deletion_desc": "删除项目列会将所有相关问题移至默认列。是否继续?", + "repo.projects.column.deletion_desc": "删除项目列会将所有相关工单移至默认列。是否继续?", "repo.projects.column.color": "颜色", "repo.projects.open": "开启", "repo.projects.close": "关闭", @@ -1395,11 +1402,12 @@ "repo.issues.new": "创建工单", "repo.issues.new.title_empty": "标题不能为空", "repo.issues.new.labels": "标签", - "repo.issues.new.no_label": "未选择标签", + "repo.issues.new.no_labels": "未选择标签", "repo.issues.new.clear_labels": "清除选中标签", "repo.issues.new.projects": "项目", "repo.issues.new.clear_projects": "清除项目", "repo.issues.new.no_projects": "未选择项目", + "repo.issues.new.no_column": "未分配列", "repo.issues.new.open_projects": "开启中的项目", "repo.issues.new.closed_projects": "已关闭的项目", "repo.issues.new.no_items": "无可选项", @@ -1438,7 +1446,7 @@ "repo.issues.add_remove_labels": "于 %[3]s 添加标签 %[1]s 移除标签 %[2]s", "repo.issues.add_milestone_at": "于 %[2]s 添加里程碑 %[1]s", "repo.issues.add_project_at": "于 %[2]s 将此工单添加到 %[1]s 项目", - "repo.issues.move_to_column_of_project": "将 %[3]s 上的 %[2]s 移至 %[1]s", + "repo.issues.move_to_column_of_project": "于 %[3]s 将此工单移至项目 %[2]s 的 %[1]s 列", "repo.issues.change_milestone_at": "于 %[3]s 修改里程碑从 %[1]s 到 %[2]s", "repo.issues.change_project_at": "于 %[3]s 将项目从 %[1]s 移至 %[2]s", "repo.issues.remove_milestone_at": "于 %[2]s 删除里程碑 %[1]s", @@ -1525,6 +1533,7 @@ "repo.issues.context.edit": "编辑", "repo.issues.context.delete": "刪除", "repo.issues.no_content": "没有提供说明。", + "repo.issues.comment_no_content": "评论内容不能为空。", "repo.issues.close": "关闭工单", "repo.issues.comment_pull_merged_at": "于 %[3]s 合并提交 %[1]s 到 %[2]s", "repo.issues.comment_manually_pull_merged_at": "于 %[3]s 手动合并提交 %[1]s 到 %[2]s", @@ -1584,7 +1593,7 @@ "repo.issues.label_modify": "编辑标签", "repo.issues.label_deletion": "删除标签", "repo.issues.label_deletion_desc": "删除标签会将其从所有问题中删除。继续?", - "repo.issues.label_deletion_success": "该标签已删除。", + "repo.issues.label_deletion_success": "标签已删除。", "repo.issues.label.filter_sort.alphabetically": "按字母顺序排序", "repo.issues.label.filter_sort.reverse_alphabetically": "按字母逆序排序", "repo.issues.label.filter_sort.by_size": "最小尺寸", @@ -1618,7 +1627,7 @@ "repo.issues.comment_on_locked": "您不能对锁定的问题发表评论。", "repo.issues.delete": "删除", "repo.issues.delete.title": "是否删除工单?", - "repo.issues.delete.text": "您真的要删除这个工单吗?(该操作将会永久删除所有内容。如果您需要保留,请关闭它)", + "repo.issues.delete.text": "确定要删除此工单吗?(这将永久移除所有内容。如果您只是想保留归档,请关闭它)", "repo.issues.tracker": "时间跟踪", "repo.issues.timetracker_timer_start": "启动计时器", "repo.issues.timetracker_timer_stop": "停止计时器", @@ -1841,6 +1850,7 @@ "repo.pulls.merge_manually": "手动合并", "repo.pulls.merge_commit_id": "合并提交 ID", "repo.pulls.require_signed_wont_sign": "分支需要签名的提交,但这个合并将不会被签名", + "repo.pulls.require_signed_head_commits_unverified": "该分支要求提交必须通过签名验证,但此合并请求中的一个或多个提交未通过验证", "repo.pulls.invalid_merge_option": "您可以在此合并请求中使用合并选项。", "repo.pulls.merge_conflict": "合并失败:合并时有冲突发生。提示:采用其它合并策略。", "repo.pulls.merge_conflict_summary": "错误信息", @@ -1894,7 +1904,7 @@ "repo.pulls.auto_merge_newly_scheduled_comment": "已于 %[1]s 设置此合并请求在所有检查成功后自动合并", "repo.pulls.auto_merge_canceled_schedule_comment": "已于 %[1]s 取消自动合并设置 ", "repo.pulls.delete.title": "删除此合并请求?", - "repo.pulls.delete.text": "您真的要删除这个合并请求吗?(这将永久删除所有内容。如果您打算将内容存档,请考虑关闭它)", + "repo.pulls.delete.text": "确定要删除此合并请求吗?(这将永久移除所有内容。如果您只是想保留归档,请关闭它)", "repo.pulls.recently_pushed_new_branches": "您已经于 %[2]s 推送分支 %[1]s", "repo.pulls.upstream_diverging_prompt_behind_1": "该分支落后于 %[2]s %[1]d 个提交", "repo.pulls.upstream_diverging_prompt_behind_n": "该分支落后于 %[2]s %[1]d 个提交", @@ -1923,7 +1933,7 @@ "repo.milestones.edit_subheader": "里程碑组织工单,合并请求和跟踪进度。", "repo.milestones.cancel": "取消", "repo.milestones.modify": "更新里程碑", - "repo.milestones.edit_success": "里程碑「%s」更新成功。", + "repo.milestones.edit_success": "里程碑「%s」已更新。", "repo.milestones.deletion": "删除里程碑", "repo.milestones.deletion_desc": "删除该里程碑将会移除所有工单中相关的信息。是否继续?", "repo.milestones.deletion_success": "里程碑已删除。", @@ -2170,7 +2180,8 @@ "repo.settings.transfer_abort_invalid": "您不能取消不存在的仓库转移。", "repo.settings.transfer_abort_success": "成功取消将仓库转移给 %s。", "repo.settings.transfer_desc": "您可以将仓库转移至您拥有管理员权限的帐户或组织。", - "repo.settings.transfer_form_title": "输入仓库名称以确认:", + "repo.settings.enter_repo_name_to_confirm": "输入仓库名称以确认:", + "repo.settings.enter_repo_full_name_to_confirm": "输入完整的仓库名称(所有者/仓库名)以确认:", "repo.settings.transfer_in_progress": "当前正在进行转移。 如果您想将此仓库转移给另一个用户,请取消它。", "repo.settings.transfer_notices_1": "- 如果将此仓库转移给其他用户,您将失去对此仓库的访问权限。", "repo.settings.transfer_notices_2": "- 如果将其转移到您(共同)拥有的组织,您可以继续访问该仓库。", @@ -2197,7 +2208,7 @@ "repo.settings.wiki_delete_desc": "删除仓库百科数据是永久性的,无法撤消。", "repo.settings.wiki_delete_notices_1": "- 这将永久删除和禁用 %s 的百科。", "repo.settings.confirm_wiki_delete": "删除百科数据", - "repo.settings.wiki_deletion_success": "仓库百科数据删除成功!", + "repo.settings.wiki_deletion_success": "仓库百科数据已删除。", "repo.settings.delete": "删除本仓库", "repo.settings.delete_desc": "删除仓库是永久性的,无法撤消。", "repo.settings.delete_notices_1": "- 此操作 无法 被回滚。", @@ -2246,13 +2257,14 @@ "repo.settings.webhook.delivery.success": "一个事件已添加到推送队列。可能需要过几秒钟才会显示在推送记录中。", "repo.settings.githooks_desc": "Git 钩子是 Git 本身提供的功能。您可以在下方编辑 hook 文件以设置自定义操作。", "repo.settings.githook_edit_desc": "如果钩子未启动,则会显示样例文件中的内容。如果想要删除某个钩子,则提交空白文本即可。", - "repo.settings.githook_name": "钩子名称", - "repo.settings.githook_content": "钩子文本", "repo.settings.update_githook": "更新钩子设置", "repo.settings.add_webhook_desc": "Gitea 将向目标 URL 发送具有指定内容类型的 POST 请求。在 webhooks 指南 中阅读更多内容。", "repo.settings.payload_url": "目标 URL", "repo.settings.http_method": "HTTP 方法", "repo.settings.content_type": "POST 内容类型", + "repo.settings.webhook.name": "Web 钩子名称", + "repo.settings.webhook.name_helper": "可选:为此 Web 钩子设置一个便于识别的名称", + "repo.settings.webhook.name_empty": "未命名的 Web 钩子", "repo.settings.secret": "密钥", "repo.settings.webhook_secret_desc": "如果 Webhook 服务器支持使用密钥,您可以按照 Webhook 的手册在此处填写一个密钥。", "repo.settings.slack_username": "服务名称", @@ -2329,7 +2341,7 @@ "repo.settings.active_helper": "触发事件的信息将发送到此 Web 钩子 URL。", "repo.settings.add_hook_success": "Web 钩子添加成功!", "repo.settings.update_webhook": "更新 Web 钩子", - "repo.settings.update_hook_success": "Web 钩子更新成功!", + "repo.settings.update_hook_success": "Web 钩子已更新。", "repo.settings.delete_webhook": "删除 Web 钩子", "repo.settings.recent_deliveries": "最近推送记录", "repo.settings.hook_type": "钩子类型", @@ -2430,7 +2442,7 @@ "repo.settings.protect_unprotected_file_patterns_desc": "如果用户具有写权限则允许直接更改的不受保护文件,可绕过推送限制。可使用分号(';')分隔多个表达式。见 %[2]s 文档了解表达式语法。例如:.drone.yml、/docs/**/*.txt。", "repo.settings.add_protected_branch": "启用保护", "repo.settings.delete_protected_branch": "禁用保护", - "repo.settings.update_protect_branch_success": "分支保护规则「%s」更新成功。", + "repo.settings.update_protect_branch_success": "分支保护规则「%s」已更新。", "repo.settings.remove_protected_branch_success": "分支保护规则「%s」移除成功。", "repo.settings.remove_protected_branch_failed": "分支保护规则「%s」移除失败。", "repo.settings.protected_branch_deletion": "删除分支保护", @@ -2474,6 +2486,9 @@ "repo.settings.visibility.private.bullet_title": "将可见性改为私有将会:", "repo.settings.visibility.private.bullet_one": "使仓库只对允许的成员可见。", "repo.settings.visibility.private.bullet_two": "可能会删除它与 派生仓库、 关注者和 点赞 之间的关系。", + "repo.settings.visibility.private.stats_stars": "此仓库有 %d 个点赞,这些点赞可能会丢失。", + "repo.settings.visibility.private.stats_watchers": "此仓库有 %d 个关注者,这些关注关系可能会丢失。", + "repo.settings.visibility.private.stats_forks": "此仓库有关联的 %d 个派生仓库。", "repo.settings.visibility.public.button": "设为公开", "repo.settings.visibility.public.text": "将可见性更改为公开会使任何人都可见。", "repo.settings.visibility.public.bullet_title": "将可见性改为公开将会:", @@ -2495,7 +2510,7 @@ "repo.settings.unarchive.text": "撤销归档将恢复仓库接收提交、推送,以及新工单和合并请求的能力。", "repo.settings.unarchive.success": "仓库已成功撤销归档。", "repo.settings.unarchive.error": "仓库在取消归档时出现异常。请通过日志获取详细信息。", - "repo.settings.update_avatar_success": "仓库头像已经更新。", + "repo.settings.update_avatar_success": "仓库头像已更新。", "repo.settings.lfs": "LFS", "repo.settings.lfs_filelist": "存储在此仓库中的 LFS 文件", "repo.settings.lfs_no_lfs_files": "此仓库中没有 LFS 文件", @@ -2619,9 +2634,9 @@ "repo.release.delete_tag": "删除 Git 标签", "repo.release.deletion": "删除发布", "repo.release.deletion_desc": "删除发布只会从 Gitea 中移除发布。这不会影响 Git 的标签以及您仓库的内容和历史。是否继续?", - "repo.release.deletion_success": "该发布已删除。", + "repo.release.deletion_success": "发布已删除。", "repo.release.deletion_tag_desc": "将从仓库中删除此 Git 标签。仓库内容和历史记录保持不变。继续吗?", - "repo.release.deletion_tag_success": "该 Git 标签已删除。", + "repo.release.deletion_tag_success": "Git 标签已删除。", "repo.release.tag_name_already_exist": "使用此 Git 标签名称的发布已经存在。", "repo.release.tag_name_invalid": "Git 标签名称无效。", "repo.release.tag_name_protected": "Git 标签名已受保护。", @@ -2637,7 +2652,7 @@ "repo.release.generate_notes_desc": "自动为此发布添加已合并的合并请求和更新日志链接。", "repo.release.previous_tag": "前一个Git Tag", "repo.release.generate_notes_tag_not_found": "此仓库中不存在名为「%s」的Git标签。", - "repo.release.generate_notes_target_not_found": "无法找到要发布的 Git Tag \"%s\"。", + "repo.release.generate_notes_target_not_found": "未找到要发布的 Git 标签「%s」。", "repo.release.generate_notes_missing_tag": "输入 Git 标签名称以生成发布日志。", "repo.branch.name": "分支名称", "repo.branch.already_exists": "名为「%s」的分支已存在。", @@ -2645,7 +2660,7 @@ "repo.branch.delete": "删除分支「%s」", "repo.branch.delete_html": "删除分支", "repo.branch.delete_desc": "删除分支是永久的。虽然已删除的分支在实际被删除前有可能会短时间存在,但这在大多数情况下无法撤销。是否继续?", - "repo.branch.deletion_success": "分支「%s」删除成功。", + "repo.branch.deletion_success": "分支「%s」已删除。", "repo.branch.deletion_failed": "分支「%s」删除失败。", "repo.branch.delete_branch_has_new_commits": "因为合并之后有新的提交,分支「%s」无法删除。", "repo.branch.create_branch": "创建分支 %s", @@ -2709,6 +2724,8 @@ "org.members": "成员", "org.teams": "团队", "org.code": "代码", + "org.repos.empty": "暂无仓库。", + "org.repos.empty_description": "创建一个仓库与组织共享代码。", "org.lower_members": "名成员", "org.lower_repositories": "个仓库", "org.create_new_team": "新建团队", @@ -2755,7 +2772,7 @@ "org.settings.rename_failed": "由于内部错误,重命名组织失败", "org.settings.rename_notices_1": "此操作 无法 被回滚。", "org.settings.rename_notices_2": "在被人使用前,旧名称将会被重定向。", - "org.settings.update_avatar_success": "组织头像已经更新。", + "org.settings.update_avatar_success": "组织头像已更新。", "org.settings.delete": "删除组织", "org.settings.delete_account": "删除当前组织", "org.settings.delete_prompt": "删除操作会永久清除该组织的信息,并且 无法 恢复!", @@ -2801,7 +2818,10 @@ "org.teams.no_desc": "该团队暂无描述", "org.teams.settings": "团队设置", "org.teams.owners_permission_desc": "管理员团队对 所有仓库 具有操作权限,且对组织具有 管理员权限。", + "org.teams.owners_permission_suggestion": "你可以创建新团队,以便为成员提供更细粒度的访问控制。", "org.teams.members": "团队成员", + "org.teams.manage_team_member": "管理团队和成员", + "org.teams.manage_team_member_prompt": "成员通过团队进行管理。将用户添加到团队,即可邀请其加入此组织。", "org.teams.update_settings": "更新团队设置", "org.teams.delete_team": "删除团队", "org.teams.add_team_member": "添加团队成员", @@ -2809,7 +2829,7 @@ "org.teams.invite_team_member.list": "待处理的邀请", "org.teams.delete_team_title": "删除团队", "org.teams.delete_team_desc": "删除一个团队将删除团队成员的访问权限,继续?", - "org.teams.delete_team_success": "该团队已删除。", + "org.teams.delete_team_success": "团队已删除。", "org.teams.read_permission_desc": "该团队拥有对所属仓库的 读取 权限,团队成员可以进行查看和克隆等只读操作。", "org.teams.write_permission_desc": "该团队拥有对所属仓库的 读取 和 写入 的权限。", "org.teams.admin_permission_desc": "该团队拥有一定的 管理 权限,团队成员可以读取、克隆、推送以及添加其它仓库协作者。", @@ -2842,6 +2862,8 @@ "org.worktime.date_range_end": "结束日期", "org.worktime.query": "查询", "org.worktime.time": "时间", + "org.worktime.empty": "暂无工作时间数据。", + "org.worktime.empty_description": "调整日期范围以查看跟踪时间。", "org.worktime.by_repositories": "按仓库", "org.worktime.by_milestones": "按里程碑", "org.worktime.by_members": "按成员", @@ -2856,6 +2878,30 @@ "admin.hooks": "Web 钩子", "admin.integrations": "集成", "admin.authentication": "认证源", + "admin.badges": "徽章", + "admin.badges.badges_manage_panel": "徽章管理", + "admin.badges.details": "徽章详情", + "admin.badges.new_badge": "创建新徽章", + "admin.badges.slug": "别名", + "admin.badges.slug_been_taken": "别名已使用。", + "admin.badges.description": "描述", + "admin.badges.image_url": "图像 URL", + "admin.badges.new_success": "徽章「%s」创建成功。", + "admin.badges.update_success": "徽章已更新。", + "admin.badges.deletion_success": "徽章已删除。", + "admin.badges.edit_badge": "编辑徽章", + "admin.badges.update_badge": "更新徽章", + "admin.badges.delete_badge": "删除徽章", + "admin.badges.delete_badge_desc": "确定要永久删除此徽章吗?", + "admin.badges.users_with_badge": "拥有此徽章的用户:%s", + "admin.badges.not_found": "未找到徽章。", + "admin.badges.user_already_has": "用户已拥有此徽章。", + "admin.badges.user_add_success": "用户徽章已成功授予。", + "admin.badges.user_remove_success": "用户徽章已成功移除。", + "admin.badges.manage_users": "用户管理", + "admin.badges.add_user": "添加用户", + "admin.badges.remove_user": "移除用户", + "admin.badges.delete_user_desc": "确定要从徽章中删除此用户吗?", "admin.emails": "用户邮箱", "admin.config": "应用配置", "admin.config_summary": "摘要", @@ -3017,7 +3063,7 @@ "admin.emails.filter_sort.name": "用户名", "admin.emails.filter_sort.name_reverse": "用户名(倒序)", "admin.emails.updated": "邮箱已更新", - "admin.emails.not_updated": "无法更新请求的邮箱地址:%v", + "admin.emails.not_updated": "更新请求的邮箱地址失败:%v", "admin.emails.duplicate_active": "此邮箱地址已被另一个用户激活使用。", "admin.emails.change_email_header": "更新邮箱属性", "admin.emails.change_email_text": "您确定要更新该邮箱地址吗?", @@ -3132,6 +3178,8 @@ "admin.auths.oauth2_required_claim_name_helper": "设置此名称,只有具有此名称的声明(Claim)的用户可从此源登录", "admin.auths.oauth2_required_claim_value": "必须填写 Claim 声明的值", "admin.auths.oauth2_required_claim_value_helper": "设置此值,只有拥有对应的声明(Claim)的名称和值的用户才被允许从此源登录", + "admin.auths.open_id_connect_external_id_claim": "外部 ID Claim 声明名称(可选)", + "admin.auths.open_id_connect_external_id_claim_helper": "用于作为用户外部身份的声明名称。默认为「sub」。对于 Azure AD / Entra ID,请将其设置为「oid」,以便在从 Azure AD V2 提供程序迁移时保持连续性。注意:「oid」声明要求在上方「Scopes」字段中包含「profile」范围。", "admin.auths.oauth2_group_claim_name": "用于提供用户组名称的 Claim 声明名称。(可选)", "admin.auths.oauth2_full_name_claim_name": "全名声明名称。(可选,如果设置,用户的全名将始终与此声明同步)", "admin.auths.oauth2_ssh_public_key_claim_name": "SSH 公钥声明名称", @@ -3170,13 +3218,13 @@ "admin.auths.edit": "修改认证源", "admin.auths.activated": "该认证源已经启用", "admin.auths.new_success": "已添加身份验证「%s」。", - "admin.auths.update_success": "认证源已经更新。", + "admin.auths.update_success": "认证源已更新。", "admin.auths.update": "更新认证源", "admin.auths.delete": "删除认证源", "admin.auths.delete_auth_title": "删除身份验证源", "admin.auths.delete_auth_desc": "删除一个认证源将阻止使用它进行登录。确认?", "admin.auths.still_in_used": "认证源仍在使用。请先解除或者删除使用此认证源的用户。", - "admin.auths.deletion_success": "认证源已经更新。", + "admin.auths.deletion_success": "认证源已删除。", "admin.auths.login_source_exist": "认证源「%s」已经存在。", "admin.auths.login_source_of_type_exist": "此类型的认证源已存在。", "admin.auths.unable_to_initialize_openid": "无法初始化 OpenID Connect 提供商:%s", @@ -3184,10 +3232,8 @@ "admin.config.server_config": "服务器配置", "admin.config.app_name": "站点名称", "admin.config.app_ver": "Gitea 版本", - "admin.config.app_url": "Gitea 基本 URL", "admin.config.custom_conf": "配置文件路径", "admin.config.custom_file_root_path": "自定义文件根路径", - "admin.config.domain": "服务器域名", "admin.config.disable_router_log": "关闭路由日志", "admin.config.run_user": "以用户名运行", "admin.config.run_mode": "运行模式", @@ -3468,6 +3514,7 @@ "packages.dependencies": "依赖", "packages.keywords": "关键词", "packages.details": "详情", + "packages.name": "软件包名称", "packages.details.author": "作者", "packages.details.project_site": "项目站点", "packages.details.repository_site": "仓库站点", @@ -3563,6 +3610,18 @@ "packages.swift.registry": "从命令行设置此仓库:", "packages.swift.install": "在您的 Package.swift 文件中添加该包:", "packages.swift.install2": "并运行以下命令:", + "packages.terraform.install": "将您的 state 配置为使用 HTTP 后端", + "packages.terraform.install2": "并运行以下命令:", + "packages.terraform.lock_status": "锁定状态", + "packages.terraform.locked_by": "已被 %s 锁定", + "packages.terraform.unlocked": "已解锁", + "packages.terraform.lock": "锁定", + "packages.terraform.unlock": "解锁​​​​", + "packages.terraform.lock.success": "Terraform 状态已成功锁定。", + "packages.terraform.unlock.success": "Terraform 状态已成功解锁。", + "packages.terraform.lock.error.already_locked": "Terraform 状态已被锁定。", + "packages.terraform.delete.locked": "Terraform 状态被锁定,无法删除。", + "packages.terraform.delete.latest": "无法删除最新版本的 Terraform 状态。", "packages.vagrant.install": "若要添加一个 Vagrant box,请运行以下命令:", "packages.settings.link": "将此软件包链接到仓库", "packages.settings.link.description": "如果您将一个软件包与一个仓库链接起来,软件包将显示在仓库的软件包列表中。", @@ -3570,14 +3629,19 @@ "packages.settings.link.button": "更新仓库链接", "packages.settings.link.success": "仓库链接已成功更新。", "packages.settings.link.error": "更新仓库链接失败。", - "packages.settings.link.repo_not_found": "仓库 %s 未找到。", + "packages.settings.link.repo_not_found": "未找到仓库 %s。", "packages.settings.unlink.error": "删除仓库链接失败。", "packages.settings.unlink.success": "仓库链接已成功删除。", "packages.settings.delete": "删除软件包", "packages.settings.delete.description": "删除软件包是永久性的,无法撤消。", "packages.settings.delete.notice": "您将要删除 %s (%s)。此操作是不可逆的,您确定吗?", + "packages.settings.delete.notice.package": "即将删除 %s 及其所有版本。此操作不可撤销,确定要继续吗?", "packages.settings.delete.success": "软件包已删除。", + "packages.settings.delete.version.success": "软件包已删除。", "packages.settings.delete.error": "删除软件包失败。", + "packages.settings.delete.version": "删除版本", + "packages.settings.delete.confirm": "输入软件包名称以确认", + "packages.settings.delete.invalid_package_name": "输入的软件包名称不正确。", "packages.owner.settings.cargo.title": "Cargo 注册中心索引", "packages.owner.settings.cargo.initialize": "初始化索引", "packages.owner.settings.cargo.initialize.description": "使用 Cargo 注册中心时需要一个特殊索引的 Git 仓库。使用此选项将(重新)创建仓库并自动配置它。", @@ -3585,7 +3649,7 @@ "packages.owner.settings.cargo.initialize.success": "Cargo 索引已经成功创建。", "packages.owner.settings.cargo.rebuild": "重建索引", "packages.owner.settings.cargo.rebuild.description": "如果索引与存储的 Cargo 包不同步,重建可能会有用。", - "packages.owner.settings.cargo.rebuild.error": "无法重建 Cargo 索引: %v", + "packages.owner.settings.cargo.rebuild.error": "重建 Cargo 索引失败:%v", "packages.owner.settings.cargo.rebuild.success": "Cargo 索引已成功重建。", "packages.owner.settings.cleanuprules.title": "管理清理规则", "packages.owner.settings.cleanuprules.add": "添加清理规则", @@ -3644,6 +3708,7 @@ "actions.runners.id": "ID", "actions.runners.name": "名称", "actions.runners.owner_type": "类型", + "actions.runners.availability": "可用性", "actions.runners.description": "描述", "actions.runners.labels": "标签", "actions.runners.last_online": "上次在线时间", @@ -3659,6 +3724,12 @@ "actions.runners.update_runner": "更新更改", "actions.runners.update_runner_success": "运行器更新成功", "actions.runners.update_runner_failed": "运行器更新失败", + "actions.runners.enable_runner": "启用此运行器", + "actions.runners.enable_runner_success": "运行器已成功启用", + "actions.runners.enable_runner_failed": "启用运行器失败", + "actions.runners.disable_runner": "禁用此运行器", + "actions.runners.disable_runner_success": "成功禁用运行器", + "actions.runners.disable_runner_failed": "禁用运行器失败", "actions.runners.delete_runner": "删除此运行器", "actions.runners.delete_runner_success": "运行器删除成功", "actions.runners.delete_runner_failed": "运行器删除失败", @@ -3677,6 +3748,8 @@ "actions.runs.workflow_run_count_1": "%d 次工作流运行", "actions.runs.workflow_run_count_n": "%d 次工作流运行", "actions.runs.commit": "提交", + "actions.runs.run_details": "运行详情", + "actions.runs.workflow_file": "工作流文件", "actions.runs.scheduled": "已计划的", "actions.runs.pushed_by": "推送者", "actions.runs.invalid_workflow_helper": "工作流配置文件无效。请检查您的配置文件:%s", @@ -3696,23 +3769,29 @@ "actions.runs.expire_log_message": "旧的日志已清除。", "actions.runs.delete": "删除工作流运行", "actions.runs.cancel": "取消工作流运行", - "actions.runs.delete.description": "您确定要永久删除此工作流运行吗?此操作无法撤消。", + "actions.runs.delete.description": "确定要永久删除此工作流运行吗?此操作无法撤消。", "actions.runs.not_done": "此工作流运行尚未完成。", "actions.runs.view_workflow_file": "查看工作流文件", - "actions.runs.workflow_graph": "工作流程图", + "actions.runs.summary": "摘要", + "actions.runs.all_jobs": "所有任务", + "actions.runs.attempt": "尝试", + "actions.runs.latest": "最新", + "actions.runs.latest_attempt": "最新尝试", + "actions.runs.triggered_via": "通过 %s 触发", + "actions.runs.total_duration": "总耗时:", "actions.workflow.disable": "禁用工作流", "actions.workflow.disable_success": "工作流「%s」已成功禁用。", "actions.workflow.enable": "启用工作流", "actions.workflow.enable_success": "工作流「%s」已成功启用。", "actions.workflow.disabled": "工作流已禁用。", "actions.workflow.run": "运行工作流", - "actions.workflow.not_found": "工作流「%s」未找到。", + "actions.workflow.not_found": "未找到工作流「%s」。", "actions.workflow.run_success": "工作流「%s」已成功运行。", "actions.workflow.from_ref": "使用工作流从", "actions.workflow.has_workflow_dispatch": "此工作流有一个 workflow_dispatch 事件触发器。", "actions.workflow.has_no_workflow_dispatch": "工作流「%s」没有 workflow_dispatch 事件触发器。", "actions.need_approval_desc": "该工作流由派生仓库的合并请求所触发,需要批准方可运行。", - "actions.approve_all_success": "已成功批准所有工作流运行。", + "actions.approve_all_success": "所有工作流运行已成功批准。", "actions.variables": "变量", "actions.variables.management": "变量管理", "actions.variables.creation": "添加变量", @@ -3749,5 +3828,24 @@ "git.filemode.normal_file": "普通文件", "git.filemode.executable_file": "可执行文件", "git.filemode.symbolic_link": "符号链接", - "git.filemode.submodule": "子模块" + "git.filemode.submodule": "子模块", + "org.repos.none": "没有仓库。", + "actions.general.permissions": "工作流令牌权限", + "actions.general.token_permissions.mode": "默认令牌权限", + "actions.general.token_permissions.mode.desc": "如果工作流任务未在工作流文件中声明其权限,则会使用默认权限。", + "actions.general.token_permissions.mode.permissive": "宽松", + "actions.general.token_permissions.mode.permissive.desc": "任务所属仓库的读写权限。", + "actions.general.token_permissions.mode.restricted": "受限", + "actions.general.token_permissions.mode.restricted.desc": "任务所属仓库的内容单元(代码、发布)的只读权限。", + "actions.general.token_permissions.override_owner": "覆盖所有者级别的配置", + "actions.general.token_permissions.override_owner_desc": "如果启用,此仓库将使用其自身的工作流配置,而不是遵循所有者级别(用户或组织)的配置。", + "actions.general.token_permissions.maximum": "最大令牌权限", + "actions.general.token_permissions.maximum.description": "工作流任务的实际权限将受到最大权限的限制。", + "actions.general.token_permissions.fork_pr_note": "如果任务是由来自派生仓库的合并请求触发的,那么它的实际权限不会超过只读权限。", + "actions.general.token_permissions.customize_max_permissions": "自定义最大权限", + "actions.general.cross_repo": "跨仓库访问", + "actions.general.cross_repo_desc": "允许此所有者下的所有仓库在运行工作流任务时,通过 GITEA_TOKEN 以只读方式访问所选仓库。", + "actions.general.cross_repo_selected": "选择的仓库", + "actions.general.cross_repo_target_repos": "目标仓库", + "actions.general.cross_repo_add": "添加目标仓库" } diff --git a/package.json b/package.json index b090b320980..04f27fec82d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@codemirror/lint": "6.9.5", "@codemirror/search": "6.6.0", "@codemirror/state": "6.6.0", - "@codemirror/view": "6.41.0", + "@codemirror/view": "6.41.1", "@deltablot/dropzone": "7.4.3", "@github/markdown-toolbar-element": "2.2.3", "@github/paste-markdown": "1.5.3", @@ -28,20 +28,19 @@ "@lezer/highlight": "1.2.3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mermaid-js/layout-elk": "0.2.1", - "@primer/octicons": "19.24.0", + "@primer/octicons": "19.24.1", "@replit/codemirror-indentation-markers": "6.5.3", "@replit/codemirror-lang-nix": "6.0.1", "@replit/codemirror-lang-svelte": "6.0.0", "@replit/codemirror-vscode-keymap": "6.0.2", "@resvg/resvg-wasm": "2.6.2", - "@silverwind/vue3-calendar-heatmap": "2.1.1", "@vitejs/plugin-vue": "6.0.6", "ansi_up": "6.0.6", "asciinema-player": "3.15.1", "chart.js": "4.5.1", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", - "clippie": "4.1.10", + "clippie": "4.1.14", "codemirror-lang-elixir": "4.0.1", "colord": "2.9.3", "compare-versions": "6.1.1", @@ -57,10 +56,10 @@ "online-3d-viewer": "0.18.0", "pdfobject": "2.3.1", "perfect-debounce": "2.1.0", - "postcss": "8.5.9", - "rolldown-license-plugin": "2.2.5", + "postcss": "8.5.10", + "rolldown-license-plugin": "3.0.1", "sortablejs": "1.15.7", - "swagger-ui-dist": "5.32.2", + "swagger-ui-dist": "5.32.4", "tailwindcss": "3.4.19", "throttle-debounce": "5.0.2", "tippy.js": "6.3.7", @@ -68,7 +67,7 @@ "tributejs": "5.1.3", "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", - "vite": "8.0.8", + "vite": "8.0.9", "vite-string-plugin": "2.0.2", "vue": "3.5.32", "vue-bar-graph": "2.2.0", @@ -90,41 +89,41 @@ "@types/swagger-ui-dist": "3.30.6", "@types/throttle-debounce": "5.0.2", "@types/toastify-js": "1.12.4", - "@typescript-eslint/parser": "8.58.2", + "@typescript-eslint/parser": "8.59.0", "@vitejs/plugin-vue": "6.0.6", - "@vitest/eslint-plugin": "1.6.15", - "eslint": "10.2.0", + "@vitest/eslint-plugin": "1.6.16", + "eslint": "10.2.1", "eslint-import-resolver-typescript": "4.4.4", "eslint-plugin-array-func": "5.1.1", "eslint-plugin-de-morgan": "2.1.1", "eslint-plugin-github": "6.0.0", "eslint-plugin-import-x": "4.16.2", - "eslint-plugin-playwright": "2.10.1", + "eslint-plugin-playwright": "2.10.2", "eslint-plugin-regexp": "3.1.0", - "eslint-plugin-sonarjs": "4.0.2", + "eslint-plugin-sonarjs": "4.0.3", "eslint-plugin-unicorn": "64.0.0", "eslint-plugin-vue": "10.8.0", "eslint-plugin-vue-scoped-css": "3.0.0", "eslint-plugin-wc": "3.1.0", "globals": "17.5.0", - "happy-dom": "20.8.9", + "happy-dom": "20.9.0", "jiti": "2.6.1", "markdownlint-cli": "0.48.0", "material-icon-theme": "5.33.1", "nolyfill": "1.0.44", "postcss-html": "1.8.1", "spectral-cli-bundle": "1.0.7", - "stylelint": "17.7.0", + "stylelint": "17.8.0", "stylelint-config-recommended": "18.0.0", "stylelint-declaration-block-no-ignored-properties": "3.0.0", "stylelint-declaration-strict-value": "1.11.1", "stylelint-value-no-unknown-custom-properties": "6.1.1", "svgo": "4.0.1", - "typescript": "6.0.2", - "typescript-eslint": "8.58.2", - "updates": "17.15.3", + "typescript": "6.0.3", + "typescript-eslint": "8.59.0", + "updates": "17.16.3", "vitest": "4.1.4", - "vue-tsc": "3.2.6" + "vue-tsc": "3.2.7" }, "pnpm": { "peerDependencyRules": { diff --git a/playwright.config.ts b/playwright.config.ts index 8fdd777ee47..9dc8a7c1b5c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -36,11 +36,5 @@ export default defineConfig({ ...devices['Desktop Firefox'], }, }, - { - name: 'webkit', - use: { - ...devices['Desktop Safari'], - }, - }, ], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a05156011e8..cca57d3b195 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,8 +71,8 @@ importers: specifier: 6.6.0 version: 6.6.0 '@codemirror/view': - specifier: 6.41.0 - version: 6.41.0 + specifier: 6.41.1 + version: 6.41.1 '@deltablot/dropzone': specifier: 7.4.3 version: 7.4.3 @@ -95,29 +95,26 @@ importers: specifier: 0.2.1 version: 0.2.1(mermaid@11.14.0) '@primer/octicons': - specifier: 19.24.0 - version: 19.24.0 + specifier: 19.24.1 + version: 19.24.1 '@replit/codemirror-indentation-markers': specifier: 6.5.3 - version: 6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.0) + version: 6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.1) '@replit/codemirror-lang-nix': specifier: 6.0.1 - version: 6.0.1(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.8) + version: 6.0.1(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.1)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10) '@replit/codemirror-lang-svelte': specifier: 6.0.0 - version: 6.0.0(@codemirror/autocomplete@6.20.1)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.8) + version: 6.0.0(@codemirror/autocomplete@6.20.1)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.1)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.10) '@replit/codemirror-vscode-keymap': specifier: 6.0.2 - version: 6.0.2(@codemirror/autocomplete@6.20.1)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/view@6.41.0) + version: 6.0.2(@codemirror/autocomplete@6.20.1)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/view@6.41.1) '@resvg/resvg-wasm': specifier: 2.6.2 version: 2.6.2 - '@silverwind/vue3-calendar-heatmap': - specifier: 2.1.1 - version: 2.1.1(tippy.js@6.3.7)(vue@3.5.32(typescript@6.0.2)) '@vitejs/plugin-vue': specifier: 6.0.6 - version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1))(vue@3.5.32(typescript@6.0.2)) + version: 6.0.6(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1))(vue@3.5.32(typescript@6.0.3)) ansi_up: specifier: 6.0.6 version: 6.0.6 @@ -134,8 +131,8 @@ importers: specifier: 2.2.0 version: 2.2.0(chart.js@4.5.1) clippie: - specifier: 4.1.10 - version: 4.1.10 + specifier: 4.1.14 + version: 4.1.14 codemirror-lang-elixir: specifier: 4.0.1 version: 4.0.1 @@ -182,17 +179,17 @@ importers: specifier: 2.1.0 version: 2.1.0 postcss: - specifier: 8.5.9 - version: 8.5.9 + specifier: 8.5.10 + version: 8.5.10 rolldown-license-plugin: - specifier: 2.2.5 - version: 2.2.5(rolldown@1.0.0-rc.15) + specifier: 3.0.1 + version: 3.0.1(rolldown@1.0.0-rc.16) sortablejs: specifier: 1.15.7 version: 1.15.7 swagger-ui-dist: - specifier: 5.32.2 - version: 5.32.2 + specifier: 5.32.4 + version: 5.32.4 tailwindcss: specifier: 3.4.19 version: 3.4.19 @@ -215,24 +212,24 @@ importers: specifier: 0.7.2 version: 0.7.2 vite: - specifier: 8.0.8 - version: 8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) + specifier: 8.0.9 + version: 8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) vite-string-plugin: specifier: 2.0.2 - version: 2.0.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + version: 2.0.2(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) vue: specifier: 3.5.32 - version: 3.5.32(typescript@6.0.2) + version: 3.5.32(typescript@6.0.3) vue-bar-graph: specifier: 2.2.0 - version: 2.2.0(typescript@6.0.2) + version: 2.2.0(typescript@6.0.3) vue-chartjs: specifier: 5.3.3 - version: 5.3.3(chart.js@4.5.1)(vue@3.5.32(typescript@6.0.2)) + version: 5.3.3(chart.js@4.5.1)(vue@3.5.32(typescript@6.0.3)) devDependencies: '@eslint-community/eslint-plugin-eslint-comments': specifier: 4.7.1 - version: 4.7.1(eslint@10.2.0(jiti@2.6.1)) + version: 4.7.1(eslint@10.2.1(jiti@2.6.1)) '@eslint/json': specifier: 1.2.0 version: 1.2.0 @@ -241,10 +238,10 @@ importers: version: 1.59.1 '@stylistic/eslint-plugin': specifier: 5.10.0 - version: 5.10.0(eslint@10.2.0(jiti@2.6.1)) + version: 5.10.0(eslint@10.2.1(jiti@2.6.1)) '@stylistic/stylelint-plugin': specifier: 5.1.0 - version: 5.1.0(stylelint@17.7.0(typescript@6.0.2)) + version: 5.1.0(stylelint@17.8.0(typescript@6.0.3)) '@types/codemirror': specifier: 5.60.17 version: 5.60.17 @@ -276,56 +273,56 @@ importers: specifier: 1.12.4 version: 1.12.4 '@typescript-eslint/parser': - specifier: 8.58.2 - version: 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + specifier: 8.59.0 + version: 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) '@vitest/eslint-plugin': - specifier: 1.6.15 - version: 1.6.15(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)(vitest@4.1.4(@types/node@25.6.0)(happy-dom@20.8.9)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1))) + specifier: 1.6.16 + version: 1.6.16(@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)(vitest@4.1.4(@types/node@25.6.0)(happy-dom@20.9.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1))) eslint: - specifier: 10.2.0 - version: 10.2.0(jiti@2.6.1) + specifier: 10.2.1 + version: 10.2.1(jiti@2.6.1) eslint-import-resolver-typescript: specifier: 4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint@10.2.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.2.0(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.2.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.2.1(jiti@2.6.1)) eslint-plugin-array-func: specifier: 5.1.1 - version: 5.1.1(eslint@10.2.0(jiti@2.6.1)) + version: 5.1.1(eslint@10.2.1(jiti@2.6.1)) eslint-plugin-de-morgan: specifier: 2.1.1 - version: 2.1.1(eslint@10.2.0(jiti@2.6.1)) + version: 2.1.1(eslint@10.2.1(jiti@2.6.1)) eslint-plugin-github: specifier: 6.0.0 - version: 6.0.0(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.0(jiti@2.6.1)) + version: 6.0.0(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.1(jiti@2.6.1)) eslint-plugin-import-x: specifier: 4.16.2 - version: 4.16.2(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint@10.2.0(jiti@2.6.1)) + version: 4.16.2(@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.2.1(jiti@2.6.1)) eslint-plugin-playwright: - specifier: 2.10.1 - version: 2.10.1(eslint@10.2.0(jiti@2.6.1)) + specifier: 2.10.2 + version: 2.10.2(eslint@10.2.1(jiti@2.6.1)) eslint-plugin-regexp: specifier: 3.1.0 - version: 3.1.0(eslint@10.2.0(jiti@2.6.1)) + version: 3.1.0(eslint@10.2.1(jiti@2.6.1)) eslint-plugin-sonarjs: - specifier: 4.0.2 - version: 4.0.2(eslint@10.2.0(jiti@2.6.1)) + specifier: 4.0.3 + version: 4.0.3(eslint@10.2.1(jiti@2.6.1)) eslint-plugin-unicorn: specifier: 64.0.0 - version: 64.0.0(eslint@10.2.0(jiti@2.6.1)) + version: 64.0.0(eslint@10.2.1(jiti@2.6.1)) eslint-plugin-vue: specifier: 10.8.0 - version: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.0(jiti@2.6.1)))(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.0(jiti@2.6.1))) + version: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1)))(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1))) eslint-plugin-vue-scoped-css: specifier: 3.0.0 - version: 3.0.0(eslint@10.2.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.0(jiti@2.6.1))) + version: 3.0.0(eslint@10.2.1(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1))) eslint-plugin-wc: specifier: 3.1.0 - version: 3.1.0(eslint@10.2.0(jiti@2.6.1)) + version: 3.1.0(eslint@10.2.1(jiti@2.6.1)) globals: specifier: 17.5.0 version: 17.5.0 happy-dom: - specifier: 20.8.9 - version: 20.8.9 + specifier: 20.9.0 + version: 20.9.0 jiti: specifier: 2.6.1 version: 2.6.1 @@ -345,38 +342,38 @@ importers: specifier: 1.0.7 version: 1.0.7 stylelint: - specifier: 17.7.0 - version: 17.7.0(typescript@6.0.2) + specifier: 17.8.0 + version: 17.8.0(typescript@6.0.3) stylelint-config-recommended: specifier: 18.0.0 - version: 18.0.0(stylelint@17.7.0(typescript@6.0.2)) + version: 18.0.0(stylelint@17.8.0(typescript@6.0.3)) stylelint-declaration-block-no-ignored-properties: specifier: 3.0.0 - version: 3.0.0(stylelint@17.7.0(typescript@6.0.2)) + version: 3.0.0(stylelint@17.8.0(typescript@6.0.3)) stylelint-declaration-strict-value: specifier: 1.11.1 - version: 1.11.1(stylelint@17.7.0(typescript@6.0.2)) + version: 1.11.1(stylelint@17.8.0(typescript@6.0.3)) stylelint-value-no-unknown-custom-properties: specifier: 6.1.1 - version: 6.1.1(stylelint@17.7.0(typescript@6.0.2)) + version: 6.1.1(stylelint@17.8.0(typescript@6.0.3)) svgo: specifier: 4.0.1 version: 4.0.1 typescript: - specifier: 6.0.2 - version: 6.0.2 + specifier: 6.0.3 + version: 6.0.3 typescript-eslint: - specifier: 8.58.2 - version: 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + specifier: 8.59.0 + version: 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) updates: - specifier: 17.15.3 - version: 17.15.3 + specifier: 17.16.3 + version: 17.16.3 vitest: specifier: 4.1.4 - version: 4.1.4(@types/node@25.6.0)(happy-dom@20.8.9)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + version: 4.1.4(@types/node@25.6.0)(happy-dom@20.9.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) vue-tsc: - specifier: 3.2.6 - version: 3.2.6(typescript@6.0.2) + specifier: 3.2.7 + version: 3.2.7(typescript@6.0.3) packages: @@ -511,8 +508,8 @@ packages: '@codemirror/lang-javascript@6.2.5': resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} - '@codemirror/lang-jinja@6.0.0': - resolution: {integrity: sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw==} + '@codemirror/lang-jinja@6.0.1': + resolution: {integrity: sha512-P5kyHLObzjtbGj16h+hyvZTxJhSjBEeSx4wMjbnAf3b0uwTy2+F0zGjMZL4PQOm/mh2eGZ5xUDVZXgwP783Nsw==} '@codemirror/lang-json@6.0.2': resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} @@ -571,8 +568,8 @@ packages: '@codemirror/state@6.6.0': resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} - '@codemirror/view@6.41.0': - resolution: {integrity: sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA==} + '@codemirror/view@6.41.1': + resolution: {integrity: sha512-ToDnWKbBnke+ZLrP6vgTTDScGi5H37YYuZGniQaBzxMVdtCxMrslsmtnOvbPZk4RX9bvkQqnWR/WS/35tJA0qg==} '@csstools/css-calc@3.2.0': resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} @@ -621,9 +618,15 @@ packages: '@deltablot/dropzone@7.4.3': resolution: {integrity: sha512-qTj4KEalPcYrocazuKYfhAS3hfgAu17KXw0DkpUj71Aw9E1/x5vTSNK5hvIJLkbpt/pBECU18JkjA3yguONcPA==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} @@ -866,12 +869,16 @@ packages: '@github/text-expander-element@2.9.4': resolution: {integrity: sha512-+zxSlek2r0NrbFmRfymVtYhES9YU033acc/mouXUkN2bs8DaYScPucvBhwg/5d0hsEb2rIykKnkA/2xxWSqCTw==} - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': @@ -944,8 +951,8 @@ packages: '@lezer/json@1.0.3': resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} - '@lezer/lr@1.4.8': - resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==} + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} '@lezer/markdown@1.6.3': resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==} @@ -988,8 +995,8 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@1.1.3': - resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -1073,8 +1080,8 @@ packages: resolution: {integrity: sha512-3dsKlf4Ma7o+uxLIg5OI1Tgwfet2pE8WTbPjEGWvOe6CSjMtK0skJnnSVHaEVX4N4mYU81To0qDeZOPqjaUotg==} engines: {node: '>=12.4.0'} - '@oxc-project/types@0.124.0': - resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@oxc-project/types@0.126.0': + resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==} '@package-json/types@0.0.12': resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} @@ -1091,8 +1098,8 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@primer/octicons@19.24.0': - resolution: {integrity: sha512-OS+8ip2nqrdd6nv6bXROgTFm/nsmZJQ/ExqaYnq80h5Zlz3+gxEtKQl2Zi/AshnhHztF7ErPe//Osbj6ZfwQLQ==} + '@primer/octicons@19.24.1': + resolution: {integrity: sha512-vgtSHq8IIf3oo/HjPGj0B7NkeatSyyw5mupMjXByPI1gY6uRZ/UQdv7uSJnSOCJYQJF3lVDUwiwp9wM71MYIgA==} '@replit/codemirror-indentation-markers@6.5.3': resolution: {integrity: sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==} @@ -1142,97 +1149,97 @@ packages: resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} engines: {node: '>= 10'} - '@rolldown/binding-android-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + '@rolldown/binding-android-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.15': - resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + resolution: {integrity: sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': - resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + resolution: {integrity: sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': - resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + resolution: {integrity: sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': - resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} - engines: {node: '>=14.0.0'} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + resolution: {integrity: sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1240,8 +1247,8 @@ packages: '@rolldown/pluginutils@1.0.0-rc.13': resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==} - '@rolldown/pluginutils@1.0.0-rc.15': - resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + '@rolldown/pluginutils@1.0.0-rc.16': + resolution: {integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==} '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1249,13 +1256,6 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} - '@silverwind/vue3-calendar-heatmap@2.1.1': - resolution: {integrity: sha512-RQtLOpkysm0LR3PbUoc+aDcYxzy7xboygb1SQEwrUm2/XB2nmt0BEra2ADXpu4kwFxtk0+IyNwzFvbBai/wvTg==} - engines: {node: '>=16'} - peerDependencies: - tippy.js: ^6.3.7 - vue: ^3.2.29 - '@simonwep/pickr@1.9.0': resolution: {integrity: sha512-oEYvv15PyfZzjoAzvXYt3UyNGwzsrpFxLaZKzkOSd0WYBVwLd19iJerePDONxC1iF6+DpcswPdLIM2KzCJuYFg==} @@ -1470,63 +1470,63 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.58.2': - resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==} + '@typescript-eslint/eslint-plugin@8.59.0': + resolution: {integrity: sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.58.2 + '@typescript-eslint/parser': ^8.59.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.58.2': - resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==} + '@typescript-eslint/parser@8.59.0': + resolution: {integrity: sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.58.2': - resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==} + '@typescript-eslint/project-service@8.59.0': + resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.58.2': - resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} + '@typescript-eslint/scope-manager@8.59.0': + resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.58.2': - resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} + '@typescript-eslint/tsconfig-utils@8.59.0': + resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.58.2': - resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} + '@typescript-eslint/type-utils@8.59.0': + resolution: {integrity: sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.58.2': - resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} + '@typescript-eslint/types@8.59.0': + resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.58.2': - resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} + '@typescript-eslint/typescript-estree@8.59.0': + resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.58.2': - resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} + '@typescript-eslint/utils@8.59.0': + resolution: {integrity: sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.58.2': - resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} + '@typescript-eslint/visitor-keys@8.59.0': + resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -1642,8 +1642,8 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 - '@vitest/eslint-plugin@1.6.15': - resolution: {integrity: sha512-dTMjrdngmcB+DxomlKQ+SUubCTvd0m2hQQFpv5sx+GRodmeoxr2PVbphk57SVp250vpxphk9Ccwyv6fQ6+2gkA==} + '@vitest/eslint-plugin@1.6.16': + resolution: {integrity: sha512-2pBN1F1JXq6zTSaYC58CMJa7pGxXIRsLfOioeZM4cPE3pRdSh1ySTSoHPQlOTEF5WgoVzWZQxhGQ3ygT78hOVg==} engines: {node: '>=18'} peerDependencies: '@typescript-eslint/eslint-plugin': '*' @@ -1708,8 +1708,8 @@ packages: '@vue/compiler-ssr@3.5.32': resolution: {integrity: sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==} - '@vue/language-core@3.2.6': - resolution: {integrity: sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==} + '@vue/language-core@3.2.7': + resolution: {integrity: sha512-Gn4q/tRxbpVGLEuARQ43p3YELlNAFgRUVCgW9U5Cr+5q4vfD2bWDWpl3ABbJMXUt5xlE1dF8dkigg2aUq7JYYw==} '@vue/reactivity@3.5.32': resolution: {integrity: sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==} @@ -1811,8 +1811,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.18: - resolution: {integrity: sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==} + baseline-browser-mapping@2.10.20: + resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -1865,8 +1865,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001787: - resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} + caniuse-lite@1.0.30001788: + resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} @@ -1927,8 +1927,8 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} - clippie@4.1.10: - resolution: {integrity: sha512-zUjK2fLH8/wju2lks5mH0u8wSRYCOJoHfT1KQ61+aCT5O1ouONnSrnKQ3BTKvIYLUYJarbLZo4FLHyce/SLF2g==} + clippie@4.1.14: + resolution: {integrity: sha512-VMyejKiX9jtz57BP2YLZeH+662xKTlIAXBoaHVXdtuuDiSd2D1z/0GyclLGX4POXKK03/vYB6cHVWlSLDI2YBw==} codemirror-lang-elixir@4.0.1: resolution: {integrity: sha512-z6W/XB4b7TZrp9EZYBGVq93vQfvKbff+1iM8YZaVErL0dguBAeLmVRlEv1NuDZHOP1qjJ3NwyibkUkNWn7q9VQ==} @@ -2277,8 +2277,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.3.3: - resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} + dompurify@3.4.0: + resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2286,8 +2286,8 @@ packages: easymde@2.20.0: resolution: {integrity: sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==} - electron-to-chromium@1.5.335: - resolution: {integrity: sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==} + electron-to-chromium@1.5.340: + resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==} elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} @@ -2461,8 +2461,8 @@ packages: resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==} engines: {node: '>=5.0.0'} - eslint-plugin-playwright@2.10.1: - resolution: {integrity: sha512-qea3UxBOb8fTwJ77FMApZKvRye5DOluDHcev0LDJwID3RELeun0JlqzrNIXAB/SXCyB/AesCW/6sZfcT9q3Edg==} + eslint-plugin-playwright@2.10.2: + resolution: {integrity: sha512-0N+2OWc3NZbOZ0gK8mp2TK6Qu3UWcJTQ9rqU0UM2yRJXgT758pvpY0lsOLIySfbyFrLqn3TcXjixbmcK90VnuQ==} engines: {node: '>=16.9.0'} peerDependencies: eslint: '>=8.40.0' @@ -2487,8 +2487,8 @@ packages: peerDependencies: eslint: '>=9.38.0' - eslint-plugin-sonarjs@4.0.2: - resolution: {integrity: sha512-BTcT1zr1iTbmJtVlcesISwnXzh+9uhf9LEOr+RRNf4kR8xA0HQTPft4oiyOCzCOGKkpSJxjR8ZYF6H7VPyplyw==} + eslint-plugin-sonarjs@4.0.3: + resolution: {integrity: sha512-5drkJKLC9qQddIiaATV0e8+ygbUc7b0Ti6VB7M2d3jmKNh3X0RaiIJYTs3dr9xnlhlrxo+/s1FoO3Jgv6O/c7g==} peerDependencies: eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -2551,8 +2551,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.2.0: - resolution: {integrity: sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==} + eslint@10.2.1: + resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2689,8 +2689,8 @@ packages: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} - get-tsconfig@4.13.7: - resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2734,8 +2734,8 @@ packages: resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} engines: {node: '>=0.8.0'} - happy-dom@20.8.9: - resolution: {integrity: sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==} + happy-dom@20.9.0: + resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} engines: {node: '>=20.0.0'} has-flag@5.0.1: @@ -3473,8 +3473,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.9: - resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -3485,8 +3485,8 @@ packages: resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier@3.8.2: - resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==} + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} hasBin: true @@ -3560,13 +3560,13 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown-license-plugin@2.2.5: - resolution: {integrity: sha512-z1Vjlyx5Q1Hsq1IjBJlt2Vj8/Uy9fqPN0NEwjJ6QHHuDKgHasv+5Pnuo0M0zSoIkCyxeRd+/9frYutccJa+jLQ==} + rolldown-license-plugin@3.0.1: + resolution: {integrity: sha512-0aBIHnHN2xiEXSmTMHFLWYCm3WBExqSbq0y9nr9LKg0JBI8wBy7Y+Nm4oL5JJHgcI4JeRniERVatHqUg2QtMzQ==} peerDependencies: rolldown: '*' - rolldown@1.0.0-rc.15: - resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + rolldown@1.0.0-rc.16: + resolution: {integrity: sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3665,8 +3665,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -3730,13 +3730,13 @@ packages: peerDependencies: stylelint: '>=16' - stylelint@17.7.0: - resolution: {integrity: sha512-n/+4RheCRl+cecGnF+S/Adz59iCRaK9BVznJYB+a7GOksfwNzjiOPnYv17pTO0HgRse9IiqbMtekGNhOb2tVYQ==} + stylelint@17.8.0: + resolution: {integrity: sha512-oHkld9T60LDSaUQ4CSVc+tlt9eUoDlxhaGWShsUCKyIL14boZfmK5bSphZqx64aiC5tCqX+BsQMTMoSz8D1zIg==} engines: {node: '>=20.19.0'} hasBin: true - stylis@4.3.6: - resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} @@ -3769,8 +3769,8 @@ packages: svgson@5.3.1: resolution: {integrity: sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==} - swagger-ui-dist@5.32.2: - resolution: {integrity: sha512-t6Ns52nS8LU2hqi0+rezMjFO1ZrCsCrnommXrU7Nfrg2va2dWahdvM6TuSwzdHpG29v6BHJyU1c/UWFhgVZzVQ==} + swagger-ui-dist@5.32.4: + resolution: {integrity: sha512-0AADFFQNJzExEN49SrD/34Nn9cxNxVLiydYl2MBwSZFPVXNkVwC/EFAjoezGGqE8oDegiDC+p47t8lKObCinMQ==} sync-fetch@0.4.5: resolution: {integrity: sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==} @@ -3857,8 +3857,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.58.2: - resolution: {integrity: sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==} + typescript-eslint@8.59.0: + resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -3869,8 +3869,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@6.0.2: - resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true @@ -3902,8 +3902,8 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - updates@17.15.3: - resolution: {integrity: sha512-owzNJZYmQf1YbX9hd8bKFwEMNyR8HSQMexlfoH+GLJew71PjTQ39VAOjtNQ9VhmiV2q8RTjoj//atkOVCHrz5w==} + updates@17.16.3: + resolution: {integrity: sha512-j+bkgObnDVB9hYTSg1tflwGtkFg23ZrgXiqRmRw3Reu/sh2P29M9oecxavO9uBMO2bbKpgC1OBrAnIpNZtL01w==} engines: {node: '>=22'} hasBin: true @@ -3925,8 +3925,8 @@ packages: peerDependencies: vite: '*' - vite@8.0.8: - resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + vite@8.0.9: + resolution: {integrity: sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4044,8 +4044,8 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - vue-tsc@3.2.6: - resolution: {integrity: sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==} + vue-tsc@3.2.7: + resolution: {integrity: sha512-zc1tL3HoQni1zGTGrwBVRQb7rGP5SWdu/m4rGB6JcnAC5MT5LFZIxF7Y+EJEnt4hGF23d60rXH7gRjHGb5KQQQ==} hasBin: true peerDependencies: typescript: '>=5.0.0' @@ -4240,14 +4240,14 @@ snapshots: dependencies: '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 '@lezer/common': 1.5.2 '@codemirror/commands@6.10.3': dependencies: '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 '@lezer/common': 1.5.2 '@codemirror/lang-angular@0.1.4': @@ -4257,7 +4257,7 @@ snapshots: '@codemirror/language': 6.12.3 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@codemirror/lang-cpp@6.0.3': dependencies: @@ -4287,7 +4287,7 @@ snapshots: '@codemirror/lang-javascript': 6.2.5 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 '@lezer/common': 1.5.2 '@lezer/css': 1.3.3 '@lezer/html': 1.3.13 @@ -4303,17 +4303,20 @@ snapshots: '@codemirror/language': 6.12.3 '@codemirror/lint': 6.9.5 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 '@lezer/common': 1.5.2 '@lezer/javascript': 1.5.4 - '@codemirror/lang-jinja@6.0.0': + '@codemirror/lang-jinja@6.0.1': dependencies: + '@codemirror/autocomplete': 6.20.1 '@codemirror/lang-html': 6.4.11 '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.41.1 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@codemirror/lang-json@6.0.2': dependencies: @@ -4326,7 +4329,7 @@ snapshots: '@codemirror/language': 6.12.3 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@codemirror/lang-liquid@6.3.2': dependencies: @@ -4334,10 +4337,10 @@ snapshots: '@codemirror/lang-html': 6.4.11 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@codemirror/lang-markdown@6.5.0': dependencies: @@ -4345,7 +4348,7 @@ snapshots: '@codemirror/lang-html': 6.4.11 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 '@lezer/common': 1.5.2 '@lezer/markdown': 1.6.3 @@ -4385,7 +4388,7 @@ snapshots: '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@codemirror/lang-vue@0.1.3': dependencies: @@ -4394,21 +4397,21 @@ snapshots: '@codemirror/language': 6.12.3 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@codemirror/lang-wast@6.0.2': dependencies: '@codemirror/language': 6.12.3 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@codemirror/lang-xml@6.1.0': dependencies: '@codemirror/autocomplete': 6.20.1 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 '@lezer/common': 1.5.2 '@lezer/xml': 1.0.6 @@ -4419,7 +4422,7 @@ snapshots: '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@lezer/yaml': 1.0.4 '@codemirror/language-data@6.5.2': @@ -4431,7 +4434,7 @@ snapshots: '@codemirror/lang-html': 6.4.11 '@codemirror/lang-java': 6.0.2 '@codemirror/lang-javascript': 6.2.5 - '@codemirror/lang-jinja': 6.0.0 + '@codemirror/lang-jinja': 6.0.1 '@codemirror/lang-json': 6.0.2 '@codemirror/lang-less': 6.0.2 '@codemirror/lang-liquid': 6.3.2 @@ -4451,10 +4454,10 @@ snapshots: '@codemirror/language@6.12.3': dependencies: '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 style-mod: 4.1.3 '@codemirror/legacy-modes@6.5.2': @@ -4464,20 +4467,20 @@ snapshots: '@codemirror/lint@6.9.5': dependencies: '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 crelt: 1.0.6 '@codemirror/search@6.6.0': dependencies: '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 crelt: 1.0.6 '@codemirror/state@6.6.0': dependencies: '@marijn/find-cluster-break': 1.0.2 - '@codemirror/view@6.41.0': + '@codemirror/view@6.41.1': dependencies: '@codemirror/state': 6.6.0 crelt: 1.0.6 @@ -4516,12 +4519,23 @@ snapshots: dependencies: '@swc/helpers': 0.5.21 + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.9.2': dependencies: '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 @@ -4610,24 +4624,24 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.2.0(jiti@2.6.1))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.2.1(jiti@2.6.1))': dependencies: escape-string-regexp: 4.0.0 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) ignore: 7.0.5 - '@eslint-community/eslint-utils@4.9.1(eslint@10.2.0(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1(jiti@2.6.1))': dependencies: - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@1.4.1(eslint@10.2.0(jiti@2.6.1))': + '@eslint/compat@1.4.1(eslint@10.2.1(jiti@2.6.1))': dependencies: '@eslint/core': 0.17.0 optionalDependencies: - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) '@eslint/config-array@0.23.5': dependencies: @@ -4697,13 +4711,18 @@ snapshots: '@github/combobox-nav': 2.3.1 dom-input-range: 2.0.1 - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': + '@humanfs/core@0.19.2': dependencies: - '@humanfs/core': 0.19.1 + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 '@humanwhocodes/retry': 0.4.3 + '@humanfs/types@0.15.0': {} + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/momoa@3.3.10': {} @@ -4748,19 +4767,19 @@ snapshots: dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@lezer/css@1.3.3': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@lezer/go@1.0.1': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@lezer/highlight@1.2.3': dependencies: @@ -4770,27 +4789,27 @@ snapshots: dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@lezer/java@1.1.3': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@lezer/javascript@1.5.4': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@lezer/json@1.0.3': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 - '@lezer/lr@1.4.8': + '@lezer/lr@1.4.10': dependencies: '@lezer/common': 1.5.2 @@ -4803,37 +4822,37 @@ snapshots: dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@lezer/python@1.1.18': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@lezer/rust@1.0.2': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@lezer/sass@1.1.0': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@lezer/xml@1.0.6': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@lezer/yaml@1.0.4': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 '@marijn/find-cluster-break@1.0.2': {} @@ -4855,12 +4874,12 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.10.1 optional: true - '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 '@emnapi/runtime': 1.9.2 @@ -4937,7 +4956,7 @@ snapshots: dependencies: '@nolyfill/shared': 1.0.44 - '@oxc-project/types@0.124.0': {} + '@oxc-project/types@0.126.0': {} '@package-json/types@0.0.12': {} @@ -4949,27 +4968,27 @@ snapshots: '@popperjs/core@2.11.8': {} - '@primer/octicons@19.24.0': + '@primer/octicons@19.24.1': dependencies: object-assign: 4.1.1 - '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.0)': + '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.1)': dependencies: '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 - '@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.8)': + '@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.1)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10)': dependencies: '@codemirror/autocomplete': 6.20.1 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 - '@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.20.1)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.8)': + '@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.20.1)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.1)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.10)': dependencies: '@codemirror/autocomplete': 6.20.1 '@codemirror/lang-css': 6.3.1 @@ -4977,13 +4996,13 @@ snapshots: '@codemirror/lang-javascript': 6.2.5 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/javascript': 1.5.4 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 - '@replit/codemirror-vscode-keymap@6.0.2(@codemirror/autocomplete@6.20.1)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/view@6.41.0)': + '@replit/codemirror-vscode-keymap@6.0.2(@codemirror/autocomplete@6.20.1)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/view@6.41.1)': dependencies: '@codemirror/autocomplete': 6.20.1 '@codemirror/commands': 6.10.3 @@ -4991,72 +5010,67 @@ snapshots: '@codemirror/lint': 6.9.5 '@codemirror/search': 6.6.0 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.41.0 + '@codemirror/view': 6.41.1 '@resvg/resvg-wasm@2.6.2': {} - '@rolldown/binding-android-arm64@1.0.0-rc.15': + '@rolldown/binding-android-arm64@1.0.0-rc.16': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.15': + '@rolldown/binding-darwin-x64@1.0.0-rc.16': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': dependencies: '@emnapi/core': 1.9.2 '@emnapi/runtime': 1.9.2 - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': optional: true '@rolldown/pluginutils@1.0.0-rc.13': {} - '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rolldown/pluginutils@1.0.0-rc.16': {} '@rtsao/scc@1.1.0': {} '@scarf/scarf@1.4.0': {} - '@silverwind/vue3-calendar-heatmap@2.1.1(tippy.js@6.3.7)(vue@3.5.32(typescript@6.0.2))': - dependencies: - tippy.js: 6.3.7 - vue: 3.5.32(typescript@6.0.2) - '@simonwep/pickr@1.9.0': dependencies: core-js: 3.32.2 @@ -5079,26 +5093,26 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@5.10.0(eslint@10.2.0(jiti@2.6.1))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) - '@typescript-eslint/types': 8.58.2 - eslint: 10.2.0(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) + '@typescript-eslint/types': 8.59.0 + eslint: 10.2.1(jiti@2.6.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 picomatch: 4.0.4 - '@stylistic/stylelint-plugin@5.1.0(stylelint@17.7.0(typescript@6.0.2))': + '@stylistic/stylelint-plugin@5.1.0(stylelint@17.8.0(typescript@6.0.3))': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - postcss: 8.5.9 + postcss: 8.5.10 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 style-search: 0.1.0 - stylelint: 17.7.0(typescript@6.0.2) + stylelint: 17.8.0(typescript@6.0.3) '@swc/helpers@0.5.21': dependencies: @@ -5292,15 +5306,15 @@ snapshots: dependencies: '@types/node': 25.6.0 - '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.58.2 - eslint: 10.2.0(jiti@2.6.1) + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/type-utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.0 + eslint: 10.2.1(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -5308,109 +5322,109 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/visitor-keys': 8.58.2 - eslint: 10.2.0(jiti@2.6.1) + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/type-utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.0 + eslint: 10.2.1(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.0 debug: 4.4.3 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + '@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.0 debug: 4.4.3 - eslint: 10.2.0(jiti@2.6.1) - typescript: 6.0.2 + eslint: 10.2.1(jiti@2.6.1) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.2(typescript@5.9.3)': + '@typescript-eslint/project-service@8.59.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3) - '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) + '@typescript-eslint/types': 8.59.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.2(typescript@6.0.2)': + '@typescript-eslint/project-service@8.59.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) - '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 debug: 4.4.3 - typescript: 6.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.58.2': + '@typescript-eslint/scope-manager@8.59.0': dependencies: - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 - '@typescript-eslint/tsconfig-utils@8.58.2(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.2)': + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@6.0.3)': dependencies: - typescript: 6.0.2 + typescript: 6.0.3 - '@typescript-eslint/type-utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + '@typescript-eslint/type-utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.2.0(jiti@2.6.1) - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 + eslint: 10.2.1(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.58.2': {} + '@typescript-eslint/types@8.59.0': {} - '@typescript-eslint/typescript-estree@8.58.2(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.59.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.58.2(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3) - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/project-service': 8.59.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 debug: 4.4.3 minimatch: 10.2.5 semver: 7.7.4 @@ -5420,46 +5434,46 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2)': + '@typescript-eslint/typescript-estree@8.59.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.58.2(typescript@6.0.2) - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/project-service': 8.59.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 debug: 4.4.3 minimatch: 10.2.5 semver: 7.7.4 tinyglobby: 0.2.16 - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3) - eslint: 10.2.0(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + eslint: 10.2.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + '@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - eslint: 10.2.0(jiti@2.6.1) - typescript: 6.0.2 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + eslint: 10.2.1(jiti@2.6.1) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.58.2': + '@typescript-eslint/visitor-keys@8.59.0': dependencies: - '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/types': 8.59.0 eslint-visitor-keys: 5.0.1 '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -5526,21 +5540,21 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1))(vue@3.5.32(typescript@6.0.2))': + '@vitejs/plugin-vue@6.0.6(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1))(vue@3.5.32(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.13 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) - vue: 3.5.32(typescript@6.0.2) + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) + vue: 3.5.32(typescript@6.0.3) - '@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)(vitest@4.1.4(@types/node@25.6.0)(happy-dom@20.8.9)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)))': + '@vitest/eslint-plugin@1.6.16(@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)(vitest@4.1.4(@types/node@25.6.0)(happy-dom@20.9.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)))': dependencies: - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - eslint: 10.2.0(jiti@2.6.1) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + eslint: 10.2.1(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - typescript: 6.0.2 - vitest: 4.1.4(@types/node@25.6.0)(happy-dom@20.8.9)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + typescript: 6.0.3 + vitest: 4.1.4(@types/node@25.6.0)(happy-dom@20.9.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -5553,13 +5567,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1))': + '@vitest/mocker@4.1.4(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) '@vitest/pretty-format@4.1.4': dependencies: @@ -5619,7 +5633,7 @@ snapshots: '@vue/shared': 3.5.32 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.9 + postcss: 8.5.10 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.32': @@ -5627,7 +5641,7 @@ snapshots: '@vue/compiler-dom': 3.5.32 '@vue/shared': 3.5.32 - '@vue/language-core@3.2.6': + '@vue/language-core@3.2.7': dependencies: '@volar/language-core': 2.4.28 '@vue/compiler-dom': 3.5.32 @@ -5653,11 +5667,11 @@ snapshots: '@vue/shared': 3.5.32 csstype: 3.2.3 - '@vue/server-renderer@3.5.32(vue@3.5.32(typescript@6.0.2))': + '@vue/server-renderer@3.5.32(vue@3.5.32(typescript@6.0.3))': dependencies: '@vue/compiler-ssr': 3.5.32 '@vue/shared': 3.5.32 - vue: 3.5.32(typescript@6.0.2) + vue: 3.5.32(typescript@6.0.3) '@vue/shared@3.5.32': {} @@ -5728,7 +5742,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.18: {} + baseline-browser-mapping@2.10.20: {} binary-extensions@2.3.0: {} @@ -5749,9 +5763,9 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.18 - caniuse-lite: 1.0.30001787 - electron-to-chromium: 1.5.335 + baseline-browser-mapping: 2.10.20 + caniuse-lite: 1.0.30001788 + electron-to-chromium: 1.5.340 node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) @@ -5778,7 +5792,7 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001787: {} + caniuse-lite@1.0.30001788: {} chai@6.2.2: {} @@ -5840,7 +5854,7 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 - clippie@4.1.10: {} + clippie@4.1.14: {} codemirror-lang-elixir@4.0.1: dependencies: @@ -5895,14 +5909,14 @@ snapshots: dependencies: layout-base: 2.0.1 - cosmiconfig@9.0.1(typescript@6.0.2): + cosmiconfig@9.0.1(typescript@6.0.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 js-yaml: 4.1.1 parse-json: 5.2.0 optionalDependencies: - typescript: 6.0.2 + typescript: 6.0.3 crelt@1.0.6: {} @@ -6187,7 +6201,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.3.3: + dompurify@3.4.0: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -6205,7 +6219,7 @@ snapshots: codemirror-spell-checker: 1.1.2 marked: 4.3.0 - electron-to-chromium@1.5.335: {} + electron-to-chromium@1.5.340: {} elkjs@0.9.3: {} @@ -6262,13 +6276,13 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.2.0(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@10.2.1(jiti@2.6.1)): dependencies: - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: - get-tsconfig: 4.13.7 + get-tsconfig: 4.14.0 stable-hash-x: 0.2.0 optionalDependencies: unrs-resolver: 1.11.1 @@ -6281,103 +6295,103 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint@10.2.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.2.0(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.2.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.2.1(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) - get-tsconfig: 4.13.7 + get-tsconfig: 4.14.0 is-bun-module: 2.0.0 stable-hash-x: 0.2.0 tinyglobby: 0.2.16 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.0(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.1(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.2.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - eslint: 10.2.0(jiti@2.6.1) + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + eslint: 10.2.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint@10.2.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.2.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.2.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.2.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-array-func@5.1.1(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-array-func@5.1.1(eslint@10.2.1(jiti@2.6.1)): dependencies: - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) - eslint-plugin-de-morgan@2.1.1(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-de-morgan@2.1.1(eslint@10.2.1(jiti@2.6.1)): dependencies: - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) - eslint-plugin-escompat@3.11.4(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-escompat@3.11.4(eslint@10.2.1(jiti@2.6.1)): dependencies: browserslist: 4.28.2 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) - eslint-plugin-eslint-comments@3.2.0(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-eslint-comments@3.2.0(eslint@10.2.1(jiti@2.6.1)): dependencies: escape-string-regexp: 1.0.5 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) ignore: 5.3.2 - eslint-plugin-filenames@1.3.2(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-filenames@1.3.2(eslint@10.2.1(jiti@2.6.1)): dependencies: - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) lodash.camelcase: 4.3.0 lodash.kebabcase: 4.1.1 lodash.snakecase: 4.1.1 lodash.upperfirst: 4.3.1 - eslint-plugin-github@6.0.0(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-github@6.0.0(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.1(jiti@2.6.1)): dependencies: - '@eslint/compat': 1.4.1(eslint@10.2.0(jiti@2.6.1)) + '@eslint/compat': 1.4.1(eslint@10.2.1(jiti@2.6.1)) '@eslint/eslintrc': 3.3.5 '@eslint/js': 9.39.4 '@github/browserslist-config': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) aria-query: 5.3.2 - eslint: 10.2.0(jiti@2.6.1) - eslint-config-prettier: 10.1.8(eslint@10.2.0(jiti@2.6.1)) - eslint-plugin-escompat: 3.11.4(eslint@10.2.0(jiti@2.6.1)) - eslint-plugin-eslint-comments: 3.2.0(eslint@10.2.0(jiti@2.6.1)) - eslint-plugin-filenames: 1.3.2(eslint@10.2.0(jiti@2.6.1)) - eslint-plugin-i18n-text: 1.0.1(eslint@10.2.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.0(jiti@2.6.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@10.2.0(jiti@2.6.1)) + eslint: 10.2.1(jiti@2.6.1) + eslint-config-prettier: 10.1.8(eslint@10.2.1(jiti@2.6.1)) + eslint-plugin-escompat: 3.11.4(eslint@10.2.1(jiti@2.6.1)) + eslint-plugin-eslint-comments: 3.2.0(eslint@10.2.1(jiti@2.6.1)) + eslint-plugin-filenames: 1.3.2(eslint@10.2.1(jiti@2.6.1)) + eslint-plugin-i18n-text: 1.0.1(eslint@10.2.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.1(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@10.2.1(jiti@2.6.1)) eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-prettier: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(prettier@3.8.2) + eslint-plugin-prettier: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.1(jiti@2.6.1)))(eslint@10.2.1(jiti@2.6.1))(prettier@3.8.3) eslint-rule-documentation: 1.0.23 globals: 16.5.0 jsx-ast-utils: 3.3.5 - prettier: 3.8.2 + prettier: 3.8.3 svg-element-attributes: 1.3.1 typescript: 5.9.3 - typescript-eslint: 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3) + typescript-eslint: 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - '@types/eslint' - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-i18n-text@1.0.1(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-i18n-text@1.0.1(eslint@10.2.1(jiti@2.6.1)): dependencies: - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) - eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.2.1(jiti@2.6.1)): dependencies: '@package-json/types': 0.0.12 - '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/types': 8.59.0 comment-parser: 1.4.6 debug: 4.4.3 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.2.5 @@ -6385,12 +6399,12 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: '@nolyfill/array-includes@1.0.44' @@ -6399,9 +6413,9 @@ snapshots: array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' debug: 3.2.7 doctrine: 2.1.0 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.2.1(jiti@2.6.1)) hasown: '@nolyfill/hasown@1.0.44' is-core-module: '@nolyfill/is-core-module@1.0.39' is-glob: 4.0.3 @@ -6413,13 +6427,13 @@ snapshots: string.prototype.trimend: '@nolyfill/string.prototype.trimend@1.0.44' tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@10.2.1(jiti@2.6.1)): dependencies: aria-query: 5.3.2 array-includes: '@nolyfill/array-includes@1.0.44' @@ -6429,7 +6443,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) hasown: '@nolyfill/hasown@1.0.44' jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -6440,37 +6454,37 @@ snapshots: eslint-plugin-no-only-tests@3.3.0: {} - eslint-plugin-playwright@2.10.1(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-playwright@2.10.2(eslint@10.2.1(jiti@2.6.1)): dependencies: - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) globals: 17.5.0 - eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(prettier@3.8.2): + eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.1(jiti@2.6.1)))(eslint@10.2.1(jiti@2.6.1))(prettier@3.8.3): dependencies: - eslint: 10.2.0(jiti@2.6.1) - prettier: 3.8.2 + eslint: 10.2.1(jiti@2.6.1) + prettier: 3.8.3 prettier-linter-helpers: 1.0.1 synckit: 0.11.12 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.2.0(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@10.2.1(jiti@2.6.1)) - eslint-plugin-regexp@3.1.0(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-regexp@3.1.0(eslint@10.2.1(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 comment-parser: 1.4.6 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) jsdoc-type-pratt-parser: 7.2.0 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-sonarjs@4.0.2(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-sonarjs@4.0.3(eslint@10.2.1(jiti@2.6.1)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) functional-red-black-tree: 1.0.1 globals: 17.5.0 jsx-ast-utils-x: 0.1.0 @@ -6478,18 +6492,18 @@ snapshots: minimatch: 10.2.5 scslre: 0.3.0 semver: 7.7.4 - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 - eslint-plugin-unicorn@64.0.0(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-unicorn@64.0.0(eslint@10.2.1(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) change-case: 5.4.4 ci-info: 4.4.0 clean-regexp: 1.0.0 core-js-compat: 3.49.0 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) find-up-simple: 1.0.1 globals: 17.5.0 indent-string: 5.0.0 @@ -6501,33 +6515,33 @@ snapshots: semver: 7.7.4 strip-indent: 4.1.1 - eslint-plugin-vue-scoped-css@3.0.0(eslint@10.2.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.0(jiti@2.6.1))): + eslint-plugin-vue-scoped-css@3.0.0(eslint@10.2.1(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) - eslint: 10.2.0(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) + eslint: 10.2.1(jiti@2.6.1) lodash: 4.18.1 - postcss: 8.5.9 - postcss-safe-parser: 7.0.1(postcss@8.5.9) + postcss: 8.5.10 + postcss-safe-parser: 7.0.1(postcss@8.5.10) postcss-selector-parser: 7.1.1 - vue-eslint-parser: 10.4.0(eslint@10.2.0(jiti@2.6.1)) + vue-eslint-parser: 10.4.0(eslint@10.2.1(jiti@2.6.1)) - eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.0(jiti@2.6.1)))(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.0(jiti@2.6.1))): + eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.1(jiti@2.6.1)))(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) - eslint: 10.2.0(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) + eslint: 10.2.1(jiti@2.6.1) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.1 semver: 7.7.4 - vue-eslint-parser: 10.4.0(eslint@10.2.0(jiti@2.6.1)) + vue-eslint-parser: 10.4.0(eslint@10.2.1(jiti@2.6.1)) xml-name-validator: 4.0.0 optionalDependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.0(jiti@2.6.1)) - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.1(jiti@2.6.1)) + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) - eslint-plugin-wc@3.1.0(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-wc@3.1.0(eslint@10.2.1(jiti@2.6.1)): dependencies: - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) is-valid-element-name: 1.0.0 js-levenshtein-esm: 2.0.0 @@ -6546,15 +6560,15 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.2.0(jiti@2.6.1): + eslint@10.2.1(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.5.5 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.1 - '@humanfs/node': 0.16.7 + '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 @@ -6697,7 +6711,7 @@ snapshots: get-east-asian-width@1.5.0: {} - get-tsconfig@4.13.7: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -6740,7 +6754,7 @@ snapshots: hammerjs@2.0.8: {} - happy-dom@20.8.9: + happy-dom@20.9.0: dependencies: '@types/node': 25.6.0 '@types/whatwg-mimetype': 3.0.2 @@ -6945,7 +6959,7 @@ snapshots: lezer-elixir@1.1.3: dependencies: '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.8 + '@lezer/lr': 1.4.10 lightningcss-android-arm64@1.32.0: optional: true @@ -7105,13 +7119,13 @@ snapshots: d3-sankey: 0.12.3 dagre-d3-es: 7.0.14 dayjs: 1.11.20 - dompurify: 3.3.3 + dompurify: 3.4.0 katex: 0.16.45 khroma: 2.1.0 lodash-es: 4.18.1 marked: 16.4.2 roughjs: 4.6.6 - stylis: 4.3.6 + stylis: 4.4.0 ts-dedent: 2.2.0 uuid: 11.1.0 @@ -7459,40 +7473,40 @@ snapshots: dependencies: htmlparser2: 8.0.2 js-tokens: 9.0.1 - postcss: 8.5.9 - postcss-safe-parser: 6.0.0(postcss@8.5.9) + postcss: 8.5.10 + postcss-safe-parser: 6.0.0(postcss@8.5.10) - postcss-import@15.1.0(postcss@8.5.9): + postcss-import@15.1.0(postcss@8.5.10): dependencies: - postcss: 8.5.9 + postcss: 8.5.10 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.12 - postcss-js@4.1.0(postcss@8.5.9): + postcss-js@4.1.0(postcss@8.5.10): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.9 + postcss: 8.5.10 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.9): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.10): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.9 + postcss: 8.5.10 - postcss-nested@6.2.0(postcss@8.5.9): + postcss-nested@6.2.0(postcss@8.5.10): dependencies: - postcss: 8.5.9 + postcss: 8.5.10 postcss-selector-parser: 6.1.2 - postcss-safe-parser@6.0.0(postcss@8.5.9): + postcss-safe-parser@6.0.0(postcss@8.5.10): dependencies: - postcss: 8.5.9 + postcss: 8.5.10 - postcss-safe-parser@7.0.1(postcss@8.5.9): + postcss-safe-parser@7.0.1(postcss@8.5.10): dependencies: - postcss: 8.5.9 + postcss: 8.5.10 postcss-selector-parser@6.1.2: dependencies: @@ -7506,7 +7520,7 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.9: + postcss@8.5.10: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -7518,7 +7532,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.8.2: {} + prettier@3.8.3: {} punycode.js@2.3.1: {} @@ -7581,30 +7595,30 @@ snapshots: robust-predicates@3.0.3: {} - rolldown-license-plugin@2.2.5(rolldown@1.0.0-rc.15): + rolldown-license-plugin@3.0.1(rolldown@1.0.0-rc.16): dependencies: - rolldown: 1.0.0-rc.15 + rolldown: 1.0.0-rc.16 - rolldown@1.0.0-rc.15: + rolldown@1.0.0-rc.16: dependencies: - '@oxc-project/types': 0.124.0 - '@rolldown/pluginutils': 1.0.0-rc.15 + '@oxc-project/types': 0.126.0 + '@rolldown/pluginutils': 1.0.0-rc.16 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-x64': 1.0.0-rc.15 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + '@rolldown/binding-android-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-x64': 1.0.0-rc.16 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.16 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.16 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.16 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.16 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.16 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.16 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.16 roughjs@4.6.6: dependencies: @@ -7688,7 +7702,7 @@ snapshots: stackback@0.0.2: {} - std-env@4.0.0: {} + std-env@4.1.0: {} string-width@4.2.3: dependencies: @@ -7724,25 +7738,25 @@ snapshots: style-search@0.1.0: {} - stylelint-config-recommended@18.0.0(stylelint@17.7.0(typescript@6.0.2)): + stylelint-config-recommended@18.0.0(stylelint@17.8.0(typescript@6.0.3)): dependencies: - stylelint: 17.7.0(typescript@6.0.2) + stylelint: 17.8.0(typescript@6.0.3) - stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.7.0(typescript@6.0.2)): + stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.8.0(typescript@6.0.3)): dependencies: - stylelint: 17.7.0(typescript@6.0.2) + stylelint: 17.8.0(typescript@6.0.3) - stylelint-declaration-strict-value@1.11.1(stylelint@17.7.0(typescript@6.0.2)): + stylelint-declaration-strict-value@1.11.1(stylelint@17.8.0(typescript@6.0.3)): dependencies: - stylelint: 17.7.0(typescript@6.0.2) + stylelint: 17.8.0(typescript@6.0.3) - stylelint-value-no-unknown-custom-properties@6.1.1(stylelint@17.7.0(typescript@6.0.2)): + stylelint-value-no-unknown-custom-properties@6.1.1(stylelint@17.8.0(typescript@6.0.3)): dependencies: postcss-value-parser: 4.2.0 resolve: 1.22.12 - stylelint: 17.7.0(typescript@6.0.2) + stylelint: 17.8.0(typescript@6.0.3) - stylelint@17.7.0(typescript@6.0.2): + stylelint@17.8.0(typescript@6.0.3): dependencies: '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) @@ -7752,7 +7766,7 @@ snapshots: '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) colord: 2.9.3 - cosmiconfig: 9.0.1(typescript@6.0.2) + cosmiconfig: 9.0.1(typescript@6.0.3) css-functions-list: 3.3.3 css-tree: 3.2.1 debug: 4.4.3 @@ -7771,8 +7785,8 @@ snapshots: micromatch: 4.0.8 normalize-path: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.9 - postcss-safe-parser: 7.0.1(postcss@8.5.9) + postcss: 8.5.10 + postcss-safe-parser: 7.0.1(postcss@8.5.10) postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 string-width: 8.2.0 @@ -7784,7 +7798,7 @@ snapshots: - supports-color - typescript - stylis@4.3.6: {} + stylis@4.4.0: {} sucrase@3.35.1: dependencies: @@ -7824,7 +7838,7 @@ snapshots: deep-rename-keys: 0.2.1 xml-reader: 2.4.3 - swagger-ui-dist@5.32.2: + swagger-ui-dist@5.32.4: dependencies: '@scarf/scarf': 1.4.0 @@ -7863,11 +7877,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.9 - postcss-import: 15.1.0(postcss@8.5.9) - postcss-js: 4.1.0(postcss@8.5.9) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.9) - postcss-nested: 6.2.0(postcss@8.5.9) + postcss: 8.5.10 + postcss-import: 15.1.0(postcss@8.5.10) + postcss-js: 4.1.0(postcss@8.5.10) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.10) + postcss-nested: 6.2.0(postcss@8.5.10) postcss-selector-parser: 6.1.2 resolve: 1.22.12 sucrase: 3.35.1 @@ -7916,9 +7930,9 @@ snapshots: dependencies: typescript: 5.9.3 - ts-api-utils@2.5.0(typescript@6.0.2): + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: - typescript: 6.0.2 + typescript: 6.0.3 ts-dedent@2.2.0: {} @@ -7937,31 +7951,31 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.2.0(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.2.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - typescript-eslint@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): + typescript-eslint@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - eslint: 10.2.0(jiti@2.6.1) - typescript: 6.0.2 + '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) + eslint: 10.2.1(jiti@2.6.1) + typescript: 6.0.3 transitivePeerDependencies: - supports-color typescript@5.9.3: {} - typescript@6.0.2: {} + typescript@6.0.3: {} typo-js@1.3.1: {} @@ -8005,7 +8019,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - updates@17.15.3: {} + updates@17.16.3: {} uri-js@4.4.1: dependencies: @@ -8017,16 +8031,16 @@ snapshots: vanilla-colorful@0.7.2: {} - vite-string-plugin@2.0.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)): + vite-string-plugin@2.0.2(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)): dependencies: - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) - vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1): + vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.9 - rolldown: 1.0.0-rc.15 + postcss: 8.5.10 + rolldown: 1.0.0-rc.16 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.6.0 @@ -8034,10 +8048,10 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 - vitest@4.1.4(@types/node@25.6.0)(happy-dom@20.8.9)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)): + vitest@4.1.4(@types/node@25.6.0)(happy-dom@20.9.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + '@vitest/mocker': 4.1.4(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -8049,16 +8063,16 @@ snapshots: obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.4 - std-env: 4.0.0 + std-env: 4.1.0 tinybench: 2.9.0 tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.6.0 - happy-dom: 20.8.9 + happy-dom: 20.9.0 transitivePeerDependencies: - msw @@ -8079,21 +8093,21 @@ snapshots: vscode-uri@3.1.0: {} - vue-bar-graph@2.2.0(typescript@6.0.2): + vue-bar-graph@2.2.0(typescript@6.0.3): dependencies: - vue: 3.5.32(typescript@6.0.2) + vue: 3.5.32(typescript@6.0.3) transitivePeerDependencies: - typescript - vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.32(typescript@6.0.2)): + vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.32(typescript@6.0.3)): dependencies: chart.js: 4.5.1 - vue: 3.5.32(typescript@6.0.2) + vue: 3.5.32(typescript@6.0.3) - vue-eslint-parser@10.4.0(eslint@10.2.0(jiti@2.6.1)): + vue-eslint-parser@10.4.0(eslint@10.2.1(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 10.2.0(jiti@2.6.1) + eslint: 10.2.1(jiti@2.6.1) eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 espree: 11.2.0 @@ -8102,21 +8116,21 @@ snapshots: transitivePeerDependencies: - supports-color - vue-tsc@3.2.6(typescript@6.0.2): + vue-tsc@3.2.7(typescript@6.0.3): dependencies: '@volar/typescript': 2.4.28 - '@vue/language-core': 3.2.6 - typescript: 6.0.2 + '@vue/language-core': 3.2.7 + typescript: 6.0.3 - vue@3.5.32(typescript@6.0.2): + vue@3.5.32(typescript@6.0.3): dependencies: '@vue/compiler-dom': 3.5.32 '@vue/compiler-sfc': 3.5.32 '@vue/runtime-dom': 3.5.32 - '@vue/server-renderer': 3.5.32(vue@3.5.32(typescript@6.0.2)) + '@vue/server-renderer': 3.5.32(vue@3.5.32(typescript@6.0.3)) '@vue/shared': 3.5.32 optionalDependencies: - typescript: 6.0.2 + typescript: 6.0.3 w3c-keyname@2.2.8: {} diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 00000000000..586733b317e --- /dev/null +++ b/renovate.json5 @@ -0,0 +1,88 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended", "helpers:pinGitHubActionDigests", "customManagers:githubActionsVersions"], + "configMigration": true, + "enabledManagers": ["github-actions", "gomod", "npm", "pep621", "nix"], + "labels": ["dependencies"], + "branchPrefix": "renovate/", + "schedule": ["* * * * 1"], // dependency update PRs weekly, vulnerabilityAlerts bypasses this + "minimumReleaseAge": "5 days", + "semanticCommits": "enabled", + "osvVulnerabilityAlerts": true, + "vulnerabilityAlerts": { + "enabled": true, + }, + "customManagers": [ + { + "customType": "regex", + "managerFilePatterns": ["/(^|/)Makefile$/"], + "matchStrings": [ + "[A-Z_]+_PACKAGE\\s*\\?=\\s*(?[^@\\s]+?)(?:/cmd/[^@/\\s]+)?@(?\\S+)\\s+# renovate: datasource=(?\\S+)", + ], + }, + ], + "packageRules": [ + { + "groupName": "action dependencies", + "matchManagers": ["github-actions"], + }, + { + "matchPackageNames": ["@mcaptcha/vanilla-glue"], + "allowedVersions": "^0.1", // breaking changes in rc versions need to be handled + }, + { + "matchPackageNames": ["cropperjs"], + "allowedVersions": "^1", // need to migrate to v2 but v2 is not compatible with v1 + }, + { + "matchPackageNames": ["tailwindcss"], + "allowedVersions": "^3", // need to migrate + }, + { + "matchPackageNames": ["github.com/urfave/cli/v3"], + "allowedVersions": "<3.6.2", // v3.6.2 breaks -c flag parsing in help commands + }, + { + "matchPackageNames": ["github.com/Azure/azure-sdk-for-go/sdk/azcore"], + "allowedVersions": "<1.21.0", // v1.21.0+ uses API version unsupported by Azurite in CI + }, + { + "matchPackageNames": ["github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"], + "allowedVersions": "<1.6.4", // v1.6.4+ uses API version unsupported by Azurite in CI + }, + { + "matchPackageNames": ["github.com/microsoft/go-mssqldb"], + "allowedVersions": "<=1.9.7", // downgraded with Azure SDK + }, + { + "matchPackageNames": ["go.yaml.in/yaml/v4"], + "allowedVersions": "<4.0.0-rc.4", // rc.4 changes block scalar serialization, wait for stable release + }, + { + "groupName": "go dependencies", + "matchDatasources": ["go"], // covers gomod manager + Makefile go-tool customManager + "postUpgradeTasks": { + "commands": ["make tidy"], + "fileFilters": ["go.mod", "go.sum", "assets/go-licenses.json"], + "executionMode": "branch", + }, + }, + { + "groupName": "npm dependencies", + "matchManagers": ["npm"], + "postUpgradeTasks": { + "commands": ["make svg"], + "fileFilters": ["public/assets/img/svg/**"], + "executionMode": "branch", + }, + }, + { + "groupName": "python dependencies", + "matchManagers": ["pep621"], + }, + { + "groupName": "nix dependencies", + "matchManagers": ["nix"], + }, + ], +} diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 13cbecb5cd0..838ddb7f917 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -74,6 +74,7 @@ import ( "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" @@ -310,7 +311,7 @@ func (ar artifactRoutes) confirmUploadArtifact(ctx *ArtifactContext) { ctx.HTTPError(http.StatusBadRequest, "Error artifact name is empty") return } - if err := mergeChunksForRun(ctx, ar.fs, runID, artifactName); err != nil { + if err := mergeChunksForRun(ctx, ar.fs, runID, ctx.ActionTask.Job.RunAttemptID, artifactName); err != nil { log.Error("Error merge chunks: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks") return @@ -338,8 +339,9 @@ func (ar artifactRoutes) listArtifacts(ctx *ArtifactContext) { } artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ - RunID: runID, - Status: int(actions.ArtifactStatusUploadConfirmed), + RunID: runID, + RunAttemptID: optional.Some(ctx.ActionTask.Job.RunAttemptID), + Status: int(actions.ArtifactStatusUploadConfirmed), }) if err != nil { log.Error("Error getting artifacts: %v", err) @@ -404,6 +406,7 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) { artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ RunID: runID, + RunAttemptID: optional.Some(ctx.ActionTask.Job.RunAttemptID), ArtifactName: itemPath, Status: int(actions.ArtifactStatusUploadConfirmed), }) @@ -477,6 +480,11 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) { ctx.HTTPError(http.StatusBadRequest) return } + if ctx.ActionTask.Job.RunAttemptID > 0 && artifact.RunAttemptID != ctx.ActionTask.Job.RunAttemptID { + log.Error("Error mismatch runAttemptID and artifactID, task: %v, artifact: %v", ctx.ActionTask.Job.RunAttemptID, artifactID) + ctx.HTTPError(http.StatusBadRequest) + return + } if artifact.Status != actions.ArtifactStatusUploadConfirmed { log.Error("Error artifact not found: %s", artifact.Status.ToString()) ctx.HTTPError(http.StatusNotFound, "Error artifact not found") diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index 8d04c689221..6f84f7a5cf8 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" ) @@ -257,10 +258,11 @@ func listOrderedChunksForArtifact(st storage.ObjectStorage, runID, artifactID in return emptyListAsError(chunks) } -func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int64, artifactName string) error { +func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID, runAttemptID int64, artifactName string) error { // read all db artifacts by name artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ RunID: runID, + RunAttemptID: optional.Some(runAttemptID), ArtifactName: artifactName, }) if err != nil { diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index e86645cb0cf..8bd3fb7e2b0 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -107,6 +107,7 @@ import ( "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" @@ -266,9 +267,9 @@ func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (* return task, artifactName, true } -func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions_model.ActionArtifact, error) { +func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID, runAttemptID int64, name string) (*actions_model.ActionArtifact, error) { var art actions_model.ActionArtifact - has, err := db.GetEngine(ctx).Where(builder.Eq{"run_id": runID, "artifact_name": name}, builder.Like{"content_encoding", "%/%"}).Get(&art) + has, err := db.GetEngine(ctx).Where(builder.Eq{"run_id": runID, "run_attempt_id": runAttemptID, "artifact_name": name}, builder.Like{"content_encoding", "%/%"}).Get(&art) if err != nil { return nil, err } else if !has { @@ -388,7 +389,7 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { switch comp { case "block", "appendBlock": // get artifact by name - artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) + artifact, err := r.getArtifactByName(ctx, task.Job.RunID, task.Job.RunAttemptID, artifactName) if err != nil { log.Error("Error artifact not found: %v", err) ctx.HTTPError(http.StatusNotFound, "Error artifact not found") @@ -475,7 +476,7 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { } // get artifact by name - artifact, err := r.getArtifactByName(ctx, runID, req.Name) + artifact, err := r.getArtifactByName(ctx, runID, ctx.ActionTask.Job.RunAttemptID, req.Name) if err != nil { log.Error("Error artifact not found: %v", err) ctx.HTTPError(http.StatusNotFound, "Error artifact not found") @@ -589,6 +590,7 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{ RunID: runID, + RunAttemptID: optional.Some(ctx.ActionTask.Job.RunAttemptID), Status: int(actions_model.ArtifactStatusUploadConfirmed), FinalizedArtifactsV4: true, }) @@ -642,7 +644,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { artifactName := req.Name // get artifact by name - artifact, err := r.getArtifactByName(ctx, runID, artifactName) + artifact, err := r.getArtifactByName(ctx, runID, ctx.ActionTask.Job.RunAttemptID, artifactName) if err != nil { log.Error("Error artifact not found: %v", err) ctx.HTTPError(http.StatusNotFound, "Error artifact not found") @@ -676,7 +678,7 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { } // get artifact by name - artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) + artifact, err := r.getArtifactByName(ctx, task.Job.RunID, task.Job.RunAttemptID, artifactName) if err != nil { log.Error("Error artifact not found: %v", err) ctx.HTTPError(http.StatusNotFound, "Error artifact not found") @@ -707,14 +709,14 @@ func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { } // get artifact by name - artifact, err := r.getArtifactByName(ctx, runID, req.Name) + artifact, err := r.getArtifactByName(ctx, runID, ctx.ActionTask.Job.RunAttemptID, req.Name) if err != nil { log.Error("Error artifact not found: %v", err) ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } - err = actions_model.SetArtifactNeedDelete(ctx, runID, req.Name) + err = actions_model.SetArtifactNeedDeleteByRunAttempt(ctx, runID, ctx.ActionTask.Job.RunAttemptID, req.Name) if err != nil { log.Error("Error deleting artifacts: %v", err) ctx.HTTPError(http.StatusInternalServerError, err.Error()) diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go index 886595be715..eee39760edd 100644 --- a/routers/api/actions/runner/runner.go +++ b/routers/api/actions/runner/runner.go @@ -15,7 +15,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" actions_service "code.gitea.io/gitea/services/actions" - notify_service "code.gitea.io/gitea/services/notify" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" "code.gitea.io/actions-proto-go/runner/v1/runnerv1connect" @@ -224,7 +223,7 @@ func (s *Service) UpdateTask( actions_service.CreateCommitStatusForRunJobs(ctx, task.Job.Run, task.Job) if task.Status.IsDone() { - notify_service.WorkflowJobStatusUpdate(ctx, task.Job.Run.Repo, task.Job.Run.TriggerUser, task.Job, task) + actions_service.NotifyWorkflowJobStatusUpdateWithTask(ctx, task.Job, task) } if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED { @@ -232,7 +231,7 @@ func (s *Service) UpdateTask( log.Error("Emit ready jobs of run %d: %v", task.Job.RunID, err) } if task.Job.Run.Status.IsDone() { - actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, task.Job) + actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, task.Job.RepoID, task.Job.RunID) } } diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go index 51cedd2a9f6..d4fdb0affa2 100644 --- a/routers/api/packages/rpm/rpm.go +++ b/routers/api/packages/rpm/rpm.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "net/http" + "net/url" "strings" "time" @@ -220,30 +221,38 @@ func UploadPackageFile(ctx *context.Context) { func DownloadPackageFile(ctx *context.Context) { name := ctx.PathParam("name") version := ctx.PathParam("version") + architecture := ctx.PathParam("architecture") + group := ctx.PathParam("group") - s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( - ctx, - &packages_service.PackageInfo{ - Owner: ctx.Package.Owner, - PackageType: packages_model.TypeRpm, - Name: name, - Version: version, - }, - &packages_service.PackageFileInfo{ - Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.PathParam("architecture")), - CompositeKey: ctx.PathParam("group"), - }, - ctx.Req.Method, - ) - if err != nil { - if errors.Is(err, util.ErrNotExist) { - apiError(ctx, http.StatusNotFound, err) - } else { - apiError(ctx, http.StatusInternalServerError, err) - } - return + openForDownload := func(filename string) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { + return packages_service.OpenFileForDownloadByPackageNameAndVersion( + ctx, + &packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeRpm, + Name: name, + Version: version, + }, + &packages_service.PackageFileInfo{ + Filename: filename, + CompositeKey: group, + }, + ctx.Req.Method, + ) } + s, u, pf, err := openForDownload(fmt.Sprintf("%s-%s.%s.rpm", name, version, architecture)) + if errors.Is(err, util.ErrNotExist) && architecture != "noarch" { + s, u, pf, err = openForDownload(fmt.Sprintf("%s-%s.%s.rpm", name, version, "noarch")) + } + + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + return + } else if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } helper.ServePackageFile(ctx, s, u, pf) } diff --git a/routers/api/v1/admin/action.go b/routers/api/v1/admin/action.go index 2fbb8e1a955..62e0c6addcb 100644 --- a/routers/api/v1/admin/action.go +++ b/routers/api/v1/admin/action.go @@ -37,7 +37,7 @@ func ListWorkflowJobs(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - shared.ListJobs(ctx, 0, 0, 0) + shared.ListJobs(ctx, 0, 0, 0, nil) } // ListWorkflowRuns Lists all runs diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 2d80692fef5..633aa77430f 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -394,7 +394,7 @@ func reqSiteAdmin() func(ctx *context.APIContext) { // reqOwner user should be the owner of the repo or site admin. func reqOwner() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if !ctx.Repo.IsOwner() && !ctx.IsUserSiteAdmin() { + if !ctx.Repo.Permission.IsOwner() && !ctx.IsUserSiteAdmin() { ctx.APIError(http.StatusForbidden, "user should be the owner of the repo") return } @@ -434,7 +434,7 @@ func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { // reqRepoReader user should have specific read permission or be a repo admin or a site admin func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { + if !ctx.Repo.Permission.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { ctx.APIError(http.StatusForbidden, "user should have specific read permission or be a repo admin or a site admin") return } @@ -633,7 +633,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { } func mustEnableIssues(ctx *context.APIContext) { - if !ctx.Repo.CanRead(unit.TypeIssues) { + if !ctx.Repo.Permission.CanRead(unit.TypeIssues) { if log.IsTrace() { if ctx.IsSigned { log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+ @@ -656,7 +656,7 @@ func mustEnableIssues(ctx *context.APIContext) { } func mustAllowPulls(ctx *context.APIContext) { - if !(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.CanRead(unit.TypePullRequests)) { + if !(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.Permission.CanRead(unit.TypePullRequests)) { if ctx.Repo.Repository.CanEnablePulls() && log.IsTrace() { if ctx.IsSigned { log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+ @@ -679,8 +679,8 @@ func mustAllowPulls(ctx *context.APIContext) { } func mustEnableIssuesOrPulls(ctx *context.APIContext) { - if !ctx.Repo.CanRead(unit.TypeIssues) && - !(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.CanRead(unit.TypePullRequests)) { + if !ctx.Repo.Permission.CanRead(unit.TypeIssues) && + !(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.Permission.CanRead(unit.TypePullRequests)) { if ctx.Repo.Repository.CanEnablePulls() && log.IsTrace() { if ctx.IsSigned { log.Trace("Permission Denied: User %-v cannot read %-v and %-v in Repo %-v\n"+ @@ -705,7 +705,7 @@ func mustEnableIssuesOrPulls(ctx *context.APIContext) { } func mustEnableWiki(ctx *context.APIContext) { - if !(ctx.Repo.CanRead(unit.TypeWiki)) { + if !(ctx.Repo.Permission.CanRead(unit.TypeWiki)) { ctx.APIErrorNotFound() return } @@ -865,7 +865,6 @@ func checkDeprecatedAuthMethods(ctx *context.APIContext) { func Routes() *web.Router { m := web.NewRouter() - m.BeforeRouting(securityHeaders()) if setting.CORSConfig.Enabled { m.BeforeRouting(cors.Handler(cors.Options{ AllowedOrigins: setting.CORSConfig.AllowDomain, @@ -1255,6 +1254,10 @@ func Routes() *web.Router { m.Group("/runs", func() { m.Group("/{run}", func() { m.Get("", repo.GetWorkflowRun) + m.Group("/attempts/{attempt}", func() { + m.Get("", repo.GetWorkflowRunAttempt) + m.Get("/jobs", repo.ListWorkflowRunAttemptJobs) + }) m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun) m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun) m.Post("/rerun-failed-jobs", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunFailedWorkflowRun) @@ -1366,6 +1369,7 @@ func Routes() *web.Router { m.Combo("/requested_reviewers", reqToken()). Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests). Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests) + m.Post("/comments/{id}/replies", reqToken(), mustNotBeArchived, bind(api.CreatePullReviewCommentReplyOptions{}), repo.CreatePullReviewCommentReply) }) m.Get("/{base}/*", repo.GetPullRequestByBaseHead) }, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()) @@ -1745,14 +1749,3 @@ func Routes() *web.Router { return m } - -func securityHeaders() func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - // CORB: https://www.chromium.org/Home/chromium-security/corb-for-developers - // http://stackoverflow.com/a/3146618/244009 - resp.Header().Set("x-content-type-options", "nosniff") - next.ServeHTTP(resp, req) - }) - } -} diff --git a/routers/api/v1/misc/markup.go b/routers/api/v1/misc/markup.go index 909310b4c86..f7623b9105b 100644 --- a/routers/api/v1/misc/markup.go +++ b/routers/api/v1/misc/markup.go @@ -4,8 +4,6 @@ package misc import ( - "net/http" - "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" api "code.gitea.io/gitea/modules/structs" @@ -36,12 +34,6 @@ func Markup(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.MarkupOption) - - if ctx.HasAPIError() { - ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg()) - return - } - mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath) } @@ -67,12 +59,6 @@ func Markdown(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.MarkdownOption) - - if ctx.HasAPIError() { - ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg()) - return - } - mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "") } diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index 01b57b3fac9..d218c19fd43 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -624,7 +624,7 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) { // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" - shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0) + shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0, nil) } func (Action) ListWorkflowRuns(ctx *context.APIContext) { diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 7ac8a10575c..8a0be250da1 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -23,6 +23,7 @@ import ( secret_model "code.gitea.io/gitea/models/secret" "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -676,7 +677,7 @@ func (Action) UpdateRunner(ctx *context.APIContext) { shared.UpdateRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id")) } -// GetWorkflowRunJobs Lists all jobs for a workflow run. +// ListWorkflowJobs Lists all jobs for a repository. func (Action) ListWorkflowJobs(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs // --- @@ -717,7 +718,7 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) { repoID := ctx.Repo.Repository.ID - shared.ListJobs(ctx, 0, repoID, 0) + shared.ListJobs(ctx, 0, repoID, 0, nil) } // ListWorkflowRuns Lists all runs for a repository run. @@ -1163,7 +1164,7 @@ func getCurrentRepoActionRunJobsByID(ctx *context.APIContext) (*actions_model.Ac return nil, nil } - jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID) if err != nil { ctx.APIErrorInternal(err) return nil, nil @@ -1171,6 +1172,24 @@ func getCurrentRepoActionRunJobsByID(ctx *context.APIContext) (*actions_model.Ac return run, jobs } +func getCurrentRepoActionRunAttemptByNumber(ctx *context.APIContext) (*actions_model.ActionRun, *actions_model.ActionRunAttempt) { + run := getCurrentRepoActionRunByID(ctx) + if ctx.Written() { + return nil, nil + } + + attemptNum := ctx.PathParamInt64("attempt") + attempt, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(ctx, run.ID, attemptNum) + if errors.Is(err, util.ErrNotExist) { + ctx.APIErrorNotFound(err) + return nil, nil + } else if err != nil { + ctx.APIErrorInternal(err) + return nil, nil + } + return run, attempt +} + // GetWorkflowRun Gets a specific workflow run. func GetWorkflowRun(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun @@ -1207,7 +1226,56 @@ func GetWorkflowRun(ctx *context.APIContext) { return } - convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run) + convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run, nil) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusOK, convertedRun) +} + +// GetWorkflowRunAttempt Gets a specific workflow run attempt. +func GetWorkflowRunAttempt(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/attempts/{attempt} repository getWorkflowRunAttempt + // --- + // summary: Gets a specific workflow run attempt + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: id of the run + // type: integer + // required: true + // - name: attempt + // in: path + // description: logical attempt number of the run + // type: integer + // required: true + // responses: + // "200": + // "$ref": "#/responses/WorkflowRun" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + run, attempt := getCurrentRepoActionRunAttemptByNumber(ctx) + if ctx.Written() { + return + } + + convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run, attempt) if err != nil { ctx.APIErrorInternal(err) return @@ -1247,6 +1315,8 @@ func RerunWorkflowRun(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/error" // "422": // "$ref": "#/responses/validationError" @@ -1255,12 +1325,12 @@ func RerunWorkflowRun(ctx *context.APIContext) { return } - if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs); err != nil { + if _, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, jobs); err != nil { handleWorkflowRerunError(ctx, err) return } - convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run) + convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run, nil) if err != nil { ctx.APIErrorInternal(err) return @@ -1298,6 +1368,8 @@ func RerunFailedWorkflowRun(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/error" // "422": // "$ref": "#/responses/validationError" @@ -1306,7 +1378,7 @@ func RerunFailedWorkflowRun(ctx *context.APIContext) { return } - if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil { + if _, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, actions_service.GetFailedJobsForRerun(jobs)); err != nil { handleWorkflowRerunError(ctx, err) return } @@ -1351,6 +1423,8 @@ func RerunWorkflowJob(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/error" // "422": // "$ref": "#/responses/validationError" @@ -1367,12 +1441,28 @@ func RerunWorkflowJob(ctx *context.APIContext) { } targetJob := jobs[jobIdx] - if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetAllRerunJobs(targetJob, jobs)); err != nil { + newAttempt, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, []*actions_model.ActionRunJob{targetJob}) + if err != nil { handleWorkflowRerunError(ctx, err) return } - convertedJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, targetJob) + // Legacy jobs had AttemptJobID=0 before the rerun; createOriginalAttemptForLegacyRun inside + // RerunWorkflowRunJobs has since backfilled it in the DB, so reload only in that case. + if targetJob.AttemptJobID == 0 { + targetJob, err = actions_model.GetRunJobByRepoAndID(ctx, run.RepoID, targetJob.ID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } + rerunJob, err := actions_model.GetRunJobByAttemptJobID(ctx, run.ID, newAttempt.ID, targetJob.AttemptJobID) + if err != nil { + handleWorkflowRerunError(ctx, err) + return + } + + convertedJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, rerunJob) if err != nil { ctx.APIErrorInternal(err) return @@ -1384,6 +1474,12 @@ func handleWorkflowRerunError(ctx *context.APIContext, err error) { if errors.Is(err, util.ErrInvalidArgument) { ctx.APIError(http.StatusBadRequest, err) return + } else if errors.Is(err, util.ErrAlreadyExist) { + ctx.APIError(http.StatusConflict, err) + return + } else if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + return } ctx.APIErrorInternal(err) } @@ -1440,9 +1536,75 @@ func ListWorkflowRunJobs(ctx *context.APIContext) { return } + run, err := actions_model.GetRunByRepoAndID(ctx, repoID, runID) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.APIErrorNotFound(err) + } else { + ctx.APIErrorInternal(err) + } + return + } // runID is used as an additional filter next to repoID to ensure that we only list jobs for the specified repoID and runID. // no additional checks for runID are needed here - shared.ListJobs(ctx, 0, repoID, runID) + shared.ListJobs(ctx, 0, repoID, runID, optional.Some(run.LatestAttemptID)) +} + +// ListWorkflowRunAttemptJobs Lists all jobs for a workflow run attempt. +func ListWorkflowRunAttemptJobs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/attempts/{attempt}/jobs repository listWorkflowRunAttemptJobs + // --- + // summary: Lists all jobs for a workflow run attempt + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: id of the workflow run + // type: integer + // required: true + // - name: attempt + // in: path + // description: logical attempt number of the run + // type: integer + // required: true + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowJobsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + run, attempt := getCurrentRepoActionRunAttemptByNumber(ctx) + if ctx.Written() { + return + } + + shared.ListJobs(ctx, 0, run.RepoID, run.ID, optional.Some(attempt.ID)) } // GetWorkflowJob Gets a specific workflow job for a workflow run. @@ -1758,7 +1920,7 @@ func DeleteArtifact(ctx *context.APIContext) { } if actions.IsArtifactV4(art) { - if err := actions_model.SetArtifactNeedDelete(ctx, art.RunID, art.ArtifactName); err != nil { + if err := actions_model.SetArtifactNeedDeleteByID(ctx, art.ID); err != nil { ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 295e4c2b5ed..a9b88317d4c 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -80,7 +80,7 @@ func GetBranch(ctx *context.APIContext) { return } - br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branchName, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) + br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branchName, c, branchProtection, ctx.Doer, ctx.Repo.Permission.IsAdmin()) if err != nil { ctx.APIErrorInternal(err) return @@ -271,7 +271,7 @@ func CreateBranch(ctx *context.APIContext) { return } - br, err := convert.ToBranch(ctx, ctx.Repo.Repository, opt.BranchName, commit, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) + br, err := convert.ToBranch(ctx, ctx.Repo.Repository, opt.BranchName, commit, branchProtection, ctx.Doer, ctx.Repo.Permission.IsAdmin()) if err != nil { ctx.APIErrorInternal(err) return @@ -366,7 +366,7 @@ func ListBranches(ctx *context.APIContext) { } branchProtection := rules.GetFirstMatched(branches[i].Name) - apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i].Name, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) + apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i].Name, c, branchProtection, ctx.Doer, ctx.Repo.Permission.IsAdmin()) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go index f8d61ccf000..6b2c7627d09 100644 --- a/routers/api/v1/repo/hook_test.go +++ b/routers/api/v1/repo/hook_test.go @@ -5,8 +5,10 @@ package repo import ( "net/http" + "strconv" "testing" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/services/contexttest" @@ -17,8 +19,17 @@ import ( func TestTestHook(t *testing.T) { unittest.PrepareTestEnv(t) + hook := &webhook.Webhook{ + RepoID: 1, + URL: "https://www.example.com/test_hook", + ContentType: webhook.ContentTypeJSON, + Events: `{"push_only":true}`, + IsActive: true, + } + assert.NoError(t, db.Insert(t.Context(), hook)) + ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/wiki/_pages") - ctx.SetPathParam("id", "1") + ctx.SetPathParam("id", strconv.FormatInt(hook.ID, 10)) contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -26,6 +37,6 @@ func TestTestHook(t *testing.T) { assert.Equal(t, http.StatusNoContent, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{ - HookID: 1, + HookID: hook.ID, }, unittest.Cond("is_delivered=?", false)) } diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 20ccd099a47..f8c1c67f067 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -442,14 +442,14 @@ func ListIssues(ctx *context.APIContext) { isPull = optional.Some(false) } - if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) { + if isPull.Has() && !ctx.Repo.Permission.CanReadIssuesOrPulls(isPull.Value()) { ctx.APIErrorNotFound() return } if !isPull.Has() { - canReadIssues := ctx.Repo.CanRead(unit.TypeIssues) - canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests) + canReadIssues := ctx.Repo.Permission.CanRead(unit.TypeIssues) + canReadPulls := ctx.Repo.Permission.CanRead(unit.TypePullRequests) if !canReadIssues && !canReadPulls { ctx.APIErrorNotFound() return @@ -591,7 +591,7 @@ func GetIssue(ctx *context.APIContext) { } return } - if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { + if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) { ctx.APIErrorNotFound() return } @@ -638,7 +638,7 @@ func CreateIssue(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateIssueOption) var deadlineUnix timeutil.TimeStamp - if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) { + if form.Deadline != nil && ctx.Repo.Permission.CanWrite(unit.TypeIssues) { deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix()) } @@ -655,7 +655,7 @@ func CreateIssue(ctx *context.APIContext) { assigneeIDs := make([]int64, 0) var err error - if ctx.Repo.CanWrite(unit.TypeIssues) { + if ctx.Repo.Permission.CanWrite(unit.TypeIssues) { issue.MilestoneID = form.Milestone assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees) if err != nil { @@ -775,7 +775,7 @@ func EditIssue(ctx *context.APIContext) { return } issue.Repo = ctx.Repo.Repository - canWrite := ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) + canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) err = issue.LoadAttributes(ctx) if err != nil { @@ -1020,7 +1020,7 @@ func UpdateIssueDeadline(ctx *context.APIContext) { return } - if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { ctx.APIError(http.StatusForbidden, "Not repo writer") return } diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go index b64f7134015..b6db388a221 100644 --- a/routers/api/v1/repo/issue_attachment.go +++ b/routers/api/v1/repo/issue_attachment.go @@ -371,7 +371,7 @@ func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Iss } func canUserWriteIssueAttachment(ctx *context.APIContext, issue *issues_model.Issue) bool { - canEditIssue := ctx.IsSigned && (ctx.Doer.ID == issue.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) + canEditIssue := ctx.IsSigned && (ctx.Doer.ID == issue.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin() || ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)) if !canEditIssue { ctx.APIError(http.StatusForbidden, "user should have permission to write issue") return false diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 091fe6998c5..5d79b2ec5ad 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -73,7 +73,7 @@ func ListIssueComments(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } - if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { + if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) { ctx.APIErrorNotFound() return } @@ -279,8 +279,8 @@ func ListRepoIssueComments(ctx *context.APIContext) { } var isPull optional.Option[bool] - canReadIssue := ctx.Repo.CanRead(unit.TypeIssues) - canReadPull := ctx.Repo.CanRead(unit.TypePullRequests) + canReadIssue := ctx.Repo.Permission.CanRead(unit.TypeIssues) + canReadPull := ctx.Repo.Permission.CanRead(unit.TypePullRequests) if canReadIssue && canReadPull { isPull = optional.None[bool]() } else if canReadIssue { @@ -386,12 +386,12 @@ func CreateIssueComment(ctx *context.APIContext) { return } - if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { + if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) { ctx.APIErrorNotFound() return } - if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { + if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { ctx.APIError(http.StatusForbidden, errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked"))) return } @@ -455,7 +455,7 @@ func GetIssueComment(ctx *context.APIContext) { return } - if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { + if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) { ctx.APIErrorNotFound() return } @@ -580,7 +580,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) return } - if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { + if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { ctx.Status(http.StatusForbidden) return } @@ -689,7 +689,7 @@ func deleteIssueComment(ctx *context.APIContext) { return } - if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { + if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { ctx.Status(http.StatusForbidden) return } else if !comment.Type.HasContentSupport() { diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go index 30b79a1d548..9a1ce00f16b 100644 --- a/routers/api/v1/repo/issue_comment_attachment.go +++ b/routers/api/v1/repo/issue_comment_attachment.go @@ -358,7 +358,7 @@ func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment { return nil } - if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { + if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) { return nil } @@ -379,7 +379,7 @@ func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Att } func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool { - canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) + canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull) if !canEditComment { ctx.APIError(http.StatusForbidden, "user should have permission to edit comment") return false diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index d5eee2d469b..1ac545f41b6 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -173,7 +173,7 @@ func DeleteIssueLabel(ctx *context.APIContext) { return } - if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { ctx.Status(http.StatusForbidden) return } @@ -295,7 +295,7 @@ func ClearIssueLabels(ctx *context.APIContext) { return } - if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { ctx.Status(http.StatusForbidden) return } @@ -319,7 +319,7 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) return nil, nil, err } - if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { ctx.APIError(http.StatusForbidden, "write permission is required") return nil, nil, errors.New("permission denied") } diff --git a/routers/api/v1/repo/issue_lock.go b/routers/api/v1/repo/issue_lock.go index b9e5bcf6eba..2f797a162ff 100644 --- a/routers/api/v1/repo/issue_lock.go +++ b/routers/api/v1/repo/issue_lock.go @@ -62,7 +62,7 @@ func LockIssue(ctx *context.APIContext) { return } - if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { ctx.APIError(http.StatusForbidden, errors.New("no permission to lock this issue")) return } @@ -129,7 +129,7 @@ func UnlockIssue(ctx *context.APIContext) { return } - if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { ctx.APIError(http.StatusForbidden, errors.New("no permission to unlock this issue")) return } diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index 1f313acde8c..2c9efd91112 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -71,7 +71,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) { return } - if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { + if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) { ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions")) return } @@ -208,12 +208,12 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp return } - if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { + if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) { ctx.APIErrorNotFound() return } - if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) { + if comment.Issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull) { ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction")) return } @@ -304,7 +304,7 @@ func GetIssueReactions(ctx *context.APIContext) { return } - if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { + if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) { ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions")) return } @@ -428,7 +428,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i return } - if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction")) return } diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go index f9fbff091d9..8818ab29727 100644 --- a/routers/api/v1/repo/issue_stopwatch.go +++ b/routers/api/v1/repo/issue_stopwatch.go @@ -178,7 +178,7 @@ func prepareIssueForStopwatch(ctx *context.APIContext) *issues_model.Issue { return nil } - if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { ctx.Status(http.StatusForbidden) return nil } diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index 9355177fce6..dc99cf8c162 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -79,11 +79,6 @@ func Migrate(ctx *context.APIContext) { return } - if ctx.HasAPIError() { - ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg()) - return - } - if !ctx.Doer.IsAdmin { if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID { ctx.APIError(http.StatusForbidden, "Given user is not an organization.") diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index 4370eeb5fac..ac2d8bba06a 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -51,7 +51,7 @@ func MirrorSync(ctx *context.APIContext) { repo := ctx.Repo.Repository - if !ctx.Repo.CanWrite(unit.TypeCode) { + if !ctx.Repo.Permission.CanWrite(unit.TypeCode) { ctx.APIError(http.StatusForbidden, "Must have write access") } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index ef8cc6cd932..aeecc13f4ef 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -653,7 +653,7 @@ func EditPullRequest(ctx *context.APIContext) { return } - if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) { + if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.Permission.CanWrite(unit.TypePullRequests) { ctx.Status(http.StatusForbidden) return } @@ -715,7 +715,7 @@ func EditPullRequest(ctx *context.APIContext) { // Pass one or more user logins to replace the set of assignees on this Issue. // Send an empty array ([]) to clear all assignees from the Issue. - if ctx.Repo.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) { + if ctx.Repo.Permission.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) { err = issue_service.UpdateAssignees(ctx, issue, form.Assignee, form.Assignees, ctx.Doer) if err != nil { if user_model.IsErrUserNotExist(err) { @@ -729,7 +729,7 @@ func EditPullRequest(ctx *context.APIContext) { } } - if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Milestone != 0 && + if ctx.Repo.Permission.CanWrite(unit.TypePullRequests) && form.Milestone != 0 && issue.MilestoneID != form.Milestone { oldMilestoneID := issue.MilestoneID issue.MilestoneID = form.Milestone @@ -744,7 +744,7 @@ func EditPullRequest(ctx *context.APIContext) { } } - if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Labels != nil { + if ctx.Repo.Permission.CanWrite(unit.TypePullRequests) && form.Labels != nil { labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels) if err != nil { ctx.APIErrorInternal(err) @@ -965,7 +965,7 @@ func MergePullRequest(ctx *context.APIContext) { } // start with merging by checking - if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { + if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, repo_model.MergeStyle(form.Do), form.ForceMerge); err != nil { if errors.Is(err, pull_service.ErrIsClosed) { ctx.APIErrorNotFound() } else if errors.Is(err, pull_service.ErrNoPermissionToMerge) { @@ -980,6 +980,8 @@ func MergePullRequest(ctx *context.APIContext) { ctx.APIError(http.StatusMethodNotAllowed, err) } else if asymkey_service.IsErrWontSign(err) { ctx.APIError(http.StatusMethodNotAllowed, err) + } else if errors.Is(err, pull_service.ErrHeadCommitsNotAllVerified) { + ctx.APIError(http.StatusMethodNotAllowed, err) } else { ctx.APIErrorInternal(err) } @@ -1173,7 +1175,7 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git return nil, nil } - return compareInfo, closer + return &compareInfo, closer } // UpdatePullRequest merge PR's baseBranch into headBranch @@ -1417,7 +1419,6 @@ func GetPullRequestCommits(ctx *context.APIContext) { return } - var compareInfo *git_service.CompareInfo baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo) if err != nil { ctx.APIErrorInternal(err) @@ -1425,6 +1426,7 @@ func GetPullRequestCommits(ctx *context.APIContext) { } defer closer.Close() + var compareInfo git_service.CompareInfo if pr.HasMerged { compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false) } else { @@ -1550,7 +1552,7 @@ func GetPullRequestFiles(ctx *context.APIContext) { baseGitRepo := ctx.Repo.GitRepo - var compareInfo *git_service.CompareInfo + var compareInfo git_service.CompareInfo if pr.HasMerged { compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false) } else { diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 1e3a05212d2..a049a61aa9e 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -208,6 +208,88 @@ func GetPullReviewComments(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiComments) } +// CreatePullReviewCommentReply replies to a pull request review comment. +// The URL mirrors GitHub's endpoint, {index} is verified against the parent comment's pull request. +func CreatePullReviewCommentReply(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/comments/{id}/replies repository repoCreatePullReviewCommentReply + // --- + // summary: Reply to a pull request review comment + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the pull request + // type: integer + // format: int64 + // required: true + // - name: id + // in: path + // description: id of the review comment to reply to + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/CreatePullReviewCommentReplyOptions" + // responses: + // "201": + // "$ref": "#/responses/PullReviewComment" + // "400": + // "$ref": "#/responses/validationError" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + opts := web.GetForm(ctx).(*api.CreatePullReviewCommentReplyOptions) + + parent := getPullReviewCommentToResolve(ctx) + if parent == nil { + return + } + if parent.Issue.Index != ctx.PathParamInt64("index") { + ctx.APIErrorNotFound() + return + } + if parent.ReviewID == 0 { + ctx.APIError(http.StatusBadRequest, "comment is not a review comment") + return + } + + comment, err := pull_service.CreateCodeComment(ctx, + ctx.Doer, ctx.Repo.GitRepo, parent.Issue, + parent.Line, opts.Body, parent.TreePath, + false, parent.ReviewID, + "", nil, + ) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if err := comment.LoadPoster(ctx); err != nil { + ctx.APIErrorInternal(err) + return + } + comment.Issue = parent.Issue + + ctx.JSON(http.StatusCreated, convert.ToPullReviewComment(ctx, comment, ctx.Doer)) +} + // ResolvePullReviewComment resolves a review comment in a pull request func ResolvePullReviewComment(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/pulls/comments/{id}/resolve repository repoResolvePullReviewComment @@ -392,7 +474,7 @@ func DeletePullReview(ctx *context.APIContext) { func CreatePullReview(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews repository repoCreatePullReview // --- - // summary: Create a review to an pull request + // summary: Create a review to a pull request // produces: // - application/json // parameters: @@ -509,11 +591,11 @@ func CreatePullReview(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiReview) } -// SubmitPullReview submit a pending review to an pull request +// SubmitPullReview submit a pending review to a pull request func SubmitPullReview(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoSubmitPullReview // --- - // summary: Submit a pending review to an pull request + // summary: Submit a pending review to a pull request // produces: // - application/json // parameters: @@ -693,7 +775,7 @@ func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues return review, pr, false } -// CreateReviewRequests create review requests to an pull request +// CreateReviewRequests create review requests to a pull request func CreateReviewRequests(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoCreatePullReviewRequests // --- @@ -734,7 +816,7 @@ func CreateReviewRequests(ctx *context.APIContext) { apiReviewRequest(ctx, *opts, true) } -// DeleteReviewRequests delete review requests to an pull request +// DeleteReviewRequests delete review requests to a pull request func DeleteReviewRequests(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoDeletePullReviewRequests // --- @@ -1003,7 +1085,7 @@ func UnDismissPullReview(ctx *context.APIContext) { } func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) { - if !ctx.Repo.IsAdmin() { + if !ctx.Repo.Permission.IsAdmin() { ctx.APIError(http.StatusForbidden, "Must be repo admin") return } diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index c87d22614e8..2eade9eab58 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -22,7 +22,7 @@ import ( ) func canAccessReleaseDraft(ctx *context.APIContext) bool { - if !ctx.IsSigned || !ctx.Repo.CanWrite(unit.TypeReleases) { + if !ctx.IsSigned || !ctx.Repo.Permission.CanWrite(unit.TypeReleases) { return false } if ctx.Data["IsApiToken"] != true { diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go index 8991e201d8b..bca5871aa74 100644 --- a/routers/api/v1/repo/release_tags.go +++ b/routers/api/v1/repo/release_tags.go @@ -60,7 +60,7 @@ func GetReleaseByTag(ctx *context.APIContext) { } if release.IsDraft { // only the users with write access can see draft releases - if !ctx.IsSigned || !ctx.Repo.CanWrite(unit_model.TypeReleases) { + if !ctx.IsSigned || !ctx.Repo.Permission.CanWrite(unit_model.TypeReleases) { ctx.APIErrorNotFound() return } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 4a5091fded2..24f486be9db 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -758,7 +758,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { if opts.HasIssues != nil { if *opts.HasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { // Check that values are valid - if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) { + if !validation.IsValidURL(opts.ExternalTracker.ExternalTrackerURL) { err := errors.New("External tracker URL not valid") ctx.APIError(http.StatusUnprocessableEntity, err) return err @@ -820,7 +820,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { if opts.HasWiki != nil { if *opts.HasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { // Check that values are valid - if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) { + if !validation.IsValidURL(opts.ExternalWiki.ExternalWikiURL) { err := errors.New("External wiki URL not valid") ctx.APIError(http.StatusUnprocessableEntity, "Invalid external wiki URL") return err diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go index 739a9e3892b..cb0f026933e 100644 --- a/routers/api/v1/repo/teams.go +++ b/routers/api/v1/repo/teams.go @@ -187,7 +187,7 @@ func changeRepoTeam(ctx *context.APIContext, add bool) { if !ctx.Repo.Owner.IsOrganization() { ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization") } - if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { + if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.Permission.IsOwner() { ctx.APIError(http.StatusForbidden, "user is nor repo admin nor owner") return } diff --git a/routers/api/v1/shared/action.go b/routers/api/v1/shared/action.go index 715e76c3557..1b12023d7a0 100644 --- a/routers/api/v1/shared/action.go +++ b/routers/api/v1/shared/action.go @@ -12,6 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/webhook" @@ -27,8 +28,9 @@ import ( // ownerID != 0 and repoID != 0 undefined behavior // runID == 0 means all jobs // runID is used as an additional filter together with ownerID and repoID to only return jobs for the given run +// runAttemptID, when set, additionally limits the result to jobs of the specified run attempt. Only takes effect when runID > 0. // Access rights are checked at the API route level -func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) { +func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64, runAttemptID optional.Option[int64]) { if ownerID != 0 && repoID != 0 { setting.PanicInDevOrTesting("ownerID and repoID should not be both set") } @@ -39,6 +41,9 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) { RunID: runID, ListOptions: listOptions, } + if runID > 0 { + opts.RunAttemptID = runAttemptID + } for _, status := range ctx.FormStrings("status") { values, err := convertToInternal(status) if err != nil { @@ -178,7 +183,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) { } } - convertedRun, err := convert.ToActionWorkflowRun(ctx, repository, runs[i]) + convertedRun, err := convert.ToActionWorkflowRun(ctx, repository, runs[i], nil) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/api/v1/shared/block.go b/routers/api/v1/shared/block.go index 5762c5abf17..dfffe72bf06 100644 --- a/routers/api/v1/shared/block.go +++ b/routers/api/v1/shared/block.go @@ -9,6 +9,7 @@ import ( user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -48,17 +49,14 @@ func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) { return } - status := http.StatusNotFound - blocking, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID) - if err != nil { + _, err = user_model.GetBlocking(ctx, blocker.ID, blockee.ID) + if errors.Is(err, util.ErrNotExist) { + ctx.Status(http.StatusNotFound) + } else if err == nil { + ctx.Status(http.StatusNoContent) + } else { ctx.APIErrorInternal(err) - return } - if blocking != nil { - status = http.StatusNoContent - } - - ctx.Status(status) } func BlockUser(ctx *context.APIContext, blocker *user_model.User) { diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index f66cef61df2..1a442d11466 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -168,6 +168,9 @@ type swaggerParameterBodies struct { // in:body CreatePullReviewComment api.CreatePullReviewComment + // in:body + CreatePullReviewCommentReplyOptions api.CreatePullReviewCommentReplyOptions + // in:body SubmitPullReviewOptions api.SubmitPullReviewOptions diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index 573e2e4dd08..4de0b30d983 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -439,5 +439,5 @@ func ListWorkflowJobs(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - shared.ListJobs(ctx, ctx.Doer.ID, 0, 0) + shared.ListJobs(ctx, ctx.Doer.ID, 0, 0, nil) } diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index 6f1053e7ac9..474680adec5 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" + "code.gitea.io/gitea/services/forms" ) // ListAccessTokens list all the access tokens @@ -228,7 +229,10 @@ func CreateOauth2Application(ctx *context.APIContext) { // "$ref": "#/responses/error" data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) - + if invalidURI := forms.DetectInvalidOAuth2ApplicationRedirectURI(data.RedirectURIs); invalidURI != "" { + ctx.APIError(http.StatusBadRequest, "invalid redirect URI: "+invalidURI) + return + } app, err := auth_model.CreateOAuth2Application(ctx, auth_model.CreateOAuth2ApplicationOptions{ Name: data.Name, UserID: ctx.Doer.ID, @@ -382,11 +386,17 @@ func UpdateOauth2Application(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/OAuth2Application" + // "400": + // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" appID := ctx.PathParamInt64("id") data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) + if invalidURI := forms.DetectInvalidOAuth2ApplicationRedirectURI(data.RedirectURIs); invalidURI != "" { + ctx.APIError(http.StatusBadRequest, "invalid redirect URI: "+invalidURI) + return + } app, err := auth_model.UpdateOAuth2Application(ctx, auth_model.UpdateOAuth2ApplicationOptions{ Name: data.Name, diff --git a/routers/api/v1/user/avatar.go b/routers/api/v1/user/avatar.go index 9c7bd57bc06..8f39319112f 100644 --- a/routers/api/v1/user/avatar.go +++ b/routers/api/v1/user/avatar.go @@ -13,7 +13,7 @@ import ( user_service "code.gitea.io/gitea/services/user" ) -// UpdateAvatar updates the Avatar of an User +// UpdateAvatar updates the Avatar of a User func UpdateAvatar(ctx *context.APIContext) { // swagger:operation POST /user/avatar user userUpdateAvatar // --- @@ -45,7 +45,7 @@ func UpdateAvatar(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } -// DeleteAvatar deletes the Avatar of an User +// DeleteAvatar deletes the Avatar of a User func DeleteAvatar(ctx *context.APIContext) { // swagger:operation DELETE /user/avatar user userDeleteAvatar // --- diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index 9ec4d2c938a..39ded31bf48 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -281,11 +281,7 @@ func DeleteGPGKey(ctx *context.APIContext) { } if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.PathParamInt64("id")); err != nil { - if asymkey_model.IsErrGPGKeyAccessDenied(err) { - ctx.APIError(http.StatusForbidden, "You do not have access to this key") - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorInternal(err) return } @@ -295,8 +291,6 @@ func DeleteGPGKey(ctx *context.APIContext) { // HandleAddGPGKeyError handle add GPGKey error func HandleAddGPGKeyError(ctx *context.APIContext, err error, token string) { switch { - case asymkey_model.IsErrGPGKeyAccessDenied(err): - ctx.APIError(http.StatusUnprocessableEntity, "You do not have access to this GPG key") case asymkey_model.IsErrGPGKeyIDAlreadyUsed(err): ctx.APIError(http.StatusUnprocessableEntity, "A key with the same id already exists") case asymkey_model.IsErrGPGKeyParsing(err): diff --git a/routers/api/v1/user/runners.go b/routers/api/v1/user/runners.go index e06b022f356..6db1d48069f 100644 --- a/routers/api/v1/user/runners.go +++ b/routers/api/v1/user/runners.go @@ -14,7 +14,7 @@ import ( func CreateRegistrationToken(ctx *context.APIContext) { // swagger:operation POST /user/actions/runners/registration-token user userCreateRunnerRegistrationToken // --- - // summary: Get an user's actions runner registration token + // summary: Get a user's actions runner registration token // produces: // - application/json // parameters: diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index bbada746b7e..8dc19b63a8f 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -48,7 +48,7 @@ func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) { ctx.JSON(http.StatusOK, apiHooks) } -// GetOwnerHook gets an user or organization webhook. Errors are written to ctx. +// GetOwnerHook gets a user or organization webhook. Errors are written to ctx. func GetOwnerHook(ctx *context.APIContext, ownerID, hookID int64) (*webhook.Webhook, error) { w, err := webhook.GetWebhookByOwnerID(ctx, ownerID, hookID) if err != nil { @@ -114,7 +114,7 @@ func AddSystemHook(ctx *context.APIContext, form *api.CreateHookOption) { } } -// AddOwnerHook adds a hook to an user or organization +// AddOwnerHook adds a hook to a user or organization func AddOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.CreateHookOption) { hook, ok := addHook(ctx, form, owner.ID, 0) if !ok { @@ -294,7 +294,7 @@ func EditSystemHook(ctx *context.APIContext, form *api.EditHookOption, hookID in ctx.JSON(http.StatusOK, h) } -// EditOwnerHook updates a webhook of an user or organization +// EditOwnerHook updates a webhook of a user or organization func EditOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.EditHookOption, hookID int64) { hook, err := GetOwnerHook(ctx, owner.ID, hookID) if err != nil { diff --git a/routers/common/actions.go b/routers/common/actions.go index 4eb7078db67..2b83e5d8423 100644 --- a/routers/common/actions.go +++ b/routers/common/actions.go @@ -31,7 +31,8 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository return util.NewNotExistErrorf("job not found") } - if curJob.TaskID == 0 { + taskID := curJob.EffectiveTaskID() + if taskID == 0 { return util.NewNotExistErrorf("job not started") } @@ -39,7 +40,7 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository return fmt.Errorf("LoadRun: %w", err) } - task, err := actions_model.GetTaskByID(ctx, curJob.TaskID) + task, err := actions_model.GetTaskByID(ctx, taskID) if err != nil { return fmt.Errorf("GetTaskByID: %w", err) } diff --git a/routers/common/blockexpensive.go b/routers/common/blockexpensive.go index fec364351ca..0407264b1e4 100644 --- a/routers/common/blockexpensive.go +++ b/routers/common/blockexpensive.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/modules/web/routing" "github.com/go-chi/chi/v5" ) @@ -71,10 +72,6 @@ func isRoutePathExpensive(routePattern string) bool { return false } -func isRoutePathForLongPolling(routePattern string) bool { - return routePattern == "/user/events" -} - func determineRequestPriority(reqCtx reqctx.RequestContext) (ret struct { SignedIn bool Expensive bool @@ -86,7 +83,7 @@ func determineRequestPriority(reqCtx reqctx.RequestContext) (ret struct { ret.SignedIn = true } else { ret.Expensive = isRoutePathExpensive(chiRoutePath) - ret.LongPolling = isRoutePathForLongPolling(chiRoutePath) + ret.LongPolling = routing.GetRequestRecordInfo(reqCtx).IsLongPolling } return ret } diff --git a/routers/common/blockexpensive_test.go b/routers/common/blockexpensive_test.go index db5c0db7dda..6ee4af60e86 100644 --- a/routers/common/blockexpensive_test.go +++ b/routers/common/blockexpensive_test.go @@ -25,6 +25,4 @@ func TestBlockExpensive(t *testing.T) { for _, c := range cases { assert.Equal(t, c.expensive, isRoutePathExpensive(c.routePath), "routePath: %s", c.routePath) } - - assert.True(t, isRoutePathForLongPolling("/user/events")) } diff --git a/routers/common/errpage.go b/routers/common/errpage.go index 07760bcd18b..9baf7915e1c 100644 --- a/routers/common/errpage.go +++ b/routers/common/errpage.go @@ -33,10 +33,6 @@ func renderServerErrorPage(w http.ResponseWriter, req *http.Request, respCode in } httpcache.SetCacheControlInHeader(w.Header(), &httpcache.CacheControlOptions{NoTransform: true}) - if setting.Security.XFrameOptions != "unset" { - w.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions) - } - tmplCtx := context.NewTemplateContextForWeb(reqctx.FromContext(req.Context()), req, middleware.Locale(w, req)) w.WriteHeader(respCode) diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 39911e25481..3932a84b6d7 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -28,6 +28,7 @@ func ProtocolMiddlewares() (handlers []any) { // the order is important handlers = append(handlers, ChiRoutePathHandler()) // make sure chi has correct paths handlers = append(handlers, RequestContextHandler()) // prepare the context and panic recovery + handlers = append(handlers, SecurityHeadersHandler()) if setting.ReverseProxyLimit > 0 && len(setting.ReverseProxyTrustedProxies) > 0 { handlers = append(handlers, ForwardedHeadersHandler(setting.ReverseProxyLimit, setting.ReverseProxyTrustedProxies)) @@ -48,6 +49,21 @@ func ProtocolMiddlewares() (handlers []any) { return handlers } +// SecurityHeadersHandler sets headers globally for every response that leaves Gitea. +func SecurityHeadersHandler() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + if setting.Security.XContentTypeOptions != "unset" { + resp.Header().Set("X-Content-Type-Options", setting.Security.XContentTypeOptions) + } + if setting.Security.XFrameOptions != "unset" { + resp.Header().Set("X-Frame-Options", setting.Security.XFrameOptions) + } + next.ServeHTTP(resp, req) + }) + } +} + func RequestContextHandler() func(h http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) { diff --git a/routers/common/qos.go b/routers/common/qos.go index 96f23b64fe6..fbde4192236 100644 --- a/routers/common/qos.go +++ b/routers/common/qos.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/modules/web/routing" "github.com/bohde/codel" "github.com/go-chi/chi/v5" @@ -68,7 +69,7 @@ func QoS() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ctx := req.Context() - + reqRecordInfo := routing.GetRequestRecordInfo(ctx) priority := requestPriority(ctx) // Check if the request can begin processing. @@ -79,9 +80,8 @@ func QoS() func(next http.Handler) http.Handler { return } - // Release long-polling immediately, so they don't always - // take up an in-flight request - if strings.Contains(req.URL.Path, "/user/events") { + // Release long-polling immediately, so they don't always take up an in-flight request + if reqRecordInfo.IsLongPolling { c.Release() } else { defer c.Release() diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index d705062b3d8..1baa0225217 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -230,7 +230,7 @@ func performAutoLoginOAuth2(ctx *context.Context, data *preparedSignInData) bool return false } - skipToOAuthURL := setting.AppSubURL + "/user/oauth2/" + url.QueryEscape(data.oauth2Providers[0].DisplayName()) + skipToOAuthURL := setting.AppSubURL + "/user/oauth2/" + url.PathEscape(data.oauth2Providers[0].DisplayName()) if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" { skipToOAuthURL += "?redirect_to=" + url.QueryEscape(redirectTo) } @@ -314,15 +314,6 @@ func SignInPost(ctx *context.Context) { log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err) ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") ctx.HTML(http.StatusOK, "user/auth/prohibit_login") - } else if user_model.IsErrUserInactive(err) { - if setting.Service.RegisterEmailConfirm { - ctx.Data["Title"] = ctx.Tr("auth.active_your_account") - ctx.HTML(http.StatusOK, TplActivate) - } else { - log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err) - ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") - ctx.HTML(http.StatusOK, "user/auth/prohibit_login") - } } else { ctx.ServerError("UserSignIn", err) } diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go index 943085a9635..a06e209e4f4 100644 --- a/routers/web/auth/auth_test.go +++ b/routers/web/auth/auth_test.go @@ -4,6 +4,7 @@ package auth import ( + "html/template" "net/http" "net/http/httptest" "net/url" @@ -67,15 +68,15 @@ func TestWebAuthOAuth2(t *testing.T) { defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() _ = oauth2.Init(t.Context()) - addOAuth2Source(t, "dummy-auth-source", oauth2.Source{}) + addOAuth2Source(t, "dummy+auth's source", oauth2.Source{}) t.Run("OAuth2MissingField", func(t *testing.T) { defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) { - return goth.User{Provider: "dummy-auth-source", UserID: "dummy-user"}, nil + return goth.User{Provider: "dummy+auth's source", UserID: "dummy-user"}, nil })() mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")} - ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt) - ctx.SetPathParam("provider", "dummy-auth-source") + ctx, resp := contexttest.MockContext(t, "/user/oauth2/..../callback?code=dummy-code", mockOpt) + ctx.SetPathParamRaw("provider", "dummy+auth%27s%20source") SignInOAuthCallback(ctx) assert.Equal(t, http.StatusSeeOther, resp.Code) assert.Equal(t, "/user/link_account", test.RedirectURL(resp)) @@ -83,13 +84,13 @@ func TestWebAuthOAuth2(t *testing.T) { // then the user will be redirected to the link account page, and see a message about the missing fields ctx, _ = contexttest.MockContext(t, "/user/link_account", mockOpt) LinkAccount(ctx) - assert.EqualValues(t, "auth.oauth_callback_unable_auto_reg:dummy-auth-source,email", ctx.Data["AutoRegistrationFailedPrompt"]) + assert.Equal(t, template.HTML("auth.oauth_callback_unable_auto_reg:dummy+auth's source,email"), ctx.Data["AutoRegistrationFailedPrompt"]) }) t.Run("OAuth2CallbackError", func(t *testing.T) { mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")} - ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback", mockOpt) - ctx.SetPathParam("provider", "dummy-auth-source") + ctx, resp := contexttest.MockContext(t, "/user/oauth2/...../callback", mockOpt) + ctx.SetPathParamRaw("provider", "dummy+auth%27s%20source") SignInOAuthCallback(ctx) assert.Equal(t, http.StatusSeeOther, resp.Code) assert.Equal(t, "/user/login", test.RedirectURL(resp)) @@ -112,8 +113,8 @@ func TestWebAuthOAuth2(t *testing.T) { assert.Equal(t, expectedRedirect, test.RedirectURL(resp)) } } - testSignIn(t, "/user/login", http.StatusSeeOther, "/user/oauth2/dummy-auth-source") - testSignIn(t, "/user/login?redirect_to=/", http.StatusSeeOther, "/user/oauth2/dummy-auth-source?redirect_to=%2F") + testSignIn(t, "/user/login", http.StatusSeeOther, "/user/oauth2/dummy+auth%27s%20source") + testSignIn(t, "/user/login?redirect_to=/", http.StatusSeeOther, "/user/oauth2/dummy+auth%27s%20source?redirect_to=%2F") *enablePassword, *enableOpenID, *enablePasskey = true, false, false testSignIn(t, "/user/login", http.StatusOK, "") diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index 02e1b7acd2f..ea3aa3f7cae 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -105,16 +105,6 @@ func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err) ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") ctx.HTML(http.StatusOK, "user/auth/prohibit_login") - } else if user_model.IsErrUserInactive(err) { - ctx.Data["user_exists"] = true - if setting.Service.RegisterEmailConfirm { - ctx.Data["Title"] = ctx.Tr("auth.active_your_account") - ctx.HTML(http.StatusOK, TplActivate) - } else { - log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err) - ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") - ctx.HTML(http.StatusOK, "user/auth/prohibit_login") - } } else { ctx.ServerError(invoker, err) } diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index fe12dc3079c..83e9bef9c8e 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -4,6 +4,7 @@ package devtest import ( + "fmt" mathRand "math/rand/v2" "net/http" "slices" @@ -12,7 +13,9 @@ import ( "time" actions_model "code.gitea.io/gitea/models/actions" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/repo/actions" @@ -36,6 +39,10 @@ func generateMockStepsLog(logCur actions.LogCursor, opts generateMockStepsLogOpt "##[group]test group for: step={step}, cursor={cursor}", "in group msg for: step={step}, cursor={cursor}", "##[endgroup]", + "::error::mock error for: step={step}, cursor={cursor}", + "::warning::mock warning for: step={step}, cursor={cursor}", + "::notice::mock notice for: step={step}, cursor={cursor}", + "::debug::mock debug for: step={step}, cursor={cursor}", ) // usually the cursor is the "file offset", but here we abuse it as "line number" to make the mock easier, intentionally cur := logCur.Cursor @@ -59,13 +66,18 @@ func generateMockStepsLog(logCur actions.LogCursor, opts generateMockStepsLogOpt } func MockActionsView(ctx *context.Context) { - ctx.Data["RunID"] = ctx.PathParamInt64("run") + if runID := ctx.PathParamInt64("run"); runID == 0 { + ctx.Redirect("/repo-action-view/runs/10") + return + } ctx.Data["JobID"] = ctx.PathParamInt64("job") + ctx.Data["ActionsViewURL"] = ctx.Req.URL.Path ctx.HTML(http.StatusOK, "devtest/repo-action-view") } func MockActionsRunsJobs(ctx *context.Context) { runID := ctx.PathParamInt64("run") + attemptID := ctx.PathParamInt64("attempt") alignTime := func(v, unit int64) int64 { return (v + unit) / unit * unit @@ -74,16 +86,9 @@ func MockActionsRunsJobs(ctx *context.Context) { resp.State.Run.RepoID = 12345 resp.State.Run.TitleHTML = `mock run title link` resp.State.Run.Link = setting.AppSubURL + "/devtest/repo-action-view/runs/" + strconv.FormatInt(runID, 10) - resp.State.Run.Status = actions_model.StatusRunning.String() - resp.State.Run.CanCancel = runID == 10 - resp.State.Run.CanApprove = runID == 20 - resp.State.Run.CanRerun = runID == 30 - resp.State.Run.CanRerunFailed = runID == 30 resp.State.Run.CanDeleteArtifact = true resp.State.Run.WorkflowID = "workflow-id" resp.State.Run.WorkflowLink = "./workflow-link" - resp.State.Run.Duration = "1h 23m 45s" - resp.State.Run.TriggeredAt = time.Now().Add(-time.Hour).Unix() resp.State.Run.TriggerEvent = "push" resp.State.Run.Commit = actions.ViewCommit{ ShortSha: "ccccdddd", @@ -98,6 +103,88 @@ func MockActionsRunsJobs(ctx *context.Context) { IsDeleted: false, }, } + now := time.Now() + currentAttemptNum := int64(1) + if attemptID > 0 { + currentAttemptNum = attemptID + } + user2 := &user_model.User{Name: "user2"} + user3 := &user_model.User{Name: "user3"} + attempts := []*actions_model.ActionRunAttempt{{ + Attempt: 1, + Status: actions_model.StatusSuccess, + Created: timeutil.TimeStamp(now.Add(-time.Hour).Unix()), + TriggerUserID: 2, + TriggerUser: user2, + }} + if runID == 10 { + attempts = []*actions_model.ActionRunAttempt{ + { + Attempt: 3, + Status: actions_model.StatusSuccess, + Created: timeutil.TimeStamp(alignTime(now.Add(-time.Hour).Unix(), 3600)), + TriggerUserID: 2, + TriggerUser: user2, + }, + { + Attempt: 2, + Status: actions_model.StatusFailure, + Created: timeutil.TimeStamp(alignTime(now.Add(-2*time.Hour).Unix(), 3600)), + TriggerUserID: 1, + TriggerUser: user3, + }, + { + Attempt: 1, + Status: actions_model.StatusSuccess, + Created: timeutil.TimeStamp(alignTime(now.Add(-3*time.Hour).Unix(), 3600)), + TriggerUserID: 2, + TriggerUser: user2, + }, + } + if attemptID == 0 { + currentAttemptNum = 3 + } + } + + latestAttempt := attempts[0] + resp.State.Run.RunAttempt = currentAttemptNum + resp.State.Run.Done = latestAttempt.Status.IsDone() + resp.State.Run.Status = latestAttempt.Status.String() + resp.State.Run.Duration = "1h 23m 45s" + resp.State.Run.TriggeredAt = latestAttempt.Created.AsTime().Unix() + resp.State.Run.ViewLink = resp.State.Run.Link + for _, attempt := range attempts { + link := resp.State.Run.Link + if attempt.Attempt != latestAttempt.Attempt { + link = fmt.Sprintf("%s/attempts/%d", resp.State.Run.Link, attempt.Attempt) + } + current := attempt.Attempt == currentAttemptNum + if current { + resp.State.Run.Status = attempt.Status.String() + resp.State.Run.Done = attempt.Status.IsDone() + resp.State.Run.TriggeredAt = attempt.Created.AsTime().Unix() + if attempt.Attempt != latestAttempt.Attempt { + resp.State.Run.ViewLink = link + } + } + resp.State.Run.Attempts = append(resp.State.Run.Attempts, &actions.ViewRunAttempt{ + Attempt: attempt.Attempt, + Status: attempt.Status.String(), + Done: attempt.Status.IsDone(), + Link: link, + Current: current, + Latest: attempt.Attempt == latestAttempt.Attempt, + TriggeredAt: attempt.Created.AsTime().Unix(), + TriggerUserName: attempt.TriggerUser.GetDisplayName(), + TriggerUserLink: attempt.TriggerUser.HomeLink(), + }) + } + isLatestAttempt := currentAttemptNum == latestAttempt.Attempt + resp.State.Run.CanCancel = runID == 10 && isLatestAttempt + resp.State.Run.CanApprove = runID == 20 && isLatestAttempt + resp.State.Run.CanRerun = runID == 30 && isLatestAttempt + resp.State.Run.CanRerunFailed = runID == 30 && isLatestAttempt + resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ Name: "artifact-a", Size: 100 * 1024, @@ -123,16 +210,22 @@ func MockActionsRunsJobs(ctx *context.Context) { ExpiresUnix: 0, }) + jobLink := func(jobID int64) string { + return fmt.Sprintf("%s/jobs/%d", resp.State.Run.Link, jobID) + } + resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ ID: runID * 10, + Link: jobLink(runID * 10), JobID: "job-100", - Name: "job 100", + Name: "job 100 (testsubname)", Status: actions_model.StatusRunning.String(), CanRerun: true, - Duration: "1h", + Duration: "1h23m45s", }) resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ ID: runID*10 + 1, + Link: jobLink(runID*10 + 1), JobID: "job-101", Name: "job 101", Status: actions_model.StatusWaiting.String(), @@ -142,6 +235,7 @@ func MockActionsRunsJobs(ctx *context.Context) { }) resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ ID: runID*10 + 2, + Link: jobLink(runID*10 + 2), JobID: "job-102", Name: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit", Status: actions_model.StatusFailure.String(), @@ -151,6 +245,7 @@ func MockActionsRunsJobs(ctx *context.Context) { }) resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ ID: runID*10 + 3, + Link: jobLink(runID*10 + 3), JobID: "job-103", Name: "job 103", Status: actions_model.StatusCancelled.String(), @@ -162,8 +257,10 @@ func MockActionsRunsJobs(ctx *context.Context) { // add more jobs to a run for UI testing if resp.State.Run.CanCancel { for i := range 10 { + jobID := runID*1000 + int64(i) resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ - ID: runID*1000 + int64(i), + ID: jobID, + Link: jobLink(jobID), JobID: "job-dup-test-" + strconv.Itoa(i), Name: "job dup test " + strconv.Itoa(i), Status: actions_model.StatusSuccess.String(), @@ -184,6 +281,14 @@ func fillViewRunResponseCurrentJob(ctx *context.Context, resp *actions.ViewRespo return } + for _, job := range resp.State.Run.Jobs { + if job.ID == jobID { + resp.State.CurrentJob.Title = job.Name + resp.State.CurrentJob.Detail = job.Status + break + } + } + req := web.GetForm(ctx).(*actions.ViewRequest) var mockLogOptions []generateMockStepsLogOptions resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{ diff --git a/routers/web/home.go b/routers/web/home.go index 7efa5f344e3..d14a7bab13f 100644 --- a/routers/web/home.go +++ b/routers/web/home.go @@ -109,9 +109,3 @@ func HomeSitemap(ctx *context.Context) { log.Error("Failed writing sitemap: %v", err) } } - -// NotFound render 404 page -func NotFound(ctx *context.Context) { - ctx.Data["Title"] = "Page Not Found" - ctx.NotFound(nil) -} diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go index a50d9130ac2..0b939ee4352 100644 --- a/routers/web/misc/misc.go +++ b/routers/web/misc/misc.go @@ -7,9 +7,12 @@ import ( "net/http" "path" "strconv" + "strings" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -17,6 +20,29 @@ import ( "code.gitea.io/gitea/services/context" ) +func SiteManifest(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/manifest+json") + if httpcache.HandleGenericETagPublicCache(req, w, "", &setting.AppStartTime) { + return + } + if req.Method == http.MethodHead { + return + } + + ctx := req.Context() + absoluteAssetURL := strings.TrimSuffix(httplib.MakeAbsoluteURL(ctx, setting.StaticURLPrefix), "/") + manifest := map[string]any{ + "name": setting.AppName, + "short_name": setting.AppName, + "start_url": httplib.GuessCurrentAppURL(ctx), + "icons": []map[string]string{ + {"src": absoluteAssetURL + "/assets/img/logo.png", "type": "image/png", "sizes": "512x512"}, + {"src": absoluteAssetURL + "/assets/img/logo.svg", "type": "image/svg+xml", "sizes": "512x512"}, + }, + } + _ = json.NewEncoder(w).Encode(manifest) +} + func SSHInfo(rw http.ResponseWriter, req *http.Request) { if !git.DefaultFeatures().SupportProcReceive { rw.WriteHeader(http.StatusNotFound) diff --git a/routers/web/org/block.go b/routers/web/org/block.go index 60f722dd392..e728e4dce5a 100644 --- a/routers/web/org/block.go +++ b/routers/web/org/block.go @@ -34,15 +34,5 @@ func BlockedUsers(ctx *context.Context) { } func BlockedUsersPost(ctx *context.Context) { - if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { - ctx.ServerError("RenderUserOrgHeader", err) - return - } - - shared_user.BlockedUsersPost(ctx, ctx.ContextUser) - if ctx.Written() { - return - } - - ctx.Redirect(ctx.ContextUser.OrganisationLink() + "/settings/blocked_users") + shared_user.BlockedUsersPost(ctx, ctx.ContextUser, ctx.ContextUser.OrganisationLink()+"/settings/blocked_users") } diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 4cdf81c1559..ae32be05757 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" - org_model "code.gitea.io/gitea/models/organization" project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" @@ -35,14 +34,6 @@ const ( tplProjectsView templates.TplName = "org/projects/view" ) -// MustEnableProjects check if projects are enabled in settings -func MustEnableProjects(ctx *context.Context) { - if unit.TypeProjects.UnitGlobalDisabled() { - ctx.NotFound(nil) - return - } -} - // Projects renders the home page of projects func Projects(ctx *context.Context) { if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { @@ -459,9 +450,9 @@ func ViewProject(ctx *context.Context) { ctx.Data["MilestoneID"] = milestoneID // Get assignees. - assigneeUsers, err := org_model.GetOrgAssignees(ctx, project.OwnerID) + assigneeUsers, err := project_service.LoadIssuesAssigneesForProject(ctx, issuesMap) if err != nil { - ctx.ServerError("GetRepoAssignees", err) + ctx.ServerError("LoadIssuesAssigneesForProject", err) return } ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 644a53f28a0..1e9f596fc40 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -57,7 +57,7 @@ func MustEnableActions(ctx *context.Context) { } if ctx.Repo.Repository != nil { - if !ctx.Repo.CanRead(unit.TypeActions) { + if !ctx.Repo.Permission.CanRead(unit.TypeActions) { ctx.NotFound(nil) return } @@ -181,7 +181,7 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow ctx.Data["workflows"] = workflows ctx.Data["RepoLink"] = ctx.Repo.Repository.Link() - ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin() + ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.Permission.IsAdmin() actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() ctx.Data["ActionsConfig"] = actionsConfig ctx.Data["CurWorkflow"] = curWorkflowID @@ -192,7 +192,7 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo, curWorkflowID string) { actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() - if curWorkflowID == "" || !ctx.Repo.CanWrite(unit.TypeActions) || actionsConfig.IsWorkflowDisabled(curWorkflowID) { + if curWorkflowID == "" || !ctx.Repo.Permission.CanWrite(unit.TypeActions) || actionsConfig.IsWorkflowDisabled(curWorkflowID) { return } @@ -311,7 +311,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) { if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning) { continue } - jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID) if err != nil { ctx.ServerError("GetRunJobsByRunID", err) return @@ -355,7 +355,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) { ctx.Data["Page"] = pager ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 - ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.CanWrite(unit.TypeActions) + ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.Permission.CanWrite(unit.TypeActions) } // loadIsRefDeleted loads the IsRefDeleted field for each run in the list. diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index fb4dfa9603d..e17d6b42d7a 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -34,7 +34,6 @@ import ( "code.gitea.io/gitea/routers/common" actions_service "code.gitea.io/gitea/services/actions" context_module "code.gitea.io/gitea/services/context" - notify_service "code.gitea.io/gitea/services/notify" "github.com/nektos/act/pkg/model" ) @@ -166,7 +165,7 @@ func resolveCurrentRunForView(ctx *context_module.Context) *actions_model.Action return nil } if run != nil { - jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID) if err != nil { ctx.ServerError("GetRunJobsByRunID", err) return nil @@ -203,9 +202,23 @@ func View(ctx *context_module.Context) { if ctx.Written() { return } - ctx.Data["RunID"] = run.ID - ctx.Data["JobID"] = ctx.PathParamInt64("job") // it can be 0 when no job (e.g.: run summary view) - ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions" + run.Repo = ctx.Repo.Repository + + jobID := ctx.PathParamInt64("job") + ctx.Data["JobID"] = jobID // it can be 0 when no job (e.g.: run summary view) + + attemptNum := ctx.PathParamInt64("attempt") + + // ActionsViewURL is the endpoint for viewing a run (job summary), a job, or a job attempt. + // It's POST method handler can provide the state data for the frontend rendering. + switch { + case attemptNum > 0: + ctx.Data["ActionsViewURL"] = fmt.Sprintf("%s/attempts/%d", run.Link(), attemptNum) + case jobID > 0: + ctx.Data["ActionsViewURL"] = fmt.Sprintf("%s/jobs/%d", run.Link(), jobID) + default: + ctx.Data["ActionsViewURL"] = run.Link() + } ctx.HTML(http.StatusOK, tplViewActions) } @@ -259,22 +272,30 @@ type ViewResponse struct { State struct { Run struct { - RepoID int64 `json:"repoId"` - Link string `json:"link"` - Title string `json:"title"` - TitleHTML template.HTML `json:"titleHTML"` - Status string `json:"status"` - CanCancel bool `json:"canCancel"` - CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve - CanRerun bool `json:"canRerun"` - CanRerunFailed bool `json:"canRerunFailed"` - CanDeleteArtifact bool `json:"canDeleteArtifact"` - Done bool `json:"done"` - WorkflowID string `json:"workflowID"` - WorkflowLink string `json:"workflowLink"` - IsSchedule bool `json:"isSchedule"` - Jobs []*ViewJob `json:"jobs"` - Commit ViewCommit `json:"commit"` + RepoID int64 `json:"repoId"` + // Link is the canonical HTML URL of the run, e.g. "/owner/repo/actions/runs/123". + // Used as the base for composing sub-resource URLs (cancel, rerun, artifacts, jobs) that are not attempt-scoped. + Link string `json:"link"` + // ViewLink is the attempt-aware URL for navigation, e.g. "/owner/repo/actions/runs/123" for the latest attempt + // or "/owner/repo/actions/runs/123/attempts/2" for a historical attempt. + // Use this when the target should reflect the currently-viewed attempt. + ViewLink string `json:"viewLink"` + Title string `json:"title"` + TitleHTML template.HTML `json:"titleHTML"` + Status string `json:"status"` + CanCancel bool `json:"canCancel"` + CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve + CanRerun bool `json:"canRerun"` + CanRerunFailed bool `json:"canRerunFailed"` + CanDeleteArtifact bool `json:"canDeleteArtifact"` + Done bool `json:"done"` + WorkflowID string `json:"workflowID"` + WorkflowLink string `json:"workflowLink"` + IsSchedule bool `json:"isSchedule"` + RunAttempt int64 `json:"runAttempt"` + Attempts []*ViewRunAttempt `json:"attempts"` + Jobs []*ViewJob `json:"jobs"` + Commit ViewCommit `json:"commit"` // Summary view: run duration and trigger time/event Duration string `json:"duration"` TriggeredAt int64 `json:"triggeredAt"` // unix seconds for relative time @@ -293,6 +314,7 @@ type ViewResponse struct { type ViewJob struct { ID int64 `json:"id"` + Link string `json:"link"` JobID string `json:"jobId,omitempty"` Name string `json:"name"` Status string `json:"status"` @@ -301,6 +323,18 @@ type ViewJob struct { Needs []string `json:"needs,omitempty"` } +type ViewRunAttempt struct { + Attempt int64 `json:"attempt"` + Status string `json:"status"` + Done bool `json:"done"` + Link string `json:"link"` + Current bool `json:"current"` + Latest bool `json:"latest"` + TriggeredAt int64 `json:"triggeredAt"` + TriggerUserName string `json:"triggerUserName"` + TriggerUserLink string `json:"triggerUserLink"` +} + type ViewCommit struct { ShortSha string `json:"shortSHA"` Link string `json:"link"` @@ -338,24 +372,8 @@ type ViewStepLogLine struct { Timestamp float64 `json:"timestamp"` } -func getActionsViewArtifacts(ctx context.Context, repoID, runID int64) (artifactsViewItems []*ArtifactsViewItem, err error) { - artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, repoID, runID) - if err != nil { - return nil, err - } - for _, art := range artifacts { - artifactsViewItems = append(artifactsViewItems, &ArtifactsViewItem{ - Name: art.ArtifactName, - Size: art.FileSize, - Status: util.Iif(art.Status == actions_model.ArtifactStatusExpired, "expired", "completed"), - ExpiresUnix: int64(art.ExpiredUnix), - }) - } - return artifactsViewItems, nil -} - func ViewPost(ctx *context_module.Context) { - run, jobs := getCurrentRunJobsByPathParam(ctx) + run, attempt, jobs := getCurrentRunJobsByPathParam(ctx) if ctx.Written() { return } @@ -365,7 +383,7 @@ func ViewPost(ctx *context_module.Context) { } resp := &ViewResponse{} - fillViewRunResponseSummary(ctx, resp, run, jobs) + fillViewRunResponseSummary(ctx, resp, run, attempt, jobs) if ctx.Written() { return } @@ -376,23 +394,33 @@ func ViewPost(ctx *context_module.Context) { ctx.JSON(http.StatusOK, resp) } -func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) { - var err error - resp.Artifacts, err = getActionsViewArtifacts(ctx, ctx.Repo.Repository.ID, run.ID) - if err != nil { - ctx.ServerError("getActionsViewArtifacts", err) - return - } +func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt, jobs []*actions_model.ActionRunJob) { + // Latest when the run has no attempts yet (legacy) or the viewed attempt is the run's latest. + isLatestAttempt := run.LatestAttemptID == 0 || (attempt != nil && attempt.ID == run.LatestAttemptID) resp.State.Run.RepoID = ctx.Repo.Repository.ID // the title for the "run" is from the commit message resp.State.Run.Title = run.Title resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, ctx.Repo.Repository) resp.State.Run.Link = run.Link() - resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) - resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions) - resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) - resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) + resp.State.Run.ViewLink = getRunViewLink(run, attempt) + resp.State.Run.Attempts = make([]*ViewRunAttempt, 0) + if attempt != nil { + resp.State.Run.RunAttempt = attempt.Attempt + resp.State.Run.Status = attempt.Status.String() + resp.State.Run.Done = attempt.Status.IsDone() + resp.State.Run.Duration = attempt.Duration().String() + resp.State.Run.TriggeredAt = attempt.Created.AsTime().Unix() + } else { + resp.State.Run.Status = run.Status.String() + resp.State.Run.Done = run.Status.IsDone() + resp.State.Run.Duration = run.Duration().String() + resp.State.Run.TriggeredAt = run.Created.AsTime().Unix() + } + resp.State.Run.CanCancel = isLatestAttempt && !resp.State.Run.Done && ctx.Repo.Permission.CanWrite(unit.TypeActions) + resp.State.Run.CanApprove = isLatestAttempt && run.NeedApproval && ctx.Repo.Permission.CanWrite(unit.TypeActions) + resp.State.Run.CanRerun = isLatestAttempt && resp.State.Run.Done && ctx.Repo.Permission.CanWrite(unit.TypeActions) + resp.State.Run.CanDeleteArtifact = resp.State.Run.Done && ctx.Repo.Permission.CanWrite(unit.TypeActions) if resp.State.Run.CanRerun { for _, job := range jobs { if job.Status == actions_model.StatusFailure || job.Status == actions_model.StatusCancelled { @@ -401,15 +429,16 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, } } } - resp.State.Run.Done = run.Status.IsDone() resp.State.Run.WorkflowID = run.WorkflowID - resp.State.Run.WorkflowLink = run.WorkflowLink() + if isLatestAttempt { + resp.State.Run.WorkflowLink = run.WorkflowLink() + } resp.State.Run.IsSchedule = run.IsSchedule() resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json - resp.State.Run.Status = run.Status.String() for _, v := range jobs { resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{ ID: v.ID, + Link: fmt.Sprintf("%s/jobs/%d", run.Link(), v.ID), JobID: v.JobID, Name: v.Name, Status: v.Status.String(), @@ -419,6 +448,29 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, }) } + attempts, err := actions_model.ListRunAttemptsByRunID(ctx, run.ID) + if err != nil { + ctx.ServerError("ListRunAttemptsByRunID", err) + return + } + if err := attempts.LoadTriggerUser(ctx); err != nil { + ctx.ServerError("LoadTriggerUser", err) + return + } + for _, runAttempt := range attempts { + resp.State.Run.Attempts = append(resp.State.Run.Attempts, &ViewRunAttempt{ + Attempt: runAttempt.Attempt, + Status: runAttempt.Status.String(), + Done: runAttempt.Status.IsDone(), + Link: getRunViewLink(run, runAttempt), + Current: runAttempt.ID == attempt.ID, + Latest: runAttempt.ID == run.LatestAttemptID, + TriggeredAt: runAttempt.Created.AsTime().Unix(), + TriggerUserName: runAttempt.TriggerUser.GetDisplayName(), + TriggerUserLink: runAttempt.TriggerUser.HomeLink(), + }) + } + pusher := ViewUser{ DisplayName: run.TriggerUser.GetDisplayName(), Link: run.TriggerUser.HomeLink(), @@ -443,9 +495,27 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, Pusher: pusher, Branch: branch, } - resp.State.Run.Duration = run.Duration().String() - resp.State.Run.TriggeredAt = run.Created.AsTime().Unix() resp.State.Run.TriggerEvent = run.TriggerEvent + + // Legacy runs (LatestAttemptID == 0) have no attempt; their artifacts all share run_attempt_id=0, + // so passing 0 here scopes to this run's legacy artifacts only. + var runAttemptID int64 + if attempt != nil { + runAttemptID = attempt.ID + } + arts, err := actions_model.ListUploadedArtifactsMetaByRunAttempt(ctx, ctx.Repo.Repository.ID, run.ID, runAttemptID) + if err != nil { + ctx.ServerError("ListUploadedArtifactsMetaByRunAttempt", err) + return + } + resp.Artifacts = make([]*ArtifactsViewItem, 0, len(arts)) + for _, art := range arts { + resp.Artifacts = append(resp.Artifacts, &ArtifactsViewItem{ + Name: art.ArtifactName, + Size: art.FileSize, + Status: util.Iif(art.Status == actions_model.ArtifactStatusExpired, "expired", "completed"), + }) + } } func fillViewRunResponseCurrentJob(ctx *context_module.Context, resp *ViewResponse, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) { @@ -459,9 +529,9 @@ func fillViewRunResponseCurrentJob(ctx *context_module.Context, resp *ViewRespon } var task *actions_model.ActionTask - if current.TaskID > 0 { + if effectiveTaskID := current.EffectiveTaskID(); effectiveTaskID > 0 { var err error - task, err = actions_model.GetTaskByID(ctx, current.TaskID) + task, err = actions_model.GetTaskByID(ctx, effectiveTaskID) if err != nil { ctx.ServerError("actions_model.GetTaskByID", err) return @@ -589,13 +659,24 @@ func checkRunRerunAllowed(ctx *context_module.Context, run *actions_model.Action return true } +func checkLatestAttempt(ctx *context_module.Context, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt) bool { + if attempt != nil && run.LatestAttemptID != attempt.ID { + ctx.NotFound(nil) + return false + } + return true +} + // Rerun will rerun jobs in the given run // If jobIDStr is a blank string, it means rerun all jobs func Rerun(ctx *context_module.Context) { - run, jobs := getCurrentRunJobsByPathParam(ctx) + run, attempt, jobs := getCurrentRunJobsByPathParam(ctx) if ctx.Written() { return } + if !checkLatestAttempt(ctx, run, attempt) { + return + } if !checkRunRerunAllowed(ctx, run) { return } @@ -608,35 +689,48 @@ func Rerun(ctx *context_module.Context) { var jobsToRerun []*actions_model.ActionRunJob if currentJob != nil { - jobsToRerun = actions_service.GetAllRerunJobs(currentJob, jobs) - } else { - jobsToRerun = jobs + jobsToRerun = []*actions_model.ActionRunJob{currentJob} } - if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobsToRerun); err != nil { - ctx.ServerError("RerunWorkflowRunJobs", err) + if _, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, jobsToRerun); err != nil { + handleWorkflowRerunError(ctx, err) return } - ctx.JSONOK() + ctx.JSONRedirect(run.Link()) } // RerunFailed reruns all failed jobs in the given run func RerunFailed(ctx *context_module.Context) { - run, jobs := getCurrentRunJobsByPathParam(ctx) + run, attempt, jobs := getCurrentRunJobsByPathParam(ctx) if ctx.Written() { return } + if !checkLatestAttempt(ctx, run, attempt) { + return + } if !checkRunRerunAllowed(ctx, run) { return } - if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil { - ctx.ServerError("RerunWorkflowRunJobs", err) + if _, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, actions_service.GetFailedJobsForRerun(jobs)); err != nil { + handleWorkflowRerunError(ctx, err) return } - ctx.JSONOK() + ctx.JSONRedirect(run.Link()) +} + +func handleWorkflowRerunError(ctx *context_module.Context, err error) { + if errors.Is(err, util.ErrAlreadyExist) { + ctx.JSON(http.StatusConflict, map[string]any{"message": err.Error()}) + return + } + if errors.Is(err, util.ErrInvalidArgument) { + ctx.JSON(http.StatusBadRequest, map[string]any{"message": err.Error()}) + return + } + ctx.ServerError("RerunWorkflowRunJobs", err) } func Logs(ctx *context_module.Context) { @@ -654,10 +748,13 @@ func Logs(ctx *context_module.Context) { } func Cancel(ctx *context_module.Context) { - run, jobs := getCurrentRunJobsByPathParam(ctx) + run, attempt, jobs := getCurrentRunJobsByPathParam(ctx) if ctx.Written() { return } + if !checkLatestAttempt(ctx, run, attempt) { + return + } var updatedJobs []*actions_model.ActionRunJob @@ -676,13 +773,9 @@ func Cancel(ctx *context_module.Context) { actions_service.CreateCommitStatusForRunJobs(ctx, run, jobs...) actions_service.EmitJobsIfReadyByJobs(updatedJobs) - for _, job := range updatedJobs { - _ = job.LoadAttributes(ctx) - notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) - } + actions_service.NotifyWorkflowJobsStatusUpdate(ctx, updatedJobs...) if len(updatedJobs) > 0 { - job := updatedJobs[0] - actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) + actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, run.RepoID, run.ID) } ctx.JSONOK() } @@ -692,78 +785,14 @@ func Approve(ctx *context_module.Context) { if ctx.Written() { return } - approveRuns(ctx, []int64{run.ID}) - if ctx.Written() { - return - } - - ctx.JSONOK() -} - -func approveRuns(ctx *context_module.Context, runIDs []int64) { - doer := ctx.Doer - repo := ctx.Repo.Repository - - updatedJobs := make([]*actions_model.ActionRunJob, 0) - runMap := make(map[int64]*actions_model.ActionRun, len(runIDs)) - runJobs := make(map[int64][]*actions_model.ActionRunJob, len(runIDs)) - - err := db.WithTx(ctx, func(ctx context.Context) (err error) { - for _, runID := range runIDs { - run, err := actions_model.GetRunByRepoAndID(ctx, repo.ID, runID) - if err != nil { - return err - } - runMap[run.ID] = run - run.Repo = repo - run.NeedApproval = false - run.ApprovedBy = doer.ID - if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil { - return err - } - jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) - if err != nil { - return err - } - runJobs[run.ID] = jobs - for _, job := range jobs { - job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job) - if err != nil { - return err - } - if job.Status == actions_model.StatusWaiting { - n, err := actions_model.UpdateRunJob(ctx, job, nil, "status") - if err != nil { - return err - } - if n > 0 { - updatedJobs = append(updatedJobs, job) - } - } - } - } - return nil - }) - if err != nil { - ctx.NotFoundOrServerError("approveRuns", func(err error) bool { + if err := actions_service.ApproveRuns(ctx, ctx.Repo.Repository, ctx.Doer, []int64{run.ID}); err != nil { + ctx.NotFoundOrServerError("ApproveRuns", func(err error) bool { return errors.Is(err, util.ErrNotExist) }, err) return } - for runID, run := range runMap { - actions_service.CreateCommitStatusForRunJobs(ctx, run, runJobs[runID]...) - } - - if len(updatedJobs) > 0 { - job := updatedJobs[0] - actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) - } - - for _, job := range updatedJobs { - _ = job.LoadAttributes(ctx) - notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) - } + ctx.JSONOK() } func Delete(ctx *context_module.Context) { @@ -785,28 +814,108 @@ func Delete(ctx *context_module.Context) { ctx.JSONOK() } -// getRunJobs loads the run and its jobs for runID +func getRunViewLink(run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt) string { + if attempt == nil || run.LatestAttemptID == attempt.ID { + return run.Link() + } + return fmt.Sprintf("%s/attempts/%d", run.Link(), attempt.Attempt) +} + +// getCurrentRunJobsByPathParam resolves the current run view context from path parameters, including the run, optional attempt, and jobs to render. // Any error will be written to the ctx, empty jobs will also result in 404 error, then the return values are all nil. -func getCurrentRunJobsByPathParam(ctx *context_module.Context) (*actions_model.ActionRun, []*actions_model.ActionRunJob) { +func getCurrentRunJobsByPathParam(ctx *context_module.Context) (*actions_model.ActionRun, *actions_model.ActionRunAttempt, []*actions_model.ActionRunJob) { run := getCurrentRunByPathParam(ctx) if ctx.Written() { - return nil, nil + return nil, nil, nil } run.Repo = ctx.Repo.Repository - jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + + var err error + var selectedJob *actions_model.ActionRunJob + if ctx.PathParam("job") != "" { + jobID := ctx.PathParamInt64("job") + selectedJob, err = actions_model.GetRunJobByRunAndID(ctx, run.ID, jobID) + if err != nil { + ctx.NotFoundOrServerError("GetRunJobByRepoAndID", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + return nil, nil, nil + } + } + + // Resolve the attempt to display. + // Priority: explicit path param (/attempts/:num) > job's attempt (when navigating to a specific job) > latest attempt. + // attempt may be nil for legacy runs that pre-date ActionRunAttempt; callers must handle that case. + attemptNum := ctx.PathParamInt64("attempt") + var attempt *actions_model.ActionRunAttempt + switch { + case attemptNum > 0: + // Explicit attempt number in the URL — user is viewing a historical attempt. + attempt, err = actions_model.GetRunAttemptByRunIDAndAttemptNum(ctx, run.ID, attemptNum) + if err != nil { + ctx.NotFoundOrServerError("GetRunAttemptByRunIDAndAttempt", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + return nil, nil, nil + } + case selectedJob != nil && selectedJob.RunAttemptID > 0: + // No explicit attempt in the URL, but the requested job belongs to a known attempt — resolve via the job. + attempt, err = actions_model.GetRunAttemptByRepoAndID(ctx, selectedJob.RepoID, selectedJob.RunAttemptID) + if err != nil { + ctx.NotFoundOrServerError("GetRunAttemptByRepoAndID", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + return nil, nil, nil + } + default: + // No attempt context at all — show the latest attempt (nil for legacy runs). + attempt, _, err = run.GetLatestAttempt(ctx) + if err != nil { + ctx.NotFoundOrServerError("GetLatestAttempt", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + return nil, nil, nil + } + } + + // Resolve the jobs for the resolved attempt. + // When attempt is nil (legacy run or legacy job), jobs are stored with run_attempt_id=0. + var resolvedAttemptID int64 + if attempt != nil { + resolvedAttemptID = attempt.ID + } + jobs, err := actions_model.GetRunJobsByRunAndAttemptID(ctx, run.ID, resolvedAttemptID) if err != nil { - ctx.ServerError("GetRunJobsByRunID", err) - return nil, nil + ctx.ServerError("get current jobs", err) + return nil, nil, nil } if len(jobs) == 0 { ctx.NotFound(nil) - return nil, nil + return nil, nil, nil } for _, job := range jobs { job.Run = run } - return run, jobs + return run, attempt, jobs +} + +// resolveArtifactAttemptIDFromQuery resolves the run_attempt_id used to scope artifact lookups. +// If the `attempt` query parameter is present and valid, it returns the matching attempt's ID. +// Otherwise it falls back to run.LatestAttemptID, which is 0 only for legacy runs created before ActionRunAttempt existed. +func resolveArtifactAttemptIDFromQuery(ctx *context_module.Context, run *actions_model.ActionRun) (int64, error) { + if ctx.FormString("attempt") == "" { + return run.LatestAttemptID, nil + } + attemptNum := ctx.FormInt64("attempt") + if attemptNum <= 0 { + return 0, util.ErrNotExist + } + attempt, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(ctx, run.ID, attemptNum) + if err != nil { + return 0, err + } + return attempt.ID, nil } func ArtifactsDeleteView(ctx *context_module.Context) { @@ -814,9 +923,16 @@ func ArtifactsDeleteView(ctx *context_module.Context) { if ctx.Written() { return } + resolvedAttemptID, err := resolveArtifactAttemptIDFromQuery(ctx, run) + if err != nil { + ctx.NotFoundOrServerError("resolveArtifactAttemptIDFromQuery", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + return + } artifactName := ctx.PathParam("artifact_name") - if err := actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil { - ctx.ServerError("SetArtifactNeedDelete", err) + if err := actions_model.SetArtifactNeedDeleteByRunAttempt(ctx, run.ID, resolvedAttemptID, artifactName); err != nil { + ctx.ServerError("SetArtifactNeedDeleteByRunAttempt", err) return } ctx.JSON(http.StatusOK, struct{}{}) @@ -827,14 +943,17 @@ func ArtifactsDownloadView(ctx *context_module.Context) { if ctx.Written() { return } - - artifactName := ctx.PathParam("artifact_name") - artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{ - RunID: run.ID, - ArtifactName: artifactName, - }) + resolvedAttemptID, err := resolveArtifactAttemptIDFromQuery(ctx, run) if err != nil { - ctx.ServerError("FindArtifacts", err) + ctx.NotFoundOrServerError("resolveArtifactAttemptIDFromQuery", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + return + } + artifactName := ctx.PathParam("artifact_name") + artifacts, err := actions_model.GetArtifactsByRunAttemptAndName(ctx, run.ID, resolvedAttemptID, artifactName) + if err != nil { + ctx.ServerError("GetArtifactsByRunAttemptAndName", err) return } if len(artifacts) == 0 { @@ -931,8 +1050,10 @@ func ApproveAllChecks(ctx *context_module.Context) { return } - approveRuns(ctx, runIDs) - if ctx.Written() { + if err := actions_service.ApproveRuns(ctx, repo, ctx.Doer, runIDs); err != nil { + ctx.NotFoundOrServerError("ApproveRuns", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) return } diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go index 4cfe879032a..420fa6d557f 100644 --- a/routers/web/repo/activity.go +++ b/routers/web/repo/activity.go @@ -48,7 +48,7 @@ func Activity(ctx *context.Context) { ctx.Data["Period"] = period ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + period) - canReadCode := ctx.Repo.CanRead(unit.TypeCode) + canReadCode := ctx.Repo.Permission.CanRead(unit.TypeCode) if canReadCode { // GetActivityStats needs to read the default branch to get some information branchExist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, ctx.Repo.Repository.DefaultBranch) @@ -62,9 +62,9 @@ func Activity(ctx *context.Context) { var err error // TODO: refactor these arguments to a struct ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom, - ctx.Repo.CanRead(unit.TypeReleases), - ctx.Repo.CanRead(unit.TypeIssues), - ctx.Repo.CanRead(unit.TypePullRequests), + ctx.Repo.Permission.CanRead(unit.TypeReleases), + ctx.Repo.Permission.CanRead(unit.TypeIssues), + ctx.Repo.Permission.CanRead(unit.TypePullRequests), canReadCode, ) if err != nil { diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index c566e465e9d..ce0e0b03abf 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -39,10 +39,10 @@ const ( func Branches(ctx *context.Context) { ctx.Data["Title"] = "Branches" ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls(ctx) - ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode) + ctx.Data["IsWriter"] = ctx.Repo.Permission.CanWrite(unit.TypeCode) ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror // TODO: Can be replaced by ctx.Repo.PullRequestCtx.CanCreateNewPull() - ctx.Data["CanPull"] = ctx.Repo.CanWrite(unit.TypeCode) || + ctx.Data["CanPull"] = ctx.Repo.Permission.CanWrite(unit.TypeCode) || (ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)) ctx.Data["PageIsViewCode"] = true ctx.Data["PageIsBranches"] = true @@ -68,7 +68,7 @@ func Branches(ctx *context.Context) { ctx.ServerError("LoadBranches", err) return } - if !ctx.Repo.CanRead(unit.TypeActions) { + if !ctx.Repo.Permission.CanRead(unit.TypeActions) { for key := range commitStatuses { git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) } diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 34e588b1416..736a2dff003 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -384,7 +384,7 @@ func Diff(ctx *context.Context) { if err != nil { log.Error("GetLatestCommitStatus: %v", err) } - if !ctx.Repo.CanRead(unit_model.TypeActions) { + if !ctx.Repo.Permission.CanRead(unit_model.TypeActions) { git_model.CommitStatusesHideActionsURL(ctx, statuses) } @@ -466,7 +466,7 @@ func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) ([]*git_m if err != nil { return nil, err } - if !ctx.Repo.CanRead(unit_model.TypeActions) { + if !ctx.Repo.Permission.CanRead(unit_model.TypeActions) { for _, commit := range commits { if commit.Status == nil { continue diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 285f3968d41..7598ce561c3 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -421,8 +421,7 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { } else { ctx.Data["BeforeCommitID"] = compareInfo.MergeBase } - - return compareInfo + return &compareInfo } func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses) (title, content string) { @@ -708,11 +707,11 @@ func CompareDiff(ctx *context.Context) { } } - ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanWrite(unit.TypeProjects) + ctx.Data["IsProjectsEnabled"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") - ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypePullRequests) + ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypePullRequests) if unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypePullRequests); err == nil { config := unit.PullRequestsConfig() @@ -803,7 +802,7 @@ func ExcerptBlob(ctx *context.Context) { diffBlobExcerptData.PullIssueIndex = ctx.FormInt64("pull_issue_index") if diffBlobExcerptData.PullIssueIndex > 0 { - if !ctx.Repo.CanRead(unit.TypePullRequests) { + if !ctx.Repo.Permission.CanRead(unit.TypePullRequests) { ctx.NotFound(nil) return } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 0fe703e150c..1e0abd6ed2b 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -81,7 +81,7 @@ func MustAllowUserComment(ctx *context.Context) { return } - if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { + if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked")) ctx.Redirect(issue.Link()) return @@ -90,8 +90,8 @@ func MustAllowUserComment(ctx *context.Context) { // MustEnableIssues check if repository enable internal issues func MustEnableIssues(ctx *context.Context) { - if !ctx.Repo.CanRead(unit.TypeIssues) && - !ctx.Repo.CanRead(unit.TypeExternalTracker) { + if !ctx.Repo.Permission.CanRead(unit.TypeIssues) && + !ctx.Repo.Permission.CanRead(unit.TypeExternalTracker) { ctx.NotFound(nil) return } @@ -105,7 +105,7 @@ func MustEnableIssues(ctx *context.Context) { // MustAllowPulls check if repository enable pull requests and user have right to do that func MustAllowPulls(ctx *context.Context) { - if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) { + if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.Permission.CanRead(unit.TypePullRequests) { ctx.NotFound(nil) return } @@ -195,8 +195,8 @@ func GetActionIssue(ctx *context.Context) *issues_model.Issue { } func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) { - if issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) || - !issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) { + if issue.IsPull && !ctx.Repo.Permission.CanRead(unit.TypePullRequests) || + !issue.IsPull && !ctx.Repo.Permission.CanRead(unit.TypeIssues) { ctx.NotFound(nil) } } @@ -221,8 +221,8 @@ func getActionIssues(ctx *context.Context) issues_model.IssueList { return nil } // Check access rights for all issues - issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues) - prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests) + issueUnitEnabled := ctx.Repo.Permission.CanRead(unit.TypeIssues) + prUnitEnabled := ctx.Repo.Permission.CanRead(unit.TypePullRequests) for _, issue := range issues { if issue.RepoID != ctx.Repo.Repository.ID { ctx.NotFound(errors.New("some issue's RepoID is incorrect")) @@ -254,13 +254,13 @@ func GetIssueInfo(ctx *context.Context) { if issue.IsPull { // Need to check if Pulls are enabled and we can read Pulls - if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) { + if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.Permission.CanRead(unit.TypePullRequests) { ctx.HTTPError(http.StatusNotFound) return } } else { // Need to check if Issues are enabled and we can read Issues - if !ctx.Repo.CanRead(unit.TypeIssues) { + if !ctx.Repo.Permission.CanRead(unit.TypeIssues) { ctx.HTTPError(http.StatusNotFound) return } @@ -279,7 +279,7 @@ func UpdateIssueTitle(ctx *context.Context) { return } - if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) { + if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)) { ctx.HTTPError(http.StatusForbidden) return } @@ -307,7 +307,7 @@ func UpdateIssueRef(ctx *context.Context) { return } - if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull { + if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull { ctx.HTTPError(http.StatusForbidden) return } @@ -331,7 +331,7 @@ func UpdateIssueContent(ctx *context.Context) { return } - if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) { + if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)) { ctx.HTTPError(http.StatusForbidden) return } @@ -387,7 +387,7 @@ func UpdateIssueDeadline(ctx *context.Context) { return } - if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { ctx.HTTPError(http.StatusForbidden, "", "Not repo writer") return } @@ -486,7 +486,7 @@ func ChangeIssueReaction(ctx *context.Context) { return } - if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) { + if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull)) { if log.IsTrace() { if ctx.IsSigned { issueType := "issues" diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go index 860dcd74423..ccf9a3749cd 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -45,14 +45,14 @@ func NewComment(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateCommentForm) issueType := util.Iif(issue.IsPull, "pulls", "issues") - if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) { + if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull)) { log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+ "User in Repo has Permissions: %-+v", ctx.Doer, issue.PosterID, issueType, ctx.Repo.Repository, ctx.Repo.Permission) ctx.HTTPError(http.StatusForbidden) return } - if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { + if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { ctx.JSONError(ctx.Tr("repo.issues.comment_on_locked")) return } @@ -85,7 +85,7 @@ func NewComment(ctx *context.Context) { // TODO: need further refactoring to the code below // Check if doer can change the status of issue (close, reopen). - if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) && + if (ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) && (form.Status == "reopen" || form.Status == "close") && !(issue.IsPull && issue.PullRequest.HasMerged) { // Duplication and conflict check should apply to reopen pull request. @@ -205,7 +205,7 @@ func UpdateCommentContent(ctx *context.Context) { return } - if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { + if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { ctx.HTTPError(http.StatusForbidden) return } @@ -289,7 +289,7 @@ func DeleteComment(ctx *context.Context) { return } - if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { + if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { ctx.HTTPError(http.StatusForbidden) return } else if !comment.Type.HasContentSupport() { @@ -324,7 +324,7 @@ func ChangeCommentReaction(ctx *context.Context) { return } - if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) { + if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull)) { if log.IsTrace() { if ctx.IsSigned { issueType := "issues" diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go index 23cedfcb80a..01fb139c1af 100644 --- a/routers/web/repo/issue_content_history.go +++ b/routers/web/repo/issue_content_history.go @@ -88,7 +88,7 @@ func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue history *issues_model.ContentHistory, ) (canSoftDelete bool) { // CanWrite means the doer can manage the issue/PR list - if ctx.Repo.IsOwner() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + if ctx.Repo.Permission.IsOwner() || ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { canSoftDelete = true } else if ctx.Doer != nil { // for read-only users, they could still post issues or comments, diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index 83ef515bde5..60d7a4f24dd 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -641,7 +641,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6 ctx.ServerError("GetIssuesAllCommitStatus", err) return } - if !ctx.Repo.CanRead(unit.TypeActions) { + if !ctx.Repo.Permission.CanRead(unit.TypeActions) { for key := range commitStatuses { git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) } @@ -700,7 +700,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6 showArchivedLabels := ctx.FormBool("archived_labels") ctx.Data["ShowArchivedLabels"] = showArchivedLabels ctx.Data["PinnedIssues"] = pinned - ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin) + ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.Permission.IsAdmin() || ctx.Doer.IsAdmin) ctx.Data["IssueStats"] = issueStats ctx.Data["OpenCount"] = issueStats.OpenCount ctx.Data["ClosedCount"] = issueStats.ClosedCount @@ -759,7 +759,7 @@ func Issues(ctx *context.Context) { return } - ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList) + ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(isPullList) ctx.HTML(http.StatusOK, tplIssues) } diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index 592d902ba8e..861709d2ffb 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -110,7 +110,7 @@ func NewIssue(ctx *context.Context) { body := ctx.FormString("body") ctx.Data["BodyQuery"] = body - isProjectsEnabled := ctx.Repo.CanRead(unit.TypeProjects) + isProjectsEnabled := ctx.Repo.Permission.CanRead(unit.TypeProjects) ctx.Data["IsProjectsEnabled"] = isProjectsEnabled ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") @@ -144,7 +144,7 @@ func NewIssue(ctx *context.Context) { ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true) } - ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues) + ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypeIssues) if !issueConfig.BlankIssuesEnabled && hasTemplates && !templateLoaded { // The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if blank issues are disabled, just redirect to the "issues/choose" page with these parameters. @@ -344,7 +344,7 @@ func NewIssuePost(ctx *context.Context) { labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID if projectID > 0 { - if !ctx.Repo.CanRead(unit.TypeProjects) { + if !ctx.Repo.Permission.CanRead(unit.TypeProjects) { // User must also be able to see the project. ctx.HTTPError(http.StatusBadRequest, "user hasn't permissions to read projects") return diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index be609d8cdfb..9c7ac65a1f9 100644 --- a/routers/web/repo/issue_page_meta.go +++ b/routers/web/repo/issue_page_meta.go @@ -110,7 +110,7 @@ func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository // A reader(creator) could update some meta (eg: target branch), but can't change assignees anymore. // For non-creator users, only writers could update some meta (eg: assignees, milestone, project) // Need to clarify the logic and add some tests in the future - data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived + data.CanModifyIssueOrPull = ctx.Repo.Permission.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived if !data.CanModifyIssueOrPull { return data } diff --git a/routers/web/repo/issue_suggestions.go b/routers/web/repo/issue_suggestions.go index 9ef39425041..592cc7b1d53 100644 --- a/routers/web/repo/issue_suggestions.go +++ b/routers/web/repo/issue_suggestions.go @@ -16,8 +16,8 @@ import ( func IssueSuggestions(ctx *context.Context) { keyword := ctx.Req.FormValue("q") - canReadIssues := ctx.Repo.CanRead(unit.TypeIssues) - canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests) + canReadIssues := ctx.Repo.Permission.CanRead(unit.TypeIssues) + canReadPulls := ctx.Repo.Permission.CanRead(unit.TypePullRequests) var isPull optional.Option[bool] if canReadPulls && !canReadIssues { diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index b9ed059fde9..e93a3107a3d 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -91,7 +91,7 @@ func UpdateIssueTimeEstimate(ctx *context.Context) { return } - if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) { + if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)) { ctx.HTTPError(http.StatusForbidden) return } diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index f678f838784..af13a1156ed 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -117,19 +117,6 @@ func roleDescriptor(ctx *context.Context, repo *repo_model.Repository, poster *u return roleDesc, nil } -func getBranchData(ctx *context.Context, issue *issues_model.Issue) { - ctx.Data["BaseBranch"] = nil - ctx.Data["HeadBranch"] = nil - ctx.Data["HeadUserName"] = nil - ctx.Data["BaseName"] = ctx.Repo.Repository.OwnerName - if issue.IsPull { - pull := issue.PullRequest - ctx.Data["BaseBranch"] = pull.BaseBranch - ctx.Data["HeadBranch"] = pull.HeadBranch - ctx.Data["HeadUserName"] = pull.MustHeadUserName(ctx) - } -} - // checkBlockedByIssues return canRead and notPermitted func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.DependencyInfo) (canRead, notPermitted []*issues_model.DependencyInfo) { repoPerms := make(map[int64]access_model.Permission) @@ -349,7 +336,7 @@ func ViewIssue(ctx *context.Context) { ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) } - ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects) + ctx.Data["IsProjectsEnabled"] = ctx.Repo.Permission.CanRead(unit.TypeProjects) ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") @@ -379,6 +366,7 @@ func ViewIssue(ctx *context.Context) { } pageMetaData.LabelsData.SetSelectedLabels(issue.Labels) + prViewInfo := newPullRequestViewInfo() prepareFuncs := []func(*context.Context, *issues_model.Issue){ prepareIssueViewContent, prepareIssueViewCommentsAndSidebarParticipants, @@ -386,10 +374,13 @@ func ViewIssue(ctx *context.Context) { prepareIssueViewSidebarTimeTracker, prepareIssueViewSidebarDependency, prepareIssueViewSidebarPin, - func(ctx *context.Context, issue *issues_model.Issue) { preparePullViewPullInfo(ctx, issue) }, - preparePullViewReviewAndMerge, } - + if issue.IsPull { + prepareFuncs = append(prepareFuncs, + prViewInfo.prepareViewInfo, + prViewInfo.prepareMergeBox, + ) + } for _, prepareFunc := range prepareFuncs { prepareFunc(ctx, issue) if ctx.Written() { @@ -402,16 +393,16 @@ func ViewIssue(ctx *context.Context) { if issue.PullRequest.HasMerged { ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged } else { - ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed + ctx.Data["DisableStatusChange"] = prViewInfo.IsPullRequestBroken && issue.IsClosed } } ctx.Data["Reference"] = issue.Ref ctx.Data["SignInLink"] = middleware.RedirectLinkUserLogin(ctx.Req) ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) - ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) - ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects) - ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin) + ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) + ctx.Data["HasProjectsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) + ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.Permission.IsAdmin() || ctx.Doer.IsAdmin) ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName() @@ -426,8 +417,8 @@ func ViewIssue(ctx *context.Context) { return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee) } - if issue.PullRequest != nil && !issue.PullRequest.IsChecking() && !setting.IsProd { - ctx.Data["PullMergeBoxReloadingInterval"] = 1 // in dev env, force using the reloading logic to make sure it won't break + if !setting.IsProd && issue.PullRequest != nil && !issue.PullRequest.IsChecking() && prViewInfo.MergeBoxData != nil { + prViewInfo.MergeBoxData.ReloadingInterval = 1 // in dev env, force using the reloading logic to make sure it won't break } ctx.HTML(http.StatusOK, tplIssueView) @@ -442,20 +433,27 @@ func ViewPullMergeBox(ctx *context.Context) { ctx.NotFound(nil) return } - preparePullViewPullInfo(ctx, issue) - preparePullViewReviewAndMerge(ctx, issue) + prViewInfo := newPullRequestViewInfo() + prViewInfo.prepareViewInfo(ctx, issue) + if ctx.Written() { + return + } + prViewInfo.prepareMergeBox(ctx, issue) + if ctx.Written() { + return + } ctx.Data["PullMergeBoxReloading"] = issue.PullRequest.IsChecking() // TODO: it should use a dedicated struct to render the pull merge box, to make sure all data is prepared correctly ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) - ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) + ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) ctx.HTML(http.StatusOK, tplPullMergeBox) } func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model.Issue) { - if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) { + if issue.IsPull && !ctx.Repo.Permission.CanRead(unit.TypeIssues) { ctx.Data["IssueDependencySearchType"] = "pulls" - } else if !issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) { + } else if !issue.IsPull && !ctx.Repo.Permission.CanRead(unit.TypePullRequests) { ctx.Data["IssueDependencySearchType"] = "issues" } else { ctx.Data["IssueDependencySearchType"] = "all" @@ -487,15 +485,12 @@ func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model ctx.Data["BlockingDependencies"], ctx.Data["BlockingDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking) } -func preparePullViewSigning(ctx *context.Context, issue *issues_model.Issue) { - if !issue.IsPull { - return - } - pull := issue.PullRequest - ctx.Data["WillSign"] = false +func (prInfo *pullRequestViewInfo) prepareMergeBoxRequireSigning(ctx *context.Context) { + pull := prInfo.issue.PullRequest + willSign := false if ctx.Doer != nil { sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, ctx.Repo.GitRepo) - ctx.Data["WillSign"] = sign + willSign = sign ctx.Data["SigningKeyMergeDisplay"] = asymkey_model.GetDisplaySigningKey(key) if err != nil { if asymkey_service.IsErrWontSign(err) { @@ -508,6 +503,8 @@ func preparePullViewSigning(ctx *context.Context, issue *issues_model.Issue) { } else { ctx.Data["WontSignReason"] = "not_signed_in" } + ctx.Data["WillSign"] = willSign + prInfo.MergeBoxData.willSign = willSign } func prepareIssueViewSidebarWatch(ctx *context.Context, issue *issues_model.Issue) { @@ -557,14 +554,11 @@ func prepareIssueViewSidebarTimeTracker(ctx *context.Context, issue *issues_mode } } -func preparePullViewDeleteBranch(ctx *context.Context, issue *issues_model.Issue, canDelete bool) { - if !issue.IsPull { - return - } - pull := issue.PullRequest +func (prInfo *pullRequestViewInfo) prepareMergeBoxDeleteBranch(ctx *context.Context, canDelete bool) { + pull := prInfo.issue.PullRequest isPullBranchDeletable := canDelete && pull.HeadRepo != nil && - (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"]) + (!pull.HasMerged || prInfo.HeadBranchCommitID == prInfo.CompareInfo.HeadCommitID) if isPullBranchDeletable { isPullBranchDeletable, _ = git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch) } @@ -579,6 +573,7 @@ func preparePullViewDeleteBranch(ctx *context.Context, issue *issues_model.Issue isPullBranchDeletable = !exist } ctx.Data["IsPullBranchDeletable"] = isPullBranchDeletable + prInfo.MergeBoxData.isPullBranchDeletable = isPullBranchDeletable } func prepareIssueViewSidebarPin(ctx *context.Context, issue *issues_model.Issue) { @@ -768,7 +763,7 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue ctx.ServerError("LoadCommentPushCommits", err) return } - if !ctx.Repo.CanRead(unit.TypeActions) { + if !ctx.Repo.Permission.CanRead(unit.TypeActions) { for _, commit := range comment.Commits { if commit.Status == nil { continue @@ -826,20 +821,42 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue ctx.Data["NumParticipants"] = len(participants) } -func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Issue) { - getBranchData(ctx, issue) - if !issue.IsPull { - return +func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *issues_model.Issue) { + if prInfo.issue != issue { + panic("impossible, issue must be the same") + } + + data := &pullMergeBoxData{} + prInfo.MergeBoxData = data + + statusCheckData := prInfo.StatusCheckData + if statusCheckData == nil { + statusCheckData = &pullCommitStatusCheckData{} // make the following logic easier, no need to keep checking "nil" } pull := issue.PullRequest - pull.Issue = issue canDelete := false allowMerge := false canWriteToHeadRepo := false pull_service.StartPullRequestCheckOnView(ctx, pull) + ctx.Data["GetCommitMessages"] = "" + if !prInfo.IsPullRequestBroken { + var err error + ctx.Data["UpdateAllowed"], ctx.Data["UpdateByRebaseAllowed"], err = pull_service.IsUserAllowedToUpdate(ctx, pull, ctx.Doer) + if err != nil { + ctx.ServerError("IsUserAllowedToUpdate", err) + return + } + ctx.Data["GetCommitMessages"] = pull_service.GetSquashMergeCommitMessages(ctx, pull) + } + + if pull.IsFilesConflicted() { + ctx.Data["IsPullFilesConflicted"] = true + ctx.Data["ConflictedFiles"] = pull.ConflictedFiles + } + if ctx.IsSigned { if err := pull.LoadHeadRepo(ctx); err != nil { log.Error("LoadHeadRepo: %v", err) @@ -886,7 +903,7 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss } } - ctx.Data["PullMergeBoxReloadingInterval"] = util.Iif(pull != nil && pull.IsChecking(), 2000, 0) + data.ReloadingInterval = util.Iif(pull != nil && pull.IsChecking(), 2000, 0) ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo ctx.Data["AllowMerge"] = allowMerge @@ -939,33 +956,41 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody - pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch) - if err != nil { - ctx.ServerError("LoadProtectedBranch", err) - return - } - + pb := prInfo.ProtectedBranchRule if pb != nil { pb.Repo = pull.BaseRepo ctx.Data["ProtectedBranch"] = pb - ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull) - ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull) - ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull) - ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pb, pull) + + data.isBlockedByApprovals = !issues_model.HasEnoughApprovals(ctx, pb, pull) + ctx.Data["IsBlockedByApprovals"] = data.isBlockedByApprovals + + data.isBlockedByRejection = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull) + ctx.Data["IsBlockedByRejection"] = data.isBlockedByRejection + + data.isBlockedByOfficialReviewRequests = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull) + ctx.Data["IsBlockedByOfficialReviewRequests"] = data.isBlockedByOfficialReviewRequests + + data.isBlockedByOutdatedBranch = issues_model.MergeBlockedByOutdatedBranch(pb, pull) + ctx.Data["IsBlockedByOutdatedBranch"] = data.isBlockedByOutdatedBranch + + data.isBlockedByChangedProtectedFiles = len(pull.ChangedProtectedFiles) != 0 + ctx.Data["IsBlockedByChangedProtectedFiles"] = data.isBlockedByChangedProtectedFiles + + data.requireSigned = pb.RequireSignedCommits + ctx.Data["RequireSigned"] = data.requireSigned + ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pb, pull) - ctx.Data["RequireSigned"] = pb.RequireSignedCommits ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles - ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0 ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles) ctx.Data["RequireApprovalsWhitelist"] = pb.EnableApprovalsWhitelist } - preparePullViewSigning(ctx, issue) + prInfo.prepareMergeBoxRequireSigning(ctx) if ctx.Written() { return } - preparePullViewDeleteBranch(ctx, issue, canDelete) + prInfo.prepareMergeBoxDeleteBranch(ctx, canDelete) if ctx.Written() { return } @@ -974,14 +999,10 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss if pull.HasMerged || issue.IsClosed || !ctx.IsSigned { return false } - if pull.CanAutoMerge() || pull.IsWorkInProgress(ctx) || pull.IsChecking() { + if pull.IsStatusMergeable() || pull.IsWorkInProgress(ctx) || pull.IsChecking() { return false } - if allowMerge && prConfig.AllowManualMerge { - return true - } - - return false + return allowMerge && prConfig.AllowManualMerge } ctx.Data["StillCanManualMerge"] = stillCanManualMerge() @@ -992,6 +1013,36 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss ctx.ServerError("GetScheduledMergeByPullID", err) return } + + enableStatusCheck := pb != nil && pb.EnableStatusCheck + ctx.Data["EnableStatusCheck"] = enableStatusCheck + + // Only show the merge box if the PR is not merged, or the branch is deletable. + // Otherwise, there is nothing to do, because the PR view page already contains enough information. + data.ShowMergeBox = !pull.HasMerged || data.isPullBranchDeletable + + isRepoAdmin := ctx.IsSigned && (ctx.Repo.Permission.IsAdmin() || ctx.Doer.IsAdmin) + + // admin can merge without checks, writer can merge when checks succeed + // admin and writer both can make an auto merge schedule (not affected by overridable blockers) + data.hasStatusCheckBlocker = enableStatusCheck && !statusCheckData.RequiredChecksState.IsSuccess() + + // this logic is from: + // {{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not $requiredStatusCheckState.IsSuccess))}} + // HINT: if a PR's status is not mergeable, then it is a non-overridable blocker, such logic is handled separately (see IsStatusMergeable) + data.HasOverridableBlockers = data.isBlockedByApprovals || data.isBlockedByRejection || + data.isBlockedByOfficialReviewRequests || data.isBlockedByOutdatedBranch || data.isBlockedByChangedProtectedFiles || + data.hasStatusCheckBlocker + + // this logic is from: + // {{$canMergeNow := and (or (and (not $.ProtectedBranch.BlockAdminMergeOverride) $.IsRepoAdmin) (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}} + // HINT: legacy "(not .AllowMerge)" is not right (always false, does nothing), fixed here + // CanMergeNow means: if the doer has write permission, whether the PR can be merged now + adminCanOverrideBlockers := (pb == nil || !pb.BlockAdminMergeOverride) && isRepoAdmin + data.CanMergeNow = (!data.HasOverridableBlockers || adminCanOverrideBlockers) && // status checks are satisfied + (!data.requireSigned || data.willSign) // signing requirement is satisfied + + ctx.Data["PullMergeBoxData"] = prInfo.MergeBoxData } func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) { diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go index 19d723c0eae..abb2a81d9e1 100644 --- a/routers/web/repo/issue_watch.go +++ b/routers/web/repo/issue_watch.go @@ -23,7 +23,7 @@ func IssueWatch(ctx *context.Context) { return } - if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) { + if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull)) { if log.IsTrace() { if ctx.IsSigned { issueType := "issues" diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index b928be28673..5e23c1c413c 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -265,8 +265,8 @@ func MilestoneIssuesAndPulls(ctx *context.Context) { ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) ctx.Data["NewIssueChooseTemplate"] = len(ret.IssueTemplates) > 0 - ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false) - ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true) + ctx.Data["CanWriteIssues"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(false) + ctx.Data["CanWritePulls"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(true) ctx.HTML(http.StatusOK, tplMilestoneIssues) } diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index cfb788a5b27..6dd54e42e2c 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -59,7 +59,7 @@ func Packages(ctx *context.Context) { ctx.Data["PackageType"] = packageType ctx.Data["AvailableTypes"] = packages.TypeList ctx.Data["HasPackages"] = hasPackages - ctx.Data["CanWritePackages"] = ctx.Repo.CanWrite(unit.TypePackages) || ctx.IsUserSiteAdmin() + ctx.Data["CanWritePackages"] = ctx.Repo.Permission.CanWrite(unit.TypePackages) || ctx.IsUserSiteAdmin() ctx.Data["PackageDescriptors"] = pds ctx.Data["Total"] = total ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index be12674223e..a94051f2980 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -45,7 +45,7 @@ func MustEnableRepoProjects(ctx *context.Context) { if ctx.Repo.Repository != nil { projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects) - if !ctx.Repo.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) { + if !ctx.Repo.Permission.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) { ctx.NotFound(nil) return } @@ -521,7 +521,7 @@ func DeleteProjectColumn(ctx *context.Context) { return } - if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { + if !ctx.Repo.Permission.IsOwner() && !ctx.Repo.Permission.IsAdmin() && !ctx.Repo.Permission.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only authorized users are allowed to perform this action.", }) @@ -568,7 +568,7 @@ func DeleteProjectColumn(ctx *context.Context) { // AddColumnToProjectPost allows a new column to be added to a project. func AddColumnToProjectPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.EditProjectColumnForm) - if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { + if !ctx.Repo.Permission.IsOwner() && !ctx.Repo.Permission.IsAdmin() && !ctx.Repo.Permission.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only authorized users are allowed to perform this action.", }) @@ -606,7 +606,7 @@ func checkProjectColumnChangePermissions(ctx *context.Context) (*project_model.P return nil, nil } - if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { + if !ctx.Repo.Permission.IsOwner() && !ctx.Repo.Permission.IsAdmin() && !ctx.Repo.Permission.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only authorized users are allowed to perform this action.", }) @@ -692,7 +692,7 @@ func MoveIssues(ctx *context.Context) { return } - if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { + if !ctx.Repo.Permission.IsOwner() && !ctx.Repo.Permission.IsAdmin() && !ctx.Repo.Permission.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only authorized users are allowed to perform this action.", }) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index efcdaac6740..c532cbba22d 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -161,11 +161,12 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) { return issue, true } -func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) { +func (prInfo *pullRequestViewInfo) setTemplateDataMergeTarget(ctx *context.Context) { + pull := prInfo.issue.PullRequest if ctx.Repo.Owner.Name == pull.MustHeadUserName(ctx) { ctx.Data["HeadTarget"] = pull.HeadBranch } else if pull.HeadRepo == nil { - ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + ":" + pull.HeadBranch + ctx.Data["HeadTarget"] = ctx.Locale.Tr("repo.pull.deleted_branch", pull.HeadBranch) } else { ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch } @@ -260,60 +261,214 @@ func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) stri return baseCommit } -func preparePullViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo { - if !issue.IsPull { - return nil - } - if issue.PullRequest.HasMerged { - return prepareMergedViewPullInfo(ctx, issue) - } - return prepareViewPullInfo(ctx, issue) +type pullMergeBoxData struct { + ShowMergeBox bool + ReloadingInterval int + + HasOverridableBlockers bool + CanMergeNow bool + + // don't expose unneeded fields to templates, need more refactoring changes + hasStatusCheckBlocker bool + isPullBranchDeletable bool + + isBlockedByApprovals bool + isBlockedByRejection bool + isBlockedByOfficialReviewRequests bool + isBlockedByOutdatedBranch bool + isBlockedByChangedProtectedFiles bool + requireSigned, willSign bool } -// prepareMergedViewPullInfo show meta information for a merged pull request view page -func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo { - pull := issue.PullRequest +// pullRequestViewInfo is a structured type for viewing pull request +// Refactoring plan: +// * move dynamic template-data-based variable into this struct +// * let backend handle complex logic, prepare everything, avoid plenty of "if" blocks in tmpl +type pullRequestViewInfo struct { + issue *issues_model.Issue - setMergeTarget(ctx, pull) - ctx.Data["HasMerged"] = true + IsPullRequestBroken bool + HeadBranchCommitID string - baseCommit := GetMergedBaseCommitID(ctx, issue) + CompareInfo git_service.CompareInfo + ProtectedBranchRule *git_model.ProtectedBranch + StatusCheckData *pullCommitStatusCheckData + CommitStatuses []*git_model.CommitStatus + MergeBoxData *pullMergeBoxData +} - compareInfo, err := git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo, - git.RefName(baseCommit), git.RefName(pull.GetGitHeadRefName()), false, false) +func newPullRequestViewInfo() *pullRequestViewInfo { + return &pullRequestViewInfo{} +} + +func (prInfo *pullRequestViewInfo) prepareViewInfo(ctx *context.Context, issue *issues_model.Issue) { + prInfo.issue = issue + ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes + + if err := issue.PullRequest.LoadHeadRepo(ctx); err != nil { + ctx.ServerError("LoadHeadRepo", err) + return + } + + if err := issue.PullRequest.LoadBaseRepo(ctx); err != nil { + ctx.ServerError("LoadBaseRepo", err) + return + } + + // for the PR target branch selector + ctx.Data["BaseBranch"] = issue.PullRequest.BaseBranch + ctx.Data["HeadBranch"] = issue.PullRequest.HeadBranch + ctx.Data["HeadUserName"] = issue.PullRequest.MustHeadUserName(ctx) + + if issue.PullRequest.HasMerged { + prInfo.prepareViewMergedPullInfo(ctx) + } else { + prInfo.prepareViewOpenPullInfo(ctx) + } +} + +func (prInfo *pullRequestViewInfo) prepareViewFillInfo(ctx *context.Context, baseRef git.RefName) { + prInfo.prepareViewFillCompareInfo(ctx, baseRef) + if ctx.Written() { + return + } + prInfo.prepareViewFillCommitStatusInfo(ctx) +} + +func (prInfo *pullRequestViewInfo) prepareViewFillCompareInfo(ctx *context.Context, baseRef git.RefName) { + var err error + pull := prInfo.issue.PullRequest + prInfo.CompareInfo, err = git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo, baseRef, git.RefName(pull.GetGitHeadRefName()), false, false) if err != nil { - if gitcmd.IsStdErrorNotValidObjectName(err) || strings.Contains(err.Error(), "unknown revision or path not in the working tree") { - ctx.Data["IsPullRequestBroken"] = true - ctx.Data["BaseTarget"] = pull.BaseBranch - ctx.Data["NumCommits"] = 0 - ctx.Data["NumFiles"] = 0 - return nil - } - - ctx.ServerError("GetCompareInfo", err) - return nil - } - ctx.Data["NumCommits"] = len(compareInfo.Commits) - ctx.Data["NumFiles"] = compareInfo.NumFiles - - if len(compareInfo.Commits) != 0 { - sha := compareInfo.Commits[0].ID.String() - commitStatuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll) - if err != nil { - ctx.ServerError("GetLatestCommitStatus", err) - return nil - } - if !ctx.Repo.CanRead(unit.TypeActions) { - git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) - } - - if len(commitStatuses) != 0 { - ctx.Data["LatestCommitStatuses"] = commitStatuses - ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses) + isKnownErrorForBroken := gitcmd.IsStdErrorNotValidObjectName(err) || + // fatal: ambiguous argument 'origin': unknown revision or path not in the working tree. + gitcmd.StderrContains(err, "unknown revision or path not in the working tree") + if !isKnownErrorForBroken { + log.Error("GetCompareInfo: %v", err) } + prInfo.IsPullRequestBroken = true } - return compareInfo + prInfo.HeadBranchCommitID, err = getViewPullHeadBranchCommitID(ctx, pull) + if err != nil { + if !errors.Is(err, util.ErrNotExist) { + log.Error("GetViewPullHeadBranchCommitID: %v", err) + } + prInfo.IsPullRequestBroken = true + } + if !pull.Issue.IsClosed && (prInfo.HeadBranchCommitID != prInfo.CompareInfo.HeadCommitID) { + // if the PR is still open, but its "branch commit in head repo" + // doesn't match "the PR's internal git ref commit in base repo", then the PR is broken + prInfo.IsPullRequestBroken = true + } + + ctx.Data["IsPullRequestBroken"] = prInfo.IsPullRequestBroken + ctx.Data["NumCommits"] = len(prInfo.CompareInfo.Commits) + ctx.Data["NumFiles"] = prInfo.CompareInfo.NumFiles + prInfo.setTemplateDataMergeTarget(ctx) +} + +func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfo(ctx *context.Context) { + headCommitID := prInfo.CompareInfo.HeadCommitID + if headCommitID == "" { + return + } + + repo := ctx.Repo.Repository + statusCheckData := &pullCommitStatusCheckData{} + prInfo.StatusCheckData = statusCheckData + + commitStatuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, prInfo.CompareInfo.HeadCommitID, db.ListOptionsAll) + if err != nil { + ctx.ServerError("GetLatestCommitStatus", err) + return + } + if !ctx.Repo.Permission.CanRead(unit.TypeActions) { + git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) + } + + prInfo.CommitStatuses = commitStatuses + statusCheckData.ApproveLink = fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s", repo.Link(), headCommitID) + statusCheckData.LatestCommitStatus = git_model.CalcCommitStatus(commitStatuses) + ctx.Data["LatestCommitStatuses"] = commitStatuses + ctx.Data["LatestCommitStatus"] = statusCheckData.LatestCommitStatus + ctx.Data["StatusCheckData"] = prInfo.StatusCheckData + + prInfo.ProtectedBranchRule, err = git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, prInfo.issue.PullRequest.BaseBranch) + if err != nil { + ctx.ServerError("GetFirstMatchProtectedBranchRule", err) + return + } + + if !prInfo.issue.IsClosed { + prInfo.prepareViewFillCommitStatusInfoForOpen(ctx) + } +} + +func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfoForOpen(ctx *context.Context) { + statusCheckData := prInfo.StatusCheckData + commitStatuses := prInfo.CommitStatuses + runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses) + if err != nil { + ctx.ServerError("GetRunsFromCommitStatuses", err) + return + } + for _, run := range runs { + if run.NeedApproval { + statusCheckData.RequireApprovalRunCount++ + } + } + if statusCheckData.RequireApprovalRunCount > 0 { + statusCheckData.CanApprove = ctx.Repo.Permission.CanWrite(unit.TypeActions) + } + + pb := prInfo.ProtectedBranchRule + enableStatusCheck := pb != nil && pb.EnableStatusCheck + if !enableStatusCheck { + return + } + + var missingRequiredChecks []string + for _, requiredContext := range pb.StatusCheckContexts { + contextFound := false + matchesRequiredContext := createRequiredContextMatcher(requiredContext) + for _, presentStatus := range commitStatuses { + if matchesRequiredContext(presentStatus.Context) { + contextFound = true + break + } + } + + if !contextFound { + missingRequiredChecks = append(missingRequiredChecks, requiredContext) + } + } + statusCheckData.MissingRequiredChecks = missingRequiredChecks + + statusCheckData.IsContextRequired = func(context string) bool { + for _, c := range pb.StatusCheckContexts { + if c == context { + return true + } + if gp, err := glob.Compile(c); err != nil { + // All newly created status_check_contexts are checked to ensure they are valid glob expressions before being stored in the database. + // But some old status_check_context created before glob was introduced may be invalid glob expressions. + // So log the error here for debugging. + log.Error("compile glob %q: %v", c, err) + } else if gp.Match(context) { + return true + } + } + return false + } + statusCheckData.RequiredChecksState = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pb.StatusCheckContexts) +} + +// prepareViewMergedPullInfo show meta information for a merged pull request view page +func (prInfo *pullRequestViewInfo) prepareViewMergedPullInfo(ctx *context.Context) { + ctx.Data["HasMerged"] = true + baseCommit := GetMergedBaseCommitID(ctx, prInfo.issue) + prInfo.prepareViewFillInfo(ctx, git.RefName(baseCommit)) } type pullCommitStatusCheckData struct { @@ -344,275 +499,53 @@ func (d *pullCommitStatusCheckData) CommitStatusCheckPrompt(locale translation.L return locale.TrString("repo.pulls.status_checking") } -func getViewPullHeadBranchInfo(ctx *context.Context, pull *issues_model.PullRequest, baseGitRepo *git.Repository) (headCommitID string, headCommitExists bool, err error) { - if pull.HeadRepo == nil { - return "", false, nil - } - headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pull.HeadRepo) - if err != nil { - return "", false, util.Iif(errors.Is(err, util.ErrNotExist), nil, err) - } - defer closer.Close() - - if pull.Flow == issues_model.PullRequestFlowGithub { - headCommitExists, _ = git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch) - } else { - headCommitExists = gitrepo.IsReferenceExist(ctx, pull.BaseRepo, pull.GetGitHeadRefName()) - } - - if headCommitExists { - if pull.Flow != issues_model.PullRequestFlowGithub { - headCommitID, err = baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName()) - } else { - headCommitID, err = headGitRepo.GetBranchCommitID(pull.HeadBranch) +func getViewPullHeadBranchCommitID(ctx *context.Context, pull *issues_model.PullRequest) (string, error) { + switch pull.Flow { + case issues_model.PullRequestFlowGithub: + if pull.HeadRepo == nil { + return "", util.ErrNotExist } + headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, pull.HeadRepo) if err != nil { - return "", false, util.Iif(errors.Is(err, util.ErrNotExist), nil, err) + return "", err } + return headGitRepo.GetRefCommitID(git.RefNameFromBranch(pull.HeadBranch).String()) + case issues_model.PullRequestFlowAGit: + baseGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, pull.BaseRepo) + if err != nil { + return "", err + } + return baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName()) } - return headCommitID, headCommitExists, nil + setting.PanicInDevOrTesting("invalid pull request flow type: %v", pull.Flow) + return "", util.ErrNotExist } -// prepareViewPullInfo show meta information for a pull request preview page -func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo { - ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes - - repo := ctx.Repo.Repository - pull := issue.PullRequest - - if err := pull.LoadHeadRepo(ctx); err != nil { - ctx.ServerError("LoadHeadRepo", err) - return nil - } - - if err := pull.LoadBaseRepo(ctx); err != nil { - ctx.ServerError("LoadBaseRepo", err) - return nil - } - - setMergeTarget(ctx, pull) - - pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pull.BaseBranch) - if err != nil { - ctx.ServerError("LoadProtectedBranch", err) - return nil - } - ctx.Data["EnableStatusCheck"] = pb != nil && pb.EnableStatusCheck - - var baseGitRepo *git.Repository - if pull.BaseRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil { - baseGitRepo = ctx.Repo.GitRepo - } else { - baseGitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo) - if err != nil { - ctx.ServerError("OpenRepository", err) - return nil - } - defer baseGitRepo.Close() - } - - statusCheckData := &pullCommitStatusCheckData{} - +func (prInfo *pullRequestViewInfo) prepareViewOpenPullInfo(ctx *context.Context) { + pull := prInfo.issue.PullRequest if exist, _ := git_model.IsBranchExist(ctx, pull.BaseRepo.ID, pull.BaseBranch); !exist { + // if base branch doesn't exist, prepare from the merge base ctx.Data["BaseBranchNotExist"] = true - ctx.Data["IsPullRequestBroken"] = true - ctx.Data["BaseTarget"] = pull.BaseBranch - ctx.Data["HeadTarget"] = pull.HeadBranch - - sha, err := baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName()) - if err != nil { - ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitHeadRefName()), err) - return nil - } - commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll) - if err != nil { - ctx.ServerError("GetLatestCommitStatus", err) - return nil - } - if !ctx.Repo.CanRead(unit.TypeActions) { - git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) - } - - statusCheckData.LatestCommitStatus = git_model.CalcCommitStatus(commitStatuses) - if len(commitStatuses) > 0 { - ctx.Data["LatestCommitStatuses"] = commitStatuses - ctx.Data["LatestCommitStatus"] = statusCheckData.LatestCommitStatus - } - - compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, - git.RefName(pull.MergeBase), git.RefName(pull.GetGitHeadRefName()), false, false) - if err != nil { - if gitcmd.IsStdErrorNotValidObjectName(err) { - ctx.Data["IsPullRequestBroken"] = true - ctx.Data["BaseTarget"] = pull.BaseBranch - ctx.Data["NumCommits"] = 0 - ctx.Data["NumFiles"] = 0 - return nil - } - - ctx.ServerError("GetCompareInfo", err) - return nil - } - - ctx.Data["NumCommits"] = len(compareInfo.Commits) - ctx.Data["NumFiles"] = compareInfo.NumFiles - return compareInfo + prInfo.prepareViewFillInfo(ctx, git.RefName(pull.MergeBase)) + return } - headBranchSha, headBranchExist, err := getViewPullHeadBranchInfo(ctx, pull, baseGitRepo) - if err != nil { - ctx.ServerError("getViewPullHeadBranchInfo", err) - return nil + prInfo.prepareViewFillInfo(ctx, git.RefNameFromBranch(pull.BaseBranch)) + if ctx.Written() { + return } - if headBranchExist { - var err error - ctx.Data["UpdateAllowed"], ctx.Data["UpdateByRebaseAllowed"], err = pull_service.IsUserAllowedToUpdate(ctx, pull, ctx.Doer) - if err != nil { - ctx.ServerError("IsUserAllowedToUpdate", err) - return nil - } - ctx.Data["GetCommitMessages"] = pull_service.GetSquashMergeCommitMessages(ctx, pull) - } else { - ctx.Data["GetCommitMessages"] = "" - } + ctx.Data["PullHeadCommitID"] = prInfo.CompareInfo.HeadCommitID - sha, err := baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName()) - if err != nil { - if git.IsErrNotExist(err) { - ctx.Data["IsPullRequestBroken"] = true - if pull.IsSameRepo() { - ctx.Data["HeadTarget"] = pull.HeadBranch - } else if pull.HeadRepo == nil { - ctx.Data["HeadTarget"] = ctx.Locale.Tr("repo.pull.deleted_branch", pull.HeadBranch) - } else { - ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch - } - ctx.Data["BaseTarget"] = pull.BaseBranch - ctx.Data["NumCommits"] = 0 - ctx.Data["NumFiles"] = 0 - return nil - } - ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitHeadRefName()), err) - return nil - } - - ctx.Data["StatusCheckData"] = statusCheckData - statusCheckData.ApproveLink = fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s", repo.Link(), sha) - - commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll) - if err != nil { - ctx.ServerError("GetLatestCommitStatus", err) - return nil - } - if !ctx.Repo.CanRead(unit.TypeActions) { - git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) - } - - runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses) - if err != nil { - ctx.ServerError("GetRunsFromCommitStatuses", err) - return nil - } - for _, run := range runs { - if run.NeedApproval { - statusCheckData.RequireApprovalRunCount++ - } - } - if statusCheckData.RequireApprovalRunCount > 0 { - statusCheckData.CanApprove = ctx.Repo.CanWrite(unit.TypeActions) - } - - statusCheckData.LatestCommitStatus = git_model.CalcCommitStatus(commitStatuses) - if len(commitStatuses) > 0 { - ctx.Data["LatestCommitStatuses"] = commitStatuses - ctx.Data["LatestCommitStatus"] = statusCheckData.LatestCommitStatus - } - - if pb != nil && pb.EnableStatusCheck { - var missingRequiredChecks []string - for _, requiredContext := range pb.StatusCheckContexts { - contextFound := false - matchesRequiredContext := createRequiredContextMatcher(requiredContext) - for _, presentStatus := range commitStatuses { - if matchesRequiredContext(presentStatus.Context) { - contextFound = true - break - } - } - - if !contextFound { - missingRequiredChecks = append(missingRequiredChecks, requiredContext) - } - } - statusCheckData.MissingRequiredChecks = missingRequiredChecks - - statusCheckData.IsContextRequired = func(context string) bool { - for _, c := range pb.StatusCheckContexts { - if c == context { - return true - } - if gp, err := glob.Compile(c); err != nil { - // All newly created status_check_contexts are checked to ensure they are valid glob expressions before being stored in the database. - // But some old status_check_context created before glob was introduced may be invalid glob expressions. - // So log the error here for debugging. - log.Error("compile glob %q: %v", c, err) - } else if gp.Match(context) { - return true - } - } - return false - } - statusCheckData.RequiredChecksState = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pb.StatusCheckContexts) - } - - ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha - ctx.Data["HeadBranchCommitID"] = headBranchSha - ctx.Data["PullHeadCommitID"] = sha - - if pull.HeadRepo == nil || !headBranchExist || (!pull.Issue.IsClosed && (headBranchSha != sha)) { - ctx.Data["IsPullRequestBroken"] = true - if pull.IsSameRepo() { - ctx.Data["HeadTarget"] = pull.HeadBranch - } else if pull.HeadRepo == nil { - ctx.Data["HeadTarget"] = ctx.Locale.Tr("repo.pull.deleted_branch", pull.HeadBranch) - } else { - ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch - } - } - - compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, - git.RefNameFromBranch(pull.BaseBranch), git.RefName(pull.GetGitHeadRefName()), false, false) - if err != nil { - if gitcmd.IsStdErrorNotValidObjectName(err) { - ctx.Data["IsPullRequestBroken"] = true - ctx.Data["BaseTarget"] = pull.BaseBranch - ctx.Data["NumCommits"] = 0 - ctx.Data["NumFiles"] = 0 - return nil - } - - ctx.ServerError("GetCompareInfo", err) - return nil - } - - if compareInfo.HeadCommitID == compareInfo.MergeBase { + if prInfo.CompareInfo.HeadCommitID == prInfo.CompareInfo.MergeBase { ctx.Data["IsNothingToCompare"] = true } + // this one is used by both sidebar and merge-box if pull.IsWorkInProgress(ctx) { ctx.Data["IsPullWorkInProgress"] = true ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix(ctx) } - - if pull.IsFilesConflicted() { - ctx.Data["IsPullFilesConflicted"] = true - ctx.Data["ConflictedFiles"] = pull.ConflictedFiles - } - - ctx.Data["NumCommits"] = len(compareInfo.Commits) - ctx.Data["NumFiles"] = compareInfo.NumFiles - return compareInfo } func createRequiredContextMatcher(requiredContext string) func(string) bool { @@ -671,11 +604,13 @@ func ViewPullCommits(ctx *context.Context) { if !ok { return } - - prInfo := preparePullViewPullInfo(ctx, issue) + prViewInfo := newPullRequestViewInfo() + prViewInfo.prepareViewInfo(ctx, issue) if ctx.Written() { return - } else if prInfo == nil { + } + prCompareInfo := &prViewInfo.CompareInfo + if prCompareInfo.HeadCommitID == "" { ctx.NotFound(nil) return } @@ -683,7 +618,7 @@ func ViewPullCommits(ctx *context.Context) { ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name - commits, err := processGitCommits(ctx, prInfo.Commits) + commits, err := processGitCommits(ctx, prCompareInfo.Commits) if err != nil { ctx.ServerError("processGitCommits", err) return @@ -691,7 +626,7 @@ func ViewPullCommits(ctx *context.Context) { ctx.Data["Commits"] = commits ctx.Data["CommitCount"] = len(commits) - ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) + ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) // For PR commits page @@ -699,7 +634,6 @@ func ViewPullCommits(ctx *context.Context) { if ctx.Written() { return } - getBranchData(ctx, issue) ctx.HTML(http.StatusOK, tplPullCommits) } @@ -725,46 +659,45 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { gitRepo := ctx.Repo.GitRepo - prInfo := preparePullViewPullInfo(ctx, issue) + prViewInfo := newPullRequestViewInfo() + prViewInfo.prepareViewInfo(ctx, issue) if ctx.Written() { return - } else if prInfo == nil { + } + prCompareInfo := &prViewInfo.CompareInfo + if prCompareInfo.HeadCommitID == "" { ctx.NotFound(nil) return } - headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitHeadRefName()) - if err != nil { - ctx.ServerError("GetRefCommitID", err) - return - } - + headCommitID := prCompareInfo.HeadCommitID isSingleCommit := beforeCommitID == "" && afterCommitID != "" ctx.Data["IsShowingOnlySingleCommit"] = isSingleCommit - isShowAllCommits := (beforeCommitID == "" || beforeCommitID == prInfo.MergeBase) && (afterCommitID == "" || afterCommitID == headCommitID) + isShowAllCommits := (beforeCommitID == "" || beforeCommitID == prCompareInfo.MergeBase) && (afterCommitID == "" || afterCommitID == headCommitID) ctx.Data["IsShowingAllCommits"] = isShowAllCommits if afterCommitID == "" || afterCommitID == headCommitID { afterCommitID = headCommitID } - afterCommit := indexCommit(prInfo.Commits, afterCommitID) + afterCommit := indexCommit(prCompareInfo.Commits, afterCommitID) if afterCommit == nil { ctx.HTTPError(http.StatusBadRequest, "after commit not found in PR commits") return } var beforeCommit *git.Commit + var err error if !isSingleCommit { - if beforeCommitID == "" || beforeCommitID == prInfo.MergeBase { - beforeCommitID = prInfo.MergeBase - // mergebase commit is not in the list of the pull request commits + if beforeCommitID == "" || beforeCommitID == prCompareInfo.MergeBase { + beforeCommitID = prCompareInfo.MergeBase + // merge base commit is not in the list of the pull request commits beforeCommit, err = gitRepo.GetCommit(beforeCommitID) if err != nil { ctx.ServerError("GetCommit", err) return } } else { - beforeCommit = indexCommit(prInfo.Commits, beforeCommitID) + beforeCommit = indexCommit(prCompareInfo.Commits, beforeCommitID) if beforeCommit == nil { ctx.HTTPError(http.StatusBadRequest, "before commit not found in PR commits") return @@ -779,9 +712,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { beforeCommitID = beforeCommit.ID.String() } - ctx.Data["Username"] = ctx.Repo.Owner.Name - ctx.Data["Reponame"] = ctx.Repo.Repository.Name - ctx.Data["MergeBase"] = prInfo.MergeBase + ctx.Data["MergeBase"] = prCompareInfo.MergeBase ctx.Data["AfterCommitID"] = afterCommitID ctx.Data["BeforeCommitID"] = beforeCommitID @@ -856,17 +787,12 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { return } - pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch) - if err != nil { - ctx.ServerError("LoadProtectedBranch", err) - return - } - - if pb != nil { - glob := pb.GetProtectedFilePatterns() - if len(glob) != 0 { + pb := prViewInfo.ProtectedBranchRule + if prViewInfo.ProtectedBranchRule != nil { + protectedFilePatterns := pb.GetProtectedFilePatterns() + if len(protectedFilePatterns) != 0 { for _, file := range diff.Files { - file.IsProtected = pb.IsProtectedFile(glob, file.Name) + file.IsProtected = pb.IsProtectedFile(protectedFilePatterns, file.Name) } } } @@ -899,11 +825,9 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { } ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0 - if ctx.IsSigned && ctx.Doer != nil { - if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil { - ctx.ServerError("CanMarkConversation", err) - return - } + if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil { + ctx.ServerError("CanMarkConversation", err) + return } setCompareContext(ctx, beforeCommit, afterCommit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) @@ -935,9 +859,8 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { ctx.Data["CurrentReview"] = currentReview ctx.Data["PendingCodeCommentNumber"] = numPendingCodeComments - getBranchData(ctx, issue) - ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) - ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) + ctx.Data["IsIssuePoster"] = ctx.Doer != nil && issue.IsPoster(ctx.Doer.ID) + ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled // For files changed page @@ -1100,7 +1023,7 @@ func MergePullRequest(ctx *context.Context) { } // start with merging by checking - if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { + if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, repo_model.MergeStyle(form.Do), form.ForceMerge); err != nil { switch { case errors.Is(err, pull_service.ErrIsClosed): if issue.IsPull { @@ -1120,6 +1043,8 @@ func MergePullRequest(ctx *context.Context) { ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready")) case asymkey_service.IsErrWontSign(err): ctx.JSONError(err.Error()) // has no translation ... + case errors.Is(err, pull_service.ErrHeadCommitsNotAllVerified): + ctx.JSONError(ctx.Tr("repo.pulls.require_signed_head_commits_unverified")) case errors.Is(err, pull_service.ErrDependenciesLeft): ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked")) default: @@ -1355,7 +1280,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") - ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypePullRequests) + ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypePullRequests) var ( repo = ctx.Repo.Repository @@ -1550,7 +1475,7 @@ func UpdatePullRequestTarget(ctx *context.Context) { return } - if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) { + if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)) { ctx.HTTPError(http.StatusForbidden) return } diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index f064058221e..eb8e8fa677e 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -72,7 +72,7 @@ func CreateCodeComment(ctx *context.Context) { } if ctx.HasError() { - ctx.Flash.Error(ctx.Data["ErrorMsg"].(string)) + ctx.Flash.Error(ctx.GetErrMsg()) ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index)) return } @@ -230,7 +230,7 @@ func SubmitReview(ctx *context.Context) { return } if ctx.HasError() { - ctx.Flash.Error(ctx.Data["ErrorMsg"].(string)) + ctx.Flash.Error(ctx.GetErrMsg()) ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index)) return } diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 1372022ae4a..288e107e53b 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -99,18 +99,14 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) } var ok bool - canReadActions := ctx.Repo.CanRead(unit.TypeActions) + canReadActions := ctx.Repo.Permission.CanRead(unit.TypeActions) releaseInfos := make([]*ReleaseInfo, 0, len(releases)) for _, r := range releases { if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok { - r.Publisher, err = user_model.GetPossibleUserByID(ctx, r.PublisherID) + r.PublisherID, r.Publisher, err = user_model.GetPossibleUserByID(ctx, r.PublisherID) if err != nil { - if user_model.IsErrUserNotExist(err) { - r.Publisher = user_model.NewGhostUser() - } else { - return nil, err - } + return nil, err } cacheUsers[r.PublisherID] = r.Publisher } @@ -165,7 +161,7 @@ func Releases(ctx *context.Context) { listOptions.PageSize = setting.API.MaxResponseItems } - writeAccess := ctx.Repo.CanWrite(unit.TypeReleases) + writeAccess := ctx.Repo.Permission.CanWrite(unit.TypeReleases) ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{ @@ -197,7 +193,7 @@ func Releases(ctx *context.Context) { func TagsList(ctx *context.Context) { ctx.Data["PageIsTagList"] = true ctx.Data["Title"] = ctx.Tr("repo.release.tags") - ctx.Data["CanCreateRelease"] = ctx.Repo.CanWrite(unit.TypeReleases) && !ctx.Repo.Repository.IsArchived + ctx.Data["CanCreateRelease"] = ctx.Repo.Permission.CanWrite(unit.TypeReleases) && !ctx.Repo.Repository.IsArchived namePattern := ctx.FormTrim("q") @@ -274,7 +270,7 @@ func releasesOrTagsFeed(ctx *context.Context, isReleasesOnly bool, formatType st func SingleRelease(ctx *context.Context) { ctx.Data["PageIsReleaseList"] = true - writeAccess := ctx.Repo.CanWrite(unit.TypeReleases) + writeAccess := ctx.Repo.Permission.CanWrite(unit.TypeReleases) ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{ diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go index 7ba91afb297..d57886c2738 100644 --- a/routers/web/repo/release_test.go +++ b/routers/web/repo/release_test.go @@ -151,7 +151,7 @@ func TestCalReleaseNumCommitsBehind(t *testing.T) { t.Cleanup(func() { ctx.Repo.GitRepo.Close() }) releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ - IncludeDrafts: ctx.Repo.CanWrite(unit.TypeReleases), + IncludeDrafts: ctx.Repo.Permission.CanWrite(unit.TypeReleases), RepoID: ctx.Repo.Repository.ID, }) assert.NoError(t, err) diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index dd5ec7dd471..c7813feae23 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -322,7 +322,7 @@ func RedirectDownload(ctx *context.Context) { tagNames := []string{vTag} curRepo := ctx.Repo.Repository releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ - IncludeDrafts: ctx.Repo.CanWrite(unit.TypeReleases), + IncludeDrafts: ctx.Repo.Permission.CanWrite(unit.TypeReleases), RepoID: curRepo.ID, TagNames: tagNames, }) @@ -532,7 +532,7 @@ func SearchRepo(ctx *context.Context) { ctx.JSON(http.StatusInternalServerError, nil) return } - if !ctx.Repo.CanRead(unit.TypeActions) { + if !ctx.Repo.Permission.CanRead(unit.TypeActions) { git_model.CommitStatusesHideActionsURL(ctx, latestCommitStatuses) } diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go index dbfd6e08b61..177d10d165c 100644 --- a/routers/web/repo/setting/collaboration.go +++ b/routers/web/repo/setting/collaboration.go @@ -149,7 +149,7 @@ func DeleteCollaboration(ctx *context.Context) { // AddTeamPost response for adding a team to a repository func AddTeamPost(ctx *context.Context) { - if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { + if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.Permission.IsOwner() { ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") return @@ -195,7 +195,7 @@ func AddTeamPost(ctx *context.Context) { // DeleteTeam response for deleting a team from a repository func DeleteTeam(ctx *context.Context) { - if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { + if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.Permission.IsOwner() { ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") return diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 29f3e62b8fc..703d0022504 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -527,7 +527,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) { } if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { - if !validation.IsValidExternalURL(form.ExternalWikiURL) { + if !validation.IsValidURL(form.ExternalWikiURL) { ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error")) ctx.Redirect(repo.Link() + "/settings") return @@ -557,7 +557,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) { } if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { - if !validation.IsValidExternalURL(form.ExternalTrackerURL) { + if !validation.IsValidURL(form.ExternalTrackerURL) { ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error")) ctx.Redirect(repo.Link() + "/settings") return @@ -724,17 +724,17 @@ func handleSettingsPostAdminIndex(ctx *context.Context) { func handleSettingsPostConvert(ctx *context.Context) { form := web.GetForm(ctx).(*forms.RepoSettingForm) repo := ctx.Repo.Repository - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusNotFound) + if !ctx.Repo.Permission.IsOwner() { + ctx.JSONErrorNotFound() return } if repo.Name != form.RepoName { - ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name")) return } if !repo.IsMirror { - ctx.HTTPError(http.StatusNotFound) + ctx.JSONErrorNotFound() return } repo.IsMirror = false @@ -748,14 +748,14 @@ func handleSettingsPostConvert(ctx *context.Context) { } log.Trace("Repository converted from mirror to regular: %s", repo.FullName()) ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed")) - ctx.Redirect(repo.Link()) + ctx.JSONRedirect(repo.Link()) } func handleSettingsPostConvertFork(ctx *context.Context) { form := web.GetForm(ctx).(*forms.RepoSettingForm) repo := ctx.Repo.Repository - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusNotFound) + if !ctx.Repo.Permission.IsOwner() { + ctx.JSONErrorNotFound() return } if err := repo.LoadOwner(ctx); err != nil { @@ -763,12 +763,12 @@ func handleSettingsPostConvertFork(ctx *context.Context) { return } if repo.Name != form.RepoName { - ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name")) return } if !repo.IsFork { - ctx.HTTPError(http.StatusNotFound) + ctx.JSONErrorNotFound() return } @@ -776,7 +776,7 @@ func handleSettingsPostConvertFork(ctx *context.Context) { maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit() msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) ctx.Flash.Error(msg) - ctx.Redirect(repo.Link() + "/settings") + ctx.JSONRedirect(repo.Link() + "/settings") return } @@ -788,25 +788,25 @@ func handleSettingsPostConvertFork(ctx *context.Context) { log.Trace("Repository converted from fork to regular: %s", repo.FullName()) ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed")) - ctx.Redirect(repo.Link()) + ctx.JSONRedirect(repo.Link()) } func handleSettingsPostTransfer(ctx *context.Context) { form := web.GetForm(ctx).(*forms.RepoSettingForm) repo := ctx.Repo.Repository - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusNotFound) + if !ctx.Repo.Permission.IsOwner() { + ctx.JSONErrorNotFound() return } if repo.Name != form.RepoName { - ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name")) return } newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) + ctx.JSONError(ctx.Tr("form.enterred_invalid_owner_name")) return } ctx.ServerError("IsUserExist", err) @@ -816,7 +816,7 @@ func handleSettingsPostTransfer(ctx *context.Context) { if newOwner.Type == user_model.UserTypeOrganization { if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) { // The user shouldn't know about this organization - ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) + ctx.JSONError(ctx.Tr("form.enterred_invalid_owner_name")) return } } @@ -830,14 +830,14 @@ func handleSettingsPostTransfer(ctx *context.Context) { oldFullname := repo.FullName() if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil { if repo_model.IsErrRepoAlreadyExist(err) { - ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) + ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo")) } else if repo_model.IsErrRepoTransferInProgress(err) { - ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) + ctx.JSONError(ctx.Tr("repo.settings.transfer_in_progress")) } else if repo_service.IsRepositoryLimitReached(err) { limit := err.(repo_service.LimitReachedError).Limit - ctx.RenderWithErrDeprecated(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit), tplSettingsOptions, nil) + ctx.JSONError(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit)) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) + ctx.JSONError(ctx.Tr("repo.settings.transfer.blocked_user")) } else { ctx.ServerError("TransferOwnership", err) } @@ -852,12 +852,12 @@ func handleSettingsPostTransfer(ctx *context.Context) { log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName()) ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed")) } - ctx.Redirect(repo.Link() + "/settings") + ctx.JSONRedirect(repo.Link() + "/settings") } func handleSettingsPostCancelTransfer(ctx *context.Context) { repo := ctx.Repo.Repository - if !ctx.Repo.IsOwner() { + if !ctx.Repo.Permission.IsOwner() { ctx.HTTPError(http.StatusNotFound) return } @@ -886,12 +886,12 @@ func handleSettingsPostCancelTransfer(ctx *context.Context) { func handleSettingsPostDelete(ctx *context.Context) { form := web.GetForm(ctx).(*forms.RepoSettingForm) repo := ctx.Repo.Repository - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusNotFound) + if !ctx.Repo.Permission.IsOwner() { + ctx.JSONErrorNotFound() return } if repo.Name != form.RepoName { - ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name")) return } @@ -907,18 +907,18 @@ func handleSettingsPostDelete(ctx *context.Context) { log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name) ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success")) - ctx.Redirect(ctx.Repo.Owner.DashboardLink()) + ctx.JSONRedirect(ctx.Repo.Owner.DashboardLink()) } func handleSettingsPostDeleteWiki(ctx *context.Context) { form := web.GetForm(ctx).(*forms.RepoSettingForm) repo := ctx.Repo.Repository - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusNotFound) + if !ctx.Repo.Permission.IsOwner() { + ctx.JSONErrorNotFound() return } if repo.Name != form.RepoName { - ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name")) return } @@ -929,12 +929,12 @@ func handleSettingsPostDeleteWiki(ctx *context.Context) { log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name) ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") + ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings") } func handleSettingsPostArchive(ctx *context.Context) { repo := ctx.Repo.Repository - if !ctx.Repo.IsOwner() { + if !ctx.Repo.Permission.IsOwner() { ctx.HTTPError(http.StatusForbidden) return } @@ -967,7 +967,7 @@ func handleSettingsPostArchive(ctx *context.Context) { func handleSettingsPostUnarchive(ctx *context.Context) { repo := ctx.Repo.Repository - if !ctx.Repo.IsOwner() { + if !ctx.Repo.Permission.IsOwner() { ctx.HTTPError(http.StatusForbidden) return } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index b455f918456..2d95d5233e3 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -139,7 +139,7 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { if err != nil { log.Error("GetLatestCommitStatus: %v", err) } - if !ctx.Repo.CanRead(unit_model.TypeActions) { + if !ctx.Repo.Permission.CanRead(unit_model.TypeActions) { git_model.CommitStatusesHideActionsURL(ctx, statuses) } @@ -174,12 +174,11 @@ func markupRenderToHTML(ctx *context.Context, renderCtx *markup.RenderContext, r } func checkHomeCodeViewable(ctx *context.Context) { - if ctx.Repo.HasUnits() { + if ctx.Repo.Permission.HasUnits() { if ctx.Repo.Repository.IsBeingCreated() { task, err := admin_model.GetMigratingTask(ctx, ctx.Repo.Repository.ID) if err != nil { if admin_model.IsErrTaskDoesNotExist(err) { - ctx.Data["Repo"] = ctx.Repo ctx.Data["CloneAddr"] = "" ctx.Data["Failed"] = true ctx.HTML(http.StatusOK, tplMigrating) @@ -194,7 +193,6 @@ func checkHomeCodeViewable(ctx *context.Context) { return } - ctx.Data["Repo"] = ctx.Repo ctx.Data["MigrateTask"] = task ctx.Data["CloneAddr"], _ = util.SanitizeURL(cfg.CloneAddr) ctx.Data["Failed"] = task.Status == structs.TaskStatusFailed diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index d1a969cf2d7..cbdc65b6c0b 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -31,7 +31,7 @@ import ( ) func checkOutdatedBranch(ctx *context.Context) { - if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) { + if !(ctx.Repo.Permission.IsAdmin() || ctx.Repo.Permission.IsOwner()) { return } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 1826ca54e1e..39075dbdf6f 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -47,8 +47,8 @@ const ( // MustEnableWiki check if wiki is enabled, if external then redirect func MustEnableWiki(ctx *context.Context) { - if !ctx.Repo.CanRead(unit.TypeWiki) && - !ctx.Repo.CanRead(unit.TypeExternalWiki) { + if !ctx.Repo.Permission.CanRead(unit.TypeWiki) && + !ctx.Repo.Permission.CanRead(unit.TypeExternalWiki) { if log.IsTrace() { log.Trace("Permission Denied: User %-v cannot read %-v or %-v of repo %-v\n"+ "User in repo has Permissions: %-+v", @@ -423,14 +423,14 @@ func renderEditPage(ctx *context.Context) { func WikiPost(ctx *context.Context) { switch ctx.FormString("action") { case "_new": - if !ctx.Repo.CanWrite(unit.TypeWiki) { + if !ctx.Repo.Permission.CanWrite(unit.TypeWiki) { ctx.NotFound(nil) return } NewWikiPost(ctx) return case "_delete": - if !ctx.Repo.CanWrite(unit.TypeWiki) { + if !ctx.Repo.Permission.CanWrite(unit.TypeWiki) { ctx.NotFound(nil) return } @@ -438,7 +438,7 @@ func WikiPost(ctx *context.Context) { return } - if !ctx.Repo.CanWrite(unit.TypeWiki) { + if !ctx.Repo.Permission.CanWrite(unit.TypeWiki) { ctx.NotFound(nil) return } @@ -447,7 +447,7 @@ func WikiPost(ctx *context.Context) { // Wiki renders single wiki page func Wiki(ctx *context.Context) { - ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived + ctx.Data["CanWriteWiki"] = ctx.Repo.Permission.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived switch ctx.FormString("action") { case "_pages": @@ -457,14 +457,14 @@ func Wiki(ctx *context.Context) { WikiRevision(ctx) return case "_edit": - if !ctx.Repo.CanWrite(unit.TypeWiki) { + if !ctx.Repo.Permission.CanWrite(unit.TypeWiki) { ctx.NotFound(nil) return } EditWiki(ctx) return case "_new": - if !ctx.Repo.CanWrite(unit.TypeWiki) { + if !ctx.Repo.Permission.CanWrite(unit.TypeWiki) { ctx.NotFound(nil) return } @@ -506,7 +506,7 @@ func Wiki(ctx *context.Context) { // WikiRevision renders file revision list of wiki page func WikiRevision(ctx *context.Context) { - ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived + ctx.Data["CanWriteWiki"] = ctx.Repo.Permission.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived if !repo_service.HasWiki(ctx, ctx.Repo.Repository) { ctx.Data["Title"] = ctx.Tr("repo.wiki") @@ -544,7 +544,7 @@ func WikiPages(ctx *context.Context) { } ctx.Data["Title"] = ctx.Tr("repo.wiki.pages") - ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived + ctx.Data["CanWriteWiki"] = ctx.Repo.Permission.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived _, commit, err := findWikiRepoCommit(ctx) if err != nil { diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index 3609258440c..9d8b2f08eb1 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -362,10 +362,6 @@ func RunnerUpdatePost(ctx *context.Context) { ctx.JSONRedirect("") } -func RedirectToDefaultSetting(ctx *context.Context) { - ctx.Redirect(ctx.Repo.RepoLink + "/settings/actions/runners") -} - func findActionsRunner(ctx *context.Context, rCtx *runnersCtx) *actions_model.ActionRunner { runnerID := ctx.PathParamInt64("runnerid") opts := &actions_model.FindRunnerOptions{ diff --git a/routers/web/shared/label/label.go b/routers/web/shared/label/label.go index 6968a318c47..4a3c84e32ad 100644 --- a/routers/web/shared/label/label.go +++ b/routers/web/shared/label/label.go @@ -13,7 +13,7 @@ import ( func GetLabelEditForm(ctx *context.Context) *forms.CreateLabelForm { form := web.GetForm(ctx).(*forms.CreateLabelForm) if ctx.HasError() { - ctx.JSONError(ctx.Data["ErrorMsg"].(string)) + ctx.JSONError(ctx.GetErrMsg()) return nil } var err error diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go index 29f4e9520dc..c8842a67e90 100644 --- a/routers/web/shared/secrets/secrets.go +++ b/routers/web/shared/secrets/secrets.go @@ -29,7 +29,7 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) { func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) { form := web.GetForm(ctx).(*forms.AddSecretForm) - s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data), form.Description) + s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.NormalizeStringEOL(form.Data), form.Description) if err != nil { log.Error("CreateOrUpdateSecret failed: %v", err) ctx.JSONError(ctx.Tr("secrets.save_failed")) diff --git a/routers/web/shared/user/block.go b/routers/web/shared/user/block.go index 8a2357623f1..beb8832da17 100644 --- a/routers/web/shared/user/block.go +++ b/routers/web/shared/user/block.go @@ -7,6 +7,8 @@ import ( "errors" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -28,49 +30,51 @@ func BlockedUsers(ctx *context.Context, blocker *user_model.User) { ctx.Data["UserBlocks"] = blocks } -func BlockedUsersPost(ctx *context.Context, blocker *user_model.User) { - form := web.GetForm(ctx).(*forms.BlockUserForm) - if ctx.HasError() { - ctx.ServerError("FormValidation", nil) - return - } - +func blockedUsersPost(ctx *context.Context, form *forms.BlockUserForm, blocker *user_model.User) error { blockee, err := user_model.GetUserByName(ctx, form.Blockee) if err != nil { - ctx.ServerError("GetUserByName", nil) - return + return err } switch form.Action { case "block": - if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note); err != nil { - if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) { - ctx.Flash.Error(ctx.Tr("user.block.block.failure", err.Error())) - } else { - ctx.ServerError("BlockUser", err) - return - } + err = user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note) + if errors.Is(err, util.ErrInvalidArgument) { + return util.ErrorWrapTranslatable(err, "user.block.block.failure", err.Error()) } + return err case "unblock": - if err := user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee); err != nil { - if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) { - ctx.Flash.Error(ctx.Tr("user.block.unblock.failure", err.Error())) - } else { - ctx.ServerError("UnblockUser", err) - return - } + err = user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee) + if errors.Is(err, util.ErrInvalidArgument) { + return util.ErrorWrapTranslatable(err, "user.block.unblock.failure", err.Error()) } + return err case "note": block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID) if err != nil { - ctx.ServerError("GetBlocking", err) - return - } - if block != nil { - if err := user_model.UpdateBlockingNote(ctx, block.ID, form.Note); err != nil { - ctx.ServerError("UpdateBlockingNote", err) - return - } + return err } + return user_model.UpdateBlockingNote(ctx, block.ID, form.Note) + } + setting.PanicInDevOrTesting("Unknown action: %q", form.Action) + return errors.New("unknown action") +} + +func BlockedUsersPost(ctx *context.Context, blocker *user_model.User, redirect string) { + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) + return + } + + form := web.GetForm(ctx).(*forms.BlockUserForm) + err := blockedUsersPost(ctx, form, blocker) + if err == nil { + ctx.JSONRedirect(redirect) + } else if errTr := util.ErrorAsTranslatable(err); errTr != nil { + ctx.JSONError(errTr.Translate(ctx.Locale)) + } else if errors.Is(err, util.ErrNotExist) { + ctx.JSONError(ctx.Locale.Tr("error.not_found")) + } else { + ctx.ServerError("BlockedUsersPost", err) } } diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 2ba45fc5a7d..b1ff9db7a28 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -4,6 +4,7 @@ package user import ( + "errors" "net/url" "code.gitea.io/gitea/models/db" @@ -83,10 +84,9 @@ func prepareContextForProfileBigAvatar(ctx *context.Context) { } if ctx.Doer != nil { - if block, err := user_model.GetBlocking(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil { + ctx.Data["UserBlocking"], err = user_model.GetBlocking(ctx, ctx.Doer.ID, ctx.ContextUser.ID) + if err != nil && !errors.Is(err, util.ErrNotExist) { ctx.ServerError("GetBlocking", err) - } else { - ctx.Data["UserBlocking"] = block } } } diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 9c99a6c8ef3..12fb1ee71b0 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -558,7 +558,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { ctx.ServerError("GetIssuesLastCommitStatus", err) return } - if !ctx.Repo.CanRead(unit.TypeActions) { + if !ctx.Repo.Permission.CanRead(unit.TypeActions) { for key := range commitStatuses { git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) } diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 3b7ecd062b3..8133388c5de 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -247,7 +247,7 @@ func NotificationSubscriptions(ctx *context.Context) { ctx.ServerError("GetIssuesAllCommitStatus", err) return } - if !ctx.Repo.CanRead(unit.TypeActions) { + if !ctx.Repo.Permission.CanRead(unit.TypeActions) { for key := range commitStatuses { git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) } diff --git a/routers/web/user/setting/block.go b/routers/web/user/setting/block.go index 3a1625ccf9b..1376c0417d0 100644 --- a/routers/web/user/setting/block.go +++ b/routers/web/user/setting/block.go @@ -29,10 +29,5 @@ func BlockedUsers(ctx *context.Context) { } func BlockedUsersPost(ctx *context.Context) { - shared_user.BlockedUsersPost(ctx, ctx.Doer) - if ctx.Written() { - return - } - - ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users") + shared_user.BlockedUsersPost(ctx, ctx.Doer, setting.AppSubURL+"/user/settings/blocked_users") } diff --git a/routers/web/web.go b/routers/web/web.go index e0ff54fcff5..d70eb2d02d5 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/routing" + "code.gitea.io/gitea/modules/web/types" "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/web/admin" "code.gitea.io/gitea/routers/web/auth" @@ -91,8 +92,8 @@ func optionsCorsHandler() func(next http.Handler) http.Handler { } type AuthMiddleware struct { - AllowOAuth2 web.PreMiddlewareProvider - AllowBasic web.PreMiddlewareProvider + AllowOAuth2 types.PreMiddlewareProvider + AllowBasic types.PreMiddlewareProvider MiddlewareHandler func(*context.Context) } @@ -101,7 +102,7 @@ func newWebAuthMiddleware() *AuthMiddleware { type keyAllowBasic struct{} webAuth := &AuthMiddleware{} - middlewareSetContextValue := func(key, val any) web.PreMiddlewareProvider { + middlewareSetContextValue := func(key, val any) types.PreMiddlewareProvider { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { dataStore := reqctx.GetRequestDataStore(r.Context()) @@ -260,6 +261,7 @@ func Routes() *web.Router { routes.BeforeRouting(chi_middleware.GetHead) routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler + routes.Methods("GET, HEAD", "/assets/site-manifest.json", misc.SiteManifest) routes.Methods("GET, HEAD, OPTIONS", "/assets/*", routing.MarkLogLevelTrace, public.AssetsCors(), public.FileHandlerFunc()) routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars)) routes.Methods("GET, HEAD", "/repo-avatars/*", avatarStorageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars)) @@ -587,7 +589,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { }) }, reqSignOut) - m.Any("/user/events", routing.MarkLongPolling, events.Events) + m.Any("/user/events", routing.MarkLongPolling(), events.Events) m.Group("/login/oauth", func() { m.Group("", func() { @@ -1539,6 +1541,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Combo(""). Get(actions.View). Post(web.Bind(actions.ViewRequest{}), actions.ViewPost) + m.Group("/attempts/{attempt}", func() { + m.Combo(""). + Get(actions.View). + Post(web.Bind(actions.ViewRequest{}), actions.ViewPost) + }) m.Group("/jobs/{job}", func() { m.Combo(""). Get(actions.View). @@ -1754,8 +1761,10 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Any("/mail-preview/*", devtest.MailPreviewRender) m.Any("/{sub}", devtest.TmplCommon) m.Get("/repo-action-view/runs/{run}", devtest.MockActionsView) + m.Get("/repo-action-view/runs/{run}/attempts/{attempt}", devtest.MockActionsView) m.Get("/repo-action-view/runs/{run}/jobs/{job}", devtest.MockActionsView) m.Post("/repo-action-view/runs/{run}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs) + m.Post("/repo-action-view/runs/{run}/attempts/{attempt}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs) m.Post("/repo-action-view/runs/{run}/jobs/{job}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs) }) } diff --git a/services/actions/approve.go b/services/actions/approve.go new file mode 100644 index 00000000000..552b055b706 --- /dev/null +++ b/services/actions/approve.go @@ -0,0 +1,69 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" +) + +func ApproveRuns(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, runIDs []int64) error { + updatedJobs := make([]*actions_model.ActionRunJob, 0) + cancelledConcurrencyJobs := make([]*actions_model.ActionRunJob, 0) + + err := db.WithTx(ctx, func(ctx context.Context) (err error) { + for _, runID := range runIDs { + run, err := actions_model.GetRunByRepoAndID(ctx, repo.ID, runID) + if err != nil { + return err + } + run.NeedApproval = false + run.ApprovedBy = doer.ID + if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil { + return err + } + jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, repo.ID, run.ID) + if err != nil { + return err + } + for _, job := range jobs { + // Skip jobs with `needs`: they stay blocked until their dependencies finish, + // at which point job_emitter will evaluate and start them. + if len(job.Needs) > 0 { + continue + } + var jobsToCancel []*actions_model.ActionRunJob + job.Status, jobsToCancel, err = PrepareToStartJobWithConcurrency(ctx, job) + if err != nil { + return err + } + cancelledConcurrencyJobs = append(cancelledConcurrencyJobs, jobsToCancel...) + if job.Status == actions_model.StatusWaiting { + n, err := actions_model.UpdateRunJob(ctx, job, nil, "status") + if err != nil { + return err + } + if n > 0 { + updatedJobs = append(updatedJobs, job) + } + } + } + } + return nil + }) + if err != nil { + return err + } + + NotifyWorkflowJobsAndRunsStatusUpdate(ctx, updatedJobs) + NotifyWorkflowJobsAndRunsStatusUpdate(ctx, cancelledConcurrencyJobs) + + EmitJobsIfReadyByJobs(cancelledConcurrencyJobs) + + return nil +} diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go index d0cc63e5388..f223c981255 100644 --- a/services/actions/cleanup.go +++ b/services/actions/cleanup.go @@ -179,7 +179,7 @@ func DeleteRun(ctx context.Context, run *actions_model.ActionRun) error { repoID := run.RepoID - jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + jobs, err := actions_model.GetAllRunJobsByRepoAndRunID(ctx, run.RepoID, run.ID) if err != nil { return err } @@ -207,6 +207,10 @@ func DeleteRun(ctx context.Context, run *actions_model.ActionRun) error { RepoID: repoID, ID: run.ID, }) + recordsToDelete = append(recordsToDelete, &actions_model.ActionRunAttempt{ + RepoID: repoID, + RunID: run.ID, + }) recordsToDelete = append(recordsToDelete, &actions_model.ActionRunJob{ RepoID: repoID, RunID: run.ID, diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index c71f63e7d17..940f1d84544 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -17,7 +17,6 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" - notify_service "code.gitea.io/gitea/services/notify" ) // StopZombieTasks stops the task which have running status, but haven't been updated for a long time @@ -36,39 +35,16 @@ func StopEndlessTasks(ctx context.Context) error { }) } -func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.ActionRunJob) { - if len(jobs) == 0 { - return - } - // The input jobs may belong to different runs, so track each affected run. - runs := make(map[int64]*actions_model.ActionRun, len(jobs)) - for _, job := range jobs { - if err := job.LoadAttributes(ctx); err != nil { - log.Error("Failed to load job attributes: %v", err) - continue - } - CreateCommitStatusForRunJobs(ctx, job.Run, job) - notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) - if _, ok := runs[job.RunID]; !ok { - runs[job.RunID] = job.Run - } - } - - for _, run := range runs { - notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) - } -} - func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { jobs, err := actions_model.CancelPreviousJobs(ctx, repoID, ref, workflowID, event) - notifyWorkflowJobStatusUpdate(ctx, jobs) + NotifyWorkflowJobsAndRunsStatusUpdate(ctx, jobs) EmitJobsIfReadyByJobs(jobs) return err } func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error { jobs, err := actions_model.CleanRepoScheduleTasks(ctx, repo) - notifyWorkflowJobStatusUpdate(ctx, jobs) + NotifyWorkflowJobsAndRunsStatusUpdate(ctx, jobs) EmitJobsIfReadyByJobs(jobs) return err } @@ -83,61 +59,59 @@ func shouldBlockJobByConcurrency(ctx context.Context, job *actions_model.ActionR return false, nil } - runs, jobs, err := actions_model.GetConcurrentRunsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, []actions_model.Status{actions_model.StatusRunning}) + attempts, jobs, err := actions_model.GetConcurrentRunAttemptsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, []actions_model.Status{actions_model.StatusRunning}) if err != nil { - return false, fmt.Errorf("GetConcurrentRunsAndJobs: %w", err) + return false, fmt.Errorf("GetConcurrentRunAttemptsAndJobs: %w", err) } - return len(runs) > 0 || len(jobs) > 0, nil + return len(attempts) > 0 || len(jobs) > 0, nil } // PrepareToStartJobWithConcurrency prepares a job to start by its evaluated concurrency group and cancelling previous jobs if necessary. -// It returns the new status of the job (either StatusBlocked or StatusWaiting) and any error encountered during the process. -func PrepareToStartJobWithConcurrency(ctx context.Context, job *actions_model.ActionRunJob) (actions_model.Status, error) { +// It returns the new status of the job (either StatusBlocked or StatusWaiting), any cancelled jobs, and any error encountered during the process. +func PrepareToStartJobWithConcurrency(ctx context.Context, job *actions_model.ActionRunJob) (actions_model.Status, []*actions_model.ActionRunJob, error) { shouldBlock, err := shouldBlockJobByConcurrency(ctx, job) if err != nil { - return actions_model.StatusBlocked, err + return actions_model.StatusBlocked, nil, err } // even if the current job is blocked, we still need to cancel previous "waiting/blocked" jobs in the same concurrency group jobs, err := actions_model.CancelPreviousJobsByJobConcurrency(ctx, job) if err != nil { - return actions_model.StatusBlocked, fmt.Errorf("CancelPreviousJobsByJobConcurrency: %w", err) + return actions_model.StatusBlocked, nil, fmt.Errorf("CancelPreviousJobsByJobConcurrency: %w", err) } - notifyWorkflowJobStatusUpdate(ctx, jobs) - return util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting), nil + return util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting), jobs, nil } -func shouldBlockRunByConcurrency(ctx context.Context, actionRun *actions_model.ActionRun) (bool, error) { - if actionRun.ConcurrencyGroup == "" || actionRun.ConcurrencyCancel { +func shouldBlockRunByConcurrency(ctx context.Context, attempt *actions_model.ActionRunAttempt) (bool, error) { + if attempt.ConcurrencyGroup == "" || attempt.ConcurrencyCancel { return false, nil } - runs, jobs, err := actions_model.GetConcurrentRunsAndJobs(ctx, actionRun.RepoID, actionRun.ConcurrencyGroup, []actions_model.Status{actions_model.StatusRunning}) + attempts, jobs, err := actions_model.GetConcurrentRunAttemptsAndJobs(ctx, attempt.RepoID, attempt.ConcurrencyGroup, []actions_model.Status{actions_model.StatusRunning}) if err != nil { return false, fmt.Errorf("find concurrent runs and jobs: %w", err) } - return len(runs) > 0 || len(jobs) > 0, nil + return len(attempts) > 0 || len(jobs) > 0, nil } -// PrepareToStartRunWithConcurrency prepares a run to start by its evaluated concurrency group and cancelling previous jobs if necessary. -// It returns the new status of the run (either StatusBlocked or StatusWaiting) and any error encountered during the process. -func PrepareToStartRunWithConcurrency(ctx context.Context, run *actions_model.ActionRun) (actions_model.Status, error) { - shouldBlock, err := shouldBlockRunByConcurrency(ctx, run) +// PrepareToStartRunWithConcurrency prepares a run attempt to start by its evaluated concurrency group and cancelling previous jobs if necessary. +// It returns the new status of the run attempt (either StatusBlocked or StatusWaiting), any cancelled jobs, and any error encountered during the process. +func PrepareToStartRunWithConcurrency(ctx context.Context, attempt *actions_model.ActionRunAttempt) (actions_model.Status, []*actions_model.ActionRunJob, error) { + shouldBlock, err := shouldBlockRunByConcurrency(ctx, attempt) if err != nil { - return actions_model.StatusBlocked, err + return actions_model.StatusBlocked, nil, err } // even if the current run is blocked, we still need to cancel previous "waiting/blocked" jobs in the same concurrency group - jobs, err := actions_model.CancelPreviousJobsByRunConcurrency(ctx, run) + jobs, err := actions_model.CancelPreviousJobsByRunConcurrency(ctx, attempt) if err != nil { - return actions_model.StatusBlocked, fmt.Errorf("CancelPreviousJobsByRunConcurrency: %w", err) + return actions_model.StatusBlocked, nil, fmt.Errorf("CancelPreviousJobsByRunConcurrency: %w", err) } - notifyWorkflowJobStatusUpdate(ctx, jobs) - return util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting), nil + return util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting), jobs, nil } func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error { @@ -175,7 +149,7 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error { remove() } - notifyWorkflowJobStatusUpdate(ctx, jobs) + NotifyWorkflowJobsAndRunsStatusUpdate(ctx, jobs) EmitJobsIfReadyByJobs(jobs) return nil @@ -194,8 +168,6 @@ func CancelAbandonedJobs(ctx context.Context) error { now := timeutil.TimeStampNow() - // Collect one job per run to send workflow run status update - updatedRuns := map[int64]*actions_model.ActionRunJob{} updatedJobs := []*actions_model.ActionRunJob{} for _, job := range jobs { @@ -211,9 +183,6 @@ func CancelAbandonedJobs(ctx context.Context) error { return err } updated = n > 0 - if updated && job.Run.Status.IsDone() { - updatedRuns[job.RunID] = job - } return nil }); err != nil { log.Warn("cancel abandoned job %v: %v", job.ID, err) @@ -222,16 +191,13 @@ func CancelAbandonedJobs(ctx context.Context) error { if job.Run == nil || job.Run.Repo == nil { continue // error occurs during loading attributes, the following code that depends on "Run.Repo" will fail, so ignore and skip } - CreateCommitStatusForRunJobs(ctx, job.Run, job) if updated { + CreateCommitStatusForRunJobs(ctx, job.Run, job) updatedJobs = append(updatedJobs, job) - notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } } - for _, job := range updatedRuns { - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) - } + NotifyWorkflowJobsAndRunsStatusUpdate(ctx, updatedJobs) EmitJobsIfReadyByJobs(updatedJobs) return nil diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index 95b848f4fb1..5a7f8f1f443 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "path" - "strconv" "strings" actions_model "code.gitea.io/gitea/models/actions" @@ -143,57 +142,59 @@ func createCommitStatus(ctx context.Context, repo *repo_model.Repository, event, if wfs, err := jobparser.Parse(job.WorkflowPayload); err == nil && len(wfs) > 0 { runName = wfs[0].Name } - ctxName := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event) - ctxName = strings.TrimSpace(ctxName) // git_model.NewCommitStatus also trims spaces + ctxName := strings.TrimSpace(fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)) // git_model.NewCommitStatus also trims spaces state := toCommitStatus(job.Status) - if statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll); err == nil { - for _, v := range statuses { - if v.Context == ctxName { - if v.State == state { - // no need to update - return nil - } - break - } - } - } else { + targetURL := fmt.Sprintf("%s/jobs/%d", run.Link(), job.ID) + description := toCommitStatusDescription(job) + + statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll) + if err != nil { return fmt.Errorf("GetLatestCommitStatus: %w", err) } - - var description string - switch job.Status { - // TODO: if we want support description in different languages, we need to support i18n placeholders in it - case actions_model.StatusSuccess: - description = fmt.Sprintf("Successful in %s", job.Duration()) - case actions_model.StatusFailure: - description = fmt.Sprintf("Failing after %s", job.Duration()) - case actions_model.StatusCancelled: - description = "Has been cancelled" - case actions_model.StatusSkipped: - description = "Has been skipped" - case actions_model.StatusRunning: - description = "Has started running" - case actions_model.StatusWaiting: - description = "Waiting to run" - case actions_model.StatusBlocked: - description = "Blocked by required conditions" - default: - description = "Unknown status: " + strconv.Itoa(int(job.Status)) + for _, v := range statuses { + if v.Context == ctxName { + if v.State == state && v.TargetURL == targetURL && v.Description == description { + return nil + } + break + } } creator := user_model.NewActionsUser() status := git_model.CommitStatus{ SHA: commitID, - TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), job.ID), + TargetURL: targetURL, Description: description, Context: ctxName, - CreatorID: creator.ID, State: state, + CreatorID: creator.ID, } return commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID, &status) } +func toCommitStatusDescription(job *actions_model.ActionRunJob) string { + switch job.Status { + // TODO: if we want support description in different languages, we need to support i18n placeholders in it + case actions_model.StatusSuccess: + return fmt.Sprintf("Successful in %s", job.Duration()) + case actions_model.StatusFailure: + return fmt.Sprintf("Failing after %s", job.Duration()) + case actions_model.StatusCancelled: + return "Has been cancelled" + case actions_model.StatusSkipped: + return "Has been skipped" + case actions_model.StatusRunning: + return "Has started running" + case actions_model.StatusWaiting: + return "Waiting to run" + case actions_model.StatusBlocked: + return "Blocked by required conditions" + default: + return fmt.Sprintf("Unknown status: %d", job.Status) + } +} + func toCommitStatus(status actions_model.Status) commitstatus.CommitStatusState { switch status { case actions_model.StatusSuccess: diff --git a/services/actions/commit_status_test.go b/services/actions/commit_status_test.go new file mode 100644 index 00000000000..6ff93933189 --- /dev/null +++ b/services/actions/commit_status_test.go @@ -0,0 +1,88 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/commitstatus" + "code.gitea.io/gitea/modules/gitrepo" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateCommitStatus_Dedupe(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + require.NoError(t, err) + defer gitRepo.Close() + + commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch) + require.NoError(t, err) + + run := &actions_model.ActionRun{ + ID: 99001, + RepoID: repo.ID, + Repo: repo, + WorkflowID: "status-dedupe-test.yaml", + } + job := &actions_model.ActionRunJob{ + ID: 99002, + RunID: run.ID, + RepoID: repo.ID, + Name: "status-dedupe-job", + Status: actions_model.StatusWaiting, + } + + expectedContext := "status-dedupe-test.yaml / status-dedupe-job (push)" + expectedTargetURL := run.Link() + "/jobs/99002" + + require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), run, job)) + + statuses := findCommitStatusesForContext(t, repo.ID, commit.ID.String(), expectedContext) + require.Len(t, statuses, 1) + assert.Equal(t, commitstatus.CommitStatusPending, statuses[0].State) + assert.Equal(t, "Waiting to run", statuses[0].Description) + assert.Equal(t, expectedTargetURL, statuses[0].TargetURL) + + job.Status = actions_model.StatusRunning + require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), run, job)) + + statuses = findCommitStatusesForContext(t, repo.ID, commit.ID.String(), expectedContext) + require.Len(t, statuses, 2) + assert.Equal(t, "Waiting to run", statuses[0].Description) + assert.Equal(t, commitstatus.CommitStatusPending, statuses[1].State) + assert.Equal(t, "Has started running", statuses[1].Description) + assert.Equal(t, expectedTargetURL, statuses[1].TargetURL) + + require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), run, job)) + statuses = findCommitStatusesForContext(t, repo.ID, commit.ID.String(), expectedContext) + assert.Len(t, statuses, 2) + + job.Status = actions_model.StatusSuccess + require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), run, job)) + statuses = findCommitStatusesForContext(t, repo.ID, commit.ID.String(), expectedContext) + require.Len(t, statuses, 3) + assert.Equal(t, commitstatus.CommitStatusSuccess, statuses[2].State) +} + +func findCommitStatusesForContext(t *testing.T, repoID int64, sha, context string) []*git_model.CommitStatus { + t.Helper() + + var statuses []*git_model.CommitStatus + err := db.GetEngine(t.Context()). + Where("repo_id = ? AND sha = ? AND context = ?", repoID, sha, context). + Asc("`index`"). + Find(&statuses) + require.NoError(t, err) + return statuses +} diff --git a/services/actions/concurrency.go b/services/actions/concurrency.go index 878e5c483bf..e1ec5499309 100644 --- a/services/actions/concurrency.go +++ b/services/actions/concurrency.go @@ -17,15 +17,15 @@ import ( ) // EvaluateRunConcurrencyFillModel evaluates the expressions in a run-level (workflow) concurrency, -// and fills the run's model fields with `concurrency.group` and `concurrency.cancel-in-progress`. +// and fills the run attempt model with the evaluated `concurrency.group` and `concurrency.cancel-in-progress` values. // Workflow-level concurrency doesn't depend on the job outputs, so it can always be evaluated if there is no syntax error. // See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency -func EvaluateRunConcurrencyFillModel(ctx context.Context, run *actions_model.ActionRun, wfRawConcurrency *act_model.RawConcurrency, vars map[string]string, inputs map[string]any) error { +func EvaluateRunConcurrencyFillModel(ctx context.Context, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt, wfRawConcurrency *act_model.RawConcurrency, vars map[string]string, inputs map[string]any) error { if err := run.LoadAttributes(ctx); err != nil { return fmt.Errorf("run LoadAttributes: %w", err) } - actionsRunCtx := GenerateGiteaContext(run, nil) + actionsRunCtx := GenerateGiteaContext(ctx, run, attempt, nil) jobResults := map[string]*jobparser.JobResult{"": {}} if inputs == nil { var err error @@ -35,12 +35,8 @@ func EvaluateRunConcurrencyFillModel(ctx context.Context, run *actions_model.Act } } - rawConcurrency, err := yaml.Marshal(wfRawConcurrency) - if err != nil { - return fmt.Errorf("marshal raw concurrency: %w", err) - } - run.RawConcurrency = string(rawConcurrency) - run.ConcurrencyGroup, run.ConcurrencyCancel, err = jobparser.EvaluateConcurrency(wfRawConcurrency, "", nil, actionsRunCtx, jobResults, vars, inputs) + var err error + attempt.ConcurrencyGroup, attempt.ConcurrencyCancel, err = jobparser.EvaluateConcurrency(wfRawConcurrency, "", nil, actionsRunCtx, jobResults, vars, inputs) if err != nil { return fmt.Errorf("evaluate concurrency: %w", err) } @@ -71,7 +67,7 @@ func findJobNeedsAndFillJobResults(ctx context.Context, job *actions_model.Actio // Job-level concurrency may depend on other job's outputs (via `needs`): `concurrency.group: my-group-${{ needs.job1.outputs.out1 }}` // If the needed jobs haven't been executed yet, this evaluation will also fail. // See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idconcurrency -func EvaluateJobConcurrencyFillModel(ctx context.Context, run *actions_model.ActionRun, actionRunJob *actions_model.ActionRunJob, vars map[string]string, inputs map[string]any) error { +func EvaluateJobConcurrencyFillModel(ctx context.Context, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt, actionRunJob *actions_model.ActionRunJob, vars map[string]string, inputs map[string]any) error { if err := actionRunJob.LoadAttributes(ctx); err != nil { return fmt.Errorf("job LoadAttributes: %w", err) } @@ -81,7 +77,7 @@ func EvaluateJobConcurrencyFillModel(ctx context.Context, run *actions_model.Act return fmt.Errorf("unmarshal raw concurrency: %w", err) } - actionsJobCtx := GenerateGiteaContext(run, actionRunJob) + actionsJobCtx := GenerateGiteaContext(ctx, run, attempt, actionRunJob) jobResults, err := findJobNeedsAndFillJobResults(ctx, actionRunJob) if err != nil { diff --git a/services/actions/context.go b/services/actions/context.go index 626ae6ee6bf..9250c409835 100644 --- a/services/actions/context.go +++ b/services/actions/context.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -22,9 +23,14 @@ import ( type GiteaContext map[string]any -// GenerateGiteaContext generate the gitea context without token and gitea_runtime_token -// job can be nil when generating a context for parsing workflow-level expressions -func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.ActionRunJob) GiteaContext { +// GenerateGiteaContext generate the gitea context without token and gitea_runtime_token. +// attempt and job can be nil when generating a context for parsing workflow-level expressions. +// +// The run_attempt value is resolved with the following precedence: +// 1. attempt.Attempt - the explicit attempt argument, or run.GetLatestAttempt() as a fallback +// 2. job.Attempt - only used when neither an explicit nor latest attempt is available +// 3. "1" - when none of the above apply (first-run parse time, before the first attempt exists) +func GenerateGiteaContext(ctx context.Context, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt, job *actions_model.ActionRunJob) GiteaContext { event := map[string]any{} _ = json.Unmarshal([]byte(run.EventPayload), &event) @@ -73,7 +79,7 @@ func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.Actio "repository_owner": run.Repo.OwnerName, // string, The repository owner's name. For example, Codertocat. "repositoryUrl": run.Repo.HTMLURL(), // string, The Git URL to the repository. For example, git://github.com/codertocat/hello-world.git. "retention_days": "", // string, The number of days that workflow run logs and artifacts are kept. - "run_id": "", // string, A unique number for each workflow run within a repository. This number does not change if you re-run the workflow run. + "run_id": strconv.FormatInt(run.ID, 10), // string, A unique number for each workflow run within a repository. This number does not change if you re-run the workflow run. "run_number": strconv.FormatInt(run.Index, 10), // string, A unique number for each run of a particular workflow in a repository. This number begins at 1 for the workflow's first run, and increments with each new run. This number does not change if you re-run the workflow run. "run_attempt": "", // string, A unique number for each attempt of a particular workflow run in a repository. This number begins at 1 for the workflow run's first attempt, and increments with each re-run. "secret_source": "Actions", // string, The source of a secret used in a workflow. Possible values are None, Actions, Dependabot, or Codespaces. @@ -89,10 +95,28 @@ func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.Actio if job != nil { gitContext["job"] = job.JobID - gitContext["run_id"] = strconv.FormatInt(job.RunID, 10) gitContext["run_attempt"] = strconv.FormatInt(job.Attempt, 10) } + if attempt == nil { + if latestAttempt, has, err := run.GetLatestAttempt(ctx); err == nil && has { + attempt = latestAttempt + } + } + + if attempt != nil { + gitContext["run_attempt"] = strconv.FormatInt(attempt.Attempt, 10) + if err := attempt.LoadAttributes(ctx); err == nil { + gitContext["triggering_actor"] = attempt.TriggerUser.Name + } + } + + // Fallback for first-run parse time: no job, no attempt (LatestAttemptID==0). github.run_attempt + // is 1-based per the documented contract, so emit "1" rather than leaving it empty. + if gitContext["run_attempt"] == "" { + gitContext["run_attempt"] = "1" + } + return gitContext } @@ -108,7 +132,13 @@ func FindTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[st } needs := container.SetOf(job.Needs...) - jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: job.RunID}) + // Scope to the same attempt. For legacy jobs RunAttemptID==0, which matches all other legacy jobs in the same run. + findOpts := actions_model.FindRunJobOptions{ + RunID: job.RunID, + RunAttemptID: optional.Some(job.RunAttemptID), + } + + jobs, err := db.Find[actions_model.ActionRunJob](ctx, findOpts) if err != nil { return nil, fmt.Errorf("FindRunJobs: %w", err) } @@ -125,11 +155,12 @@ func FindTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[st } var jobOutputs map[string]string for _, job := range jobsWithSameID { - if job.TaskID == 0 || !job.Status.IsDone() { - // it shouldn't happen, or the job has been rerun + taskID := job.EffectiveTaskID() + if taskID == 0 || !job.Status.IsDone() { + // it shouldn't happen continue } - got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) + got, err := actions_model.FindTaskOutputByTaskID(ctx, taskID) if err != nil { return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) } diff --git a/services/actions/context_test.go b/services/actions/context_test.go index 74ef694021a..22f9abcce81 100644 --- a/services/actions/context_test.go +++ b/services/actions/context_test.go @@ -4,14 +4,92 @@ package actions import ( + "strconv" "testing" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/unittest" + act_model "github.com/nektos/act/pkg/model" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func TestEvaluateRunConcurrency_RunIDFallback(t *testing.T) { + // Unit-level check that EvaluateRunConcurrencyFillModel resolves + // github.run_id from run.ID. The full-flow regression — that run.ID is + // non-zero by the time evaluation happens — is in + // TestPrepareRunAndInsert_ExpressionsSeeRunID. + assert.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + runA := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 791}) + runB := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 792}) + + attemptA := &actions_model.ActionRunAttempt{RepoID: runA.RepoID, RunID: runA.ID, Attempt: 1} + attemptB := &actions_model.ActionRunAttempt{RepoID: runB.RepoID, RunID: runB.ID, Attempt: 1} + + expr := &act_model.RawConcurrency{ + Group: "${{ github.workflow }}-${{ github.head_ref || github.run_id }}", + CancelInProgress: "true", + } + + assert.NoError(t, EvaluateRunConcurrencyFillModel(ctx, runA, attemptA, expr, nil, nil)) + assert.NoError(t, EvaluateRunConcurrencyFillModel(ctx, runB, attemptB, expr, nil, nil)) + + assert.Contains(t, attemptA.ConcurrencyGroup, "791") + assert.Contains(t, attemptB.ConcurrencyGroup, "792") + assert.NotEqual(t, attemptA.ConcurrencyGroup, attemptB.ConcurrencyGroup) +} + +func TestPrepareRunAndInsert_ExpressionsSeeRunID(t *testing.T) { + // Regression for the cross-branch concurrency leak: github.run_id must + // be available during BOTH jobparser.Parse (run-name) and workflow-level + // concurrency evaluation. Re-ordering db.Insert relative to either step + // would leave run.ID at 0 and break this test. + assert.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + content := []byte(`name: cross-branch +run-name: "Run ${{ github.run_id }}" +on: push +concurrency: + group: group-${{ github.run_id }} + cancel-in-progress: true +jobs: + hello: + runs-on: ubuntu-latest + steps: + - run: echo hi +`) + + run := &actions_model.ActionRun{ + Title: "before parse", + RepoID: 4, + OwnerID: 1, + WorkflowID: "expr-runid.yaml", + TriggerUserID: 1, + Ref: "refs/heads/master", + CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0", + Event: "push", + TriggerEvent: "push", + EventPayload: "{}", + } + require.NoError(t, PrepareRunAndInsert(ctx, content, run, nil)) + require.Positive(t, run.ID) + + persisted := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID}) + runIDStr := strconv.FormatInt(run.ID, 10) + assert.Equal(t, "Run "+runIDStr, persisted.Title) + // ConcurrencyGroup lives on the latest attempt after migration v331. + require.Positive(t, persisted.LatestAttemptID) + attempt := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunAttempt{ID: persisted.LatestAttemptID}) + assert.Equal(t, "group-"+runIDStr, attempt.ConcurrencyGroup) + // Rerun reads raw_concurrency from the DB to re-evaluate the group; + // see services/actions/rerun.go. Must survive the insert. + assert.NotEmpty(t, persisted.RawConcurrency) +} + func TestFindTaskNeeds(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index c7813360abd..489b36a3a7d 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" - notify_service "code.gitea.io/gitea/services/notify" "xorm.io/builder" ) @@ -70,30 +69,33 @@ func checkJobsByRunID(ctx context.Context, runID int64) error { if err != nil { return fmt.Errorf("get action run: %w", err) } - var jobs, updatedJobs []*actions_model.ActionRunJob + var jobs, updatedJobs, cancelledJobs []*actions_model.ActionRunJob if err := db.WithTx(ctx, func(ctx context.Context) error { // check jobs of the current run - if js, ujs, err := checkJobsOfRun(ctx, run); err != nil { + if js, ujs, cjs, err := checkJobsOfCurrentRunAttempt(ctx, run); err != nil { return err } else { jobs = append(jobs, js...) updatedJobs = append(updatedJobs, ujs...) + cancelledJobs = append(cancelledJobs, cjs...) } - if js, ujs, err := checkRunConcurrency(ctx, run); err != nil { + if js, ujs, cjs, err := checkRunConcurrency(ctx, run); err != nil { return err } else { jobs = append(jobs, js...) updatedJobs = append(updatedJobs, ujs...) + cancelledJobs = append(cancelledJobs, cjs...) } return nil }); err != nil { return err } - CreateCommitStatusForRunJobs(ctx, run, jobs...) - for _, job := range updatedJobs { - _ = job.LoadAttributes(ctx) - notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + NotifyWorkflowJobsAndRunsStatusUpdate(ctx, cancelledJobs) + EmitJobsIfReadyByJobs(cancelledJobs) + if err := createCommitStatusesForJobsByRun(ctx, jobs); err != nil { + return err } + NotifyWorkflowJobsStatusUpdate(ctx, updatedJobs...) runJobs := make(map[int64][]*actions_model.ActionRunJob) for _, job := range jobs { runJobs[job.RunID] = append(runJobs[job.RunID], job) @@ -114,71 +116,97 @@ func checkJobsByRunID(ctx context.Context, runID int64) error { } } if runUpdated { - NotifyWorkflowRunStatusUpdateWithReload(ctx, js[0]) + NotifyWorkflowRunStatusUpdateWithReload(ctx, js[0].RepoID, js[0].RunID) } } return nil } -// findBlockedRunByConcurrency finds the blocked concurrent run in a repo and returns `nil, nil` when there is no blocked run. -func findBlockedRunByConcurrency(ctx context.Context, repoID int64, concurrencyGroup string) (*actions_model.ActionRun, error) { - if concurrencyGroup == "" { - return nil, nil //nolint:nilnil // return nil to indicate that no blocked run exists - } - cRuns, cJobs, err := actions_model.GetConcurrentRunsAndJobs(ctx, repoID, concurrencyGroup, []actions_model.Status{actions_model.StatusBlocked}) - if err != nil { - return nil, fmt.Errorf("find concurrent runs and jobs: %w", err) +func createCommitStatusesForJobsByRun(ctx context.Context, jobs []*actions_model.ActionRunJob) error { + runJobs := make(map[int64][]*actions_model.ActionRunJob) + for _, job := range jobs { + runJobs[job.RunID] = append(runJobs[job.RunID], job) } - // There can be at most one blocked run or job - var concurrentRun *actions_model.ActionRun - if len(cRuns) > 0 { - concurrentRun = cRuns[0] - } else if len(cJobs) > 0 { - jobRun, exist, err := db.GetByID[actions_model.ActionRun](ctx, cJobs[0].RunID) - if !exist { - return nil, fmt.Errorf("run %d does not exist", cJobs[0].RunID) - } + for jobRunID, jobList := range runJobs { + run, err := actions_model.GetRunByRepoAndID(ctx, jobList[0].RepoID, jobRunID) if err != nil { - return nil, fmt.Errorf("get run by job %d: %w", cJobs[0].ID, err) + return fmt.Errorf("get action run %d: %w", jobRunID, err) } - concurrentRun = jobRun + CreateCommitStatusForRunJobs(ctx, run, jobList...) } - - return concurrentRun, nil + return nil } -func checkRunConcurrency(ctx context.Context, run *actions_model.ActionRun) (jobs, updatedJobs []*actions_model.ActionRunJob, err error) { +// findBlockedRunIDByConcurrency finds a blocked concurrent run in a repo and returns 0 when there is no blocked run. +func findBlockedRunIDByConcurrency(ctx context.Context, repoID int64, concurrencyGroup string) (int64, error) { + if concurrencyGroup == "" { + return 0, nil + } + cAttempts, cJobs, err := actions_model.GetConcurrentRunAttemptsAndJobs(ctx, repoID, concurrencyGroup, []actions_model.Status{actions_model.StatusBlocked}) + if err != nil { + return 0, fmt.Errorf("find concurrent runs and jobs: %w", err) + } + + if len(cAttempts) > 0 { + return cAttempts[0].RunID, nil + } + if len(cJobs) > 0 { + return cJobs[0].RunID, nil + } + + return 0, nil +} + +func checkBlockedConcurrentRun(ctx context.Context, repoID, runID int64) (jobs, updatedJobs, cancelledJobs []*actions_model.ActionRunJob, err error) { + concurrentRun, err := actions_model.GetRunByRepoAndID(ctx, repoID, runID) + if err != nil { + return nil, nil, nil, fmt.Errorf("get run %d: %w", runID, err) + } + if concurrentRun.NeedApproval { + return nil, nil, nil, nil + } + + return checkJobsOfCurrentRunAttempt(ctx, concurrentRun) +} + +// checkRunConcurrency rechecks runs blocked by concurrency that may become unblocked after the current run releases a workflow-level or job-level concurrency group. +func checkRunConcurrency(ctx context.Context, run *actions_model.ActionRun) (jobs, updatedJobs, cancelledJobs []*actions_model.ActionRunJob, err error) { checkedConcurrencyGroup := make(container.Set[string]) collect := func(concurrencyGroup string) error { - concurrentRun, err := findBlockedRunByConcurrency(ctx, run.RepoID, concurrencyGroup) + concurrentRunID, err := findBlockedRunIDByConcurrency(ctx, run.RepoID, concurrencyGroup) if err != nil { return fmt.Errorf("find blocked run by concurrency: %w", err) } - if concurrentRun != nil && !concurrentRun.NeedApproval { - js, ujs, err := checkJobsOfRun(ctx, concurrentRun) + if concurrentRunID > 0 { + js, ujs, cjs, err := checkBlockedConcurrentRun(ctx, run.RepoID, concurrentRunID) if err != nil { return err } jobs = append(jobs, js...) updatedJobs = append(updatedJobs, ujs...) + cancelledJobs = append(cancelledJobs, cjs...) } checkedConcurrencyGroup.Add(concurrencyGroup) return nil } // check run (workflow-level) concurrency - if run.ConcurrencyGroup != "" { - if err := collect(run.ConcurrencyGroup); err != nil { - return nil, nil, err + runConcurrencyGroup, _, err := run.GetEffectiveConcurrency(ctx) + if err != nil { + return nil, nil, nil, fmt.Errorf("GetEffectiveConcurrency: %w", err) + } + if runConcurrencyGroup != "" { + if err := collect(runConcurrencyGroup); err != nil { + return nil, nil, nil, err } } // check job concurrency - runJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) + runJobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID) if err != nil { - return nil, nil, fmt.Errorf("find run %d jobs: %w", run.ID, err) + return nil, nil, nil, fmt.Errorf("find run %d jobs: %w", run.ID, err) } for _, job := range runJobs { if !job.Status.IsDone() { @@ -188,28 +216,30 @@ func checkRunConcurrency(ctx context.Context, run *actions_model.ActionRun) (job continue } if err := collect(job.ConcurrencyGroup); err != nil { - return nil, nil, err + return nil, nil, nil, err } } - return jobs, updatedJobs, nil + return jobs, updatedJobs, cancelledJobs, nil } -func checkJobsOfRun(ctx context.Context, run *actions_model.ActionRun) (jobs, updatedJobs []*actions_model.ActionRunJob, err error) { - jobs, err = db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) +// checkJobsOfCurrentRunAttempt resolves blocked jobs of the run's latest attempt. +func checkJobsOfCurrentRunAttempt(ctx context.Context, run *actions_model.ActionRun) (jobs, updatedJobs, cancelledJobs []*actions_model.ActionRunJob, err error) { + jobs, err = actions_model.GetRunJobsByRunAndAttemptID(ctx, run.ID, run.LatestAttemptID) if err != nil { - return nil, nil, err + return nil, nil, nil, err } vars, err := actions_model.GetVariablesOfRun(ctx, run) if err != nil { - return nil, nil, err + return nil, nil, nil, err } + resolver := newJobStatusResolver(jobs, vars) if err = db.WithTx(ctx, func(ctx context.Context) error { for _, job := range jobs { job.Run = run } - updates := newJobStatusResolver(jobs, vars).Resolve(ctx) + updates := resolver.Resolve(ctx) for _, job := range jobs { if status, ok := updates[job.ID]; ok { job.Status = status @@ -223,26 +253,18 @@ func checkJobsOfRun(ctx context.Context, run *actions_model.ActionRun) (jobs, up } return nil }); err != nil { - return nil, nil, err + return nil, nil, nil, err } - return jobs, updatedJobs, nil -} - -func NotifyWorkflowRunStatusUpdateWithReload(ctx context.Context, job *actions_model.ActionRunJob) { - job.Run = nil - if err := job.LoadAttributes(ctx); err != nil { - log.Error("LoadAttributes: %v", err) - return - } - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + return jobs, updatedJobs, resolver.cancelledJobs, nil } type jobStatusResolver struct { - statuses map[int64]actions_model.Status - needs map[int64][]int64 - jobMap map[int64]*actions_model.ActionRunJob - vars map[string]string + statuses map[int64]actions_model.Status + needs map[int64][]int64 + jobMap map[int64]*actions_model.ActionRunJob + vars map[string]string + cancelledJobs []*actions_model.ActionRunJob } func newJobStatusResolver(jobs actions_model.ActionJobList, vars map[string]string) *jobStatusResolver { @@ -341,9 +363,12 @@ func (r *jobStatusResolver) resolve(ctx context.Context) map[int64]actions_model newStatus := util.Iif(shouldStartJob, actions_model.StatusWaiting, actions_model.StatusSkipped) if newStatus == actions_model.StatusWaiting { - newStatus, err = PrepareToStartJobWithConcurrency(ctx, actionRunJob) + var cancelledJobs []*actions_model.ActionRunJob + newStatus, cancelledJobs, err = PrepareToStartJobWithConcurrency(ctx, actionRunJob) if err != nil { log.Error("ShouldBlockJobByConcurrency failed, this job will stay blocked: job: %d, err: %v", id, err) + } else { + r.cancelledJobs = append(r.cancelledJobs, cancelledJobs...) } } @@ -359,8 +384,16 @@ func updateConcurrencyEvaluationForJobWithNeeds(ctx context.Context, actionRunJo return nil // for testing purpose only, no repo, no evaluation } - err := EvaluateJobConcurrencyFillModel(ctx, actionRunJob.Run, actionRunJob, vars, nil) - if err != nil { + // Legacy jobs (created before migration v331) have RunAttemptID=0 and no attempt record. + var attempt *actions_model.ActionRunAttempt + if actionRunJob.RunAttemptID > 0 { + var err error + attempt, err = actions_model.GetRunAttemptByRepoAndID(ctx, actionRunJob.RepoID, actionRunJob.RunAttemptID) + if err != nil { + return fmt.Errorf("GetRunAttemptByRepoAndID: %w", err) + } + } + if err := EvaluateJobConcurrencyFillModel(ctx, actionRunJob.Run, attempt, actionRunJob, vars, nil); err != nil { return fmt.Errorf("evaluate job concurrency: %w", err) } diff --git a/services/actions/job_emitter_test.go b/services/actions/job_emitter_test.go index 5ab1c0846d7..11998e01b21 100644 --- a/services/actions/job_emitter_test.go +++ b/services/actions/job_emitter_test.go @@ -144,23 +144,36 @@ func Test_checkRunConcurrency_NoDuplicateConcurrencyGroupCheck(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) ctx := t.Context() - // Run A: the triggering run with a concurrency group. + // Run A: the triggering run of attempt A runA := &actions_model.ActionRun{ + RepoID: 4, + OwnerID: 1, + TriggerUserID: 1, + WorkflowID: "test.yml", + Index: 9901, + Ref: "refs/heads/main", + Status: actions_model.StatusRunning, + } + assert.NoError(t, db.Insert(ctx, runA)) + + // Attempt A: an attempt of run A with concurrency group "test-cg" + runAAttempt := &actions_model.ActionRunAttempt{ RepoID: 4, - OwnerID: 1, - TriggerUserID: 1, - WorkflowID: "test.yml", - Index: 9901, - Ref: "refs/heads/main", + RunID: runA.ID, + Attempt: 1, Status: actions_model.StatusRunning, ConcurrencyGroup: "test-cg", } - assert.NoError(t, db.Insert(ctx, runA)) + assert.NoError(t, db.Insert(ctx, runAAttempt)) + _, err := db.Exec(t.Context(), "UPDATE `action_run` SET latest_attempt_id = ? WHERE id = ?", runAAttempt.ID, runA.ID) + assert.NoError(t, err) // A done job for run A with the same ConcurrencyGroup. // This triggers the job-level concurrency check in checkRunConcurrency. jobADone := &actions_model.ActionRunJob{ RunID: runA.ID, + RunAttemptID: runAAttempt.ID, + AttemptJobID: 1, RepoID: 4, OwnerID: 1, JobID: "job1", @@ -170,31 +183,45 @@ func Test_checkRunConcurrency_NoDuplicateConcurrencyGroupCheck(t *testing.T) { } assert.NoError(t, db.Insert(ctx, jobADone)) - // Blocked run B competing for the same concurrency group. + // Run B: a run blocked by concurrency runB := &actions_model.ActionRun{ - RepoID: 4, - OwnerID: 1, - TriggerUserID: 1, - WorkflowID: "test.yml", - Index: 9902, - Ref: "refs/heads/main", - Status: actions_model.StatusBlocked, - ConcurrencyGroup: "test-cg", + RepoID: 4, + OwnerID: 1, + TriggerUserID: 1, + WorkflowID: "test.yml", + Index: 9902, + Ref: "refs/heads/main", + Status: actions_model.StatusBlocked, } assert.NoError(t, db.Insert(ctx, runB)) + // Attempt B: an blocked attempt of run B + runBAttempt := &actions_model.ActionRunAttempt{ + RepoID: 4, + RunID: runB.ID, + Attempt: 1, + Status: actions_model.StatusBlocked, + ConcurrencyGroup: "test-cg", + } + assert.NoError(t, db.Insert(ctx, runBAttempt)) + _, err = db.Exec(t.Context(), "UPDATE `action_run` SET latest_attempt_id = ? WHERE id = ?", runBAttempt.ID, runB.ID) + assert.NoError(t, err) + // A blocked job belonging to run B (no job-level concurrency group). jobBBlocked := &actions_model.ActionRunJob{ - RunID: runB.ID, - RepoID: 4, - OwnerID: 1, - JobID: "job1", - Name: "job1", - Status: actions_model.StatusBlocked, + RunID: runB.ID, + RunAttemptID: runBAttempt.ID, + AttemptJobID: 1, + RepoID: 4, + OwnerID: 1, + JobID: "job1", + Name: "job1", + Status: actions_model.StatusBlocked, } assert.NoError(t, db.Insert(ctx, jobBBlocked)) - jobs, _, err := checkRunConcurrency(ctx, runA) + runA, _, _ = db.GetByID[actions_model.ActionRun](t.Context(), runA.ID) + jobs, _, _, err := checkRunConcurrency(ctx, runA) assert.NoError(t, err) if assert.Len(t, jobs, 1) { diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 5f7ee6fcea0..c3b2003b3cd 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -815,7 +815,7 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep log.Error("GetActionWorkflow: %v", err) return } - convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run) + convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run, nil) if err != nil { log.Error("ToActionWorkflowRun: %v", err) return diff --git a/services/actions/notify.go b/services/actions/notify.go new file mode 100644 index 00000000000..e8b05c9fecd --- /dev/null +++ b/services/actions/notify.go @@ -0,0 +1,144 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/log" + notify_service "code.gitea.io/gitea/services/notify" +) + +// NotifyWorkflowJobsAndRunsStatusUpdate notifies status changes for a batch of jobs and the runs they affect. +// Use it when a workflow operation updates multiple jobs and runs. +func NotifyWorkflowJobsAndRunsStatusUpdate(ctx context.Context, jobs []*actions_model.ActionRunJob) { + if len(jobs) == 0 { + return + } + + // The input jobs may belong to different runs, so track each affected run. + runs := make(map[int64]*actions_model.ActionRun, len(jobs)) + jobsByRunID := make(map[int64][]*actions_model.ActionRunJob) + + for _, job := range jobs { + if err := job.LoadAttributes(ctx); err != nil { + log.Error("Failed to load job attributes: %v", err) + continue + } + CreateCommitStatusForRunJobs(ctx, job.Run, job) + + if _, ok := runs[job.RunID]; !ok { + runs[job.RunID] = job.Run + } + if _, ok := jobsByRunID[job.RunID]; !ok { + jobsByRunID[job.RunID] = make([]*actions_model.ActionRunJob, 0) + } + jobsByRunID[job.RunID] = append(jobsByRunID[job.RunID], job) + } + + for _, run := range runs { + NotifyWorkflowRunStatusUpdate(ctx, run) + } + + for _, jobs := range jobsByRunID { + NotifyWorkflowJobsStatusUpdate(ctx, jobs...) + } +} + +// NotifyWorkflowRunStatusUpdateWithReload reloads the run before notifying its status update. +// Use it when only repo/run IDs are available or when the in-memory run may be stale after job updates. +func NotifyWorkflowRunStatusUpdateWithReload(ctx context.Context, repoID, runID int64) { + run, err := actions_model.GetRunByRepoAndID(ctx, repoID, runID) + if err != nil { + log.Error("GetRunByRepoAndID: %v", err) + return + } + NotifyWorkflowRunStatusUpdate(ctx, run) +} + +// NotifyWorkflowRunStatusUpdate notifies a run status update using the latest attempt trigger user when available. +// Use it for run-level notifications when the caller already has the run model loaded. +func NotifyWorkflowRunStatusUpdate(ctx context.Context, run *actions_model.ActionRun) { + if err := run.LoadAttributes(ctx); err != nil { + log.Error("run.LoadAttributes: %v", err) + return + } + triggerUser := run.TriggerUser + if run.LatestAttemptID > 0 { + attempt, err := actions_model.GetRunAttemptByRepoAndID(ctx, run.RepoID, run.LatestAttemptID) + if err != nil { + log.Error("GetRunAttemptByRepoAndID: %v", err) + return + } + if err := attempt.LoadAttributes(ctx); err != nil { + log.Error("attempt.LoadAttributes: %v", err) + return + } + triggerUser = attempt.TriggerUser + } + notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, triggerUser, run) +} + +// NotifyWorkflowJobsStatusUpdate notifies status updates for jobs without task. +// Use it for batch or single-job notifications after state changes. +func NotifyWorkflowJobsStatusUpdate(ctx context.Context, jobs ...*actions_model.ActionRunJob) { + jobsByAttempt := make(map[int64][]*actions_model.ActionRunJob) + for _, job := range jobs { + if _, ok := jobsByAttempt[job.RunAttemptID]; !ok { + jobsByAttempt[job.RunAttemptID] = make([]*actions_model.ActionRunJob, 0) + } + jobsByAttempt[job.RunAttemptID] = append(jobsByAttempt[job.RunAttemptID], job) + } + + for attemptID, js := range jobsByAttempt { + if attemptID == 0 { + for _, job := range js { + if err := job.LoadAttributes(ctx); err != nil { + log.Error("job.LoadAttributes: %v", err) + continue + } + notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + } + continue + } + + attempt, err := actions_model.GetRunAttemptByRepoAndID(ctx, js[0].RepoID, attemptID) + if err != nil { + log.Error("GetRunAttemptByRepoAndID: %v", err) + continue + } + if err := attempt.LoadAttributes(ctx); err != nil { + log.Error("attempt.LoadAttributes: %v", err) + continue + } + for _, job := range js { + notify_service.WorkflowJobStatusUpdate(ctx, attempt.Run.Repo, attempt.TriggerUser, job, nil) + } + } +} + +// NotifyWorkflowJobStatusUpdateWithTask notifies a single job status update when a concrete task is available. +// Use it for runner/task lifecycle callbacks so the notification includes the originating task context. +func NotifyWorkflowJobStatusUpdateWithTask(ctx context.Context, job *actions_model.ActionRunJob, task *actions_model.ActionTask) { + if job.RunAttemptID == 0 { + if err := job.LoadAttributes(ctx); err != nil { + log.Error("job.LoadAttributes: %v", err) + return + } + notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, task) + return + } + + attempt, err := actions_model.GetRunAttemptByRepoAndID(ctx, job.RepoID, job.RunAttemptID) + if err != nil { + log.Error("GetRunAttemptByRepoAndID: %v", err) + return + } + if err := attempt.LoadAttributes(ctx); err != nil { + log.Error("attempt.LoadAttributes: %v", err) + return + } + notify_service.WorkflowJobStatusUpdate(ctx, attempt.Run.Repo, attempt.TriggerUser, job, task) +} diff --git a/services/actions/rerun.go b/services/actions/rerun.go index 1596d9bfc5a..f253181a8dd 100644 --- a/services/actions/rerun.go +++ b/services/actions/rerun.go @@ -6,57 +6,312 @@ package actions import ( "context" "fmt" + "slices" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" - notify_service "code.gitea.io/gitea/services/notify" "github.com/nektos/act/pkg/model" "go.yaml.in/yaml/v4" - "xorm.io/builder" ) -// GetFailedRerunJobs returns all failed jobs and their downstream dependent jobs that need to be rerun -func GetFailedRerunJobs(allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob { - rerunJobIDSet := make(container.Set[int64]) +// GetFailedJobsForRerun returns the failed or cancelled jobs in a run. +func GetFailedJobsForRerun(allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob { var jobsToRerun []*actions_model.ActionRunJob for _, job := range allJobs { if job.Status == actions_model.StatusFailure || job.Status == actions_model.StatusCancelled { - for _, j := range GetAllRerunJobs(job, allJobs) { - if !rerunJobIDSet.Contains(j.ID) { - rerunJobIDSet.Add(j.ID) - jobsToRerun = append(jobsToRerun, j) - } - } + jobsToRerun = append(jobsToRerun, job) } } return jobsToRerun } -// GetAllRerunJobs returns the target job and all jobs that transitively depend on it. -// Downstream jobs are included regardless of their current status. -func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob { - rerunJobs := []*actions_model.ActionRunJob{job} - rerunJobsIDSet := make(container.Set[string]) - rerunJobsIDSet.Add(job.JobID) +// RerunWorkflowRunJobs reruns the given jobs of a workflow run. +// An empty jobsToRerun means rerunning the whole run. Otherwise jobsToRerun contains only the user-requested target jobs; +// downstream dependent jobs are expanded internally while building the rerun plan. +// +// The three stages below (legacy backfill, plan build, plan exec) deliberately run in separate DB transactions +// rather than one big outer transaction: +// - execRerunPlan performs slow work (loading variables, YAML unmarshal, concurrency expression evaluation) +// before opening its own transaction, so the tx stays focused on inserts/updates. +// - The legacy backfill is idempotent-friendly: if it succeeds but a later stage fails, a subsequent rerun +// will observe run.LatestAttemptID != 0 and skip the backfill, continuing naturally. No data corruption +// or stuck state results from partial progress. +// +// Fast validations that can catch failures early (workflow disabled, run not done, etc.) are therefore +// pushed into validateRerun so we rarely enter createOriginalAttemptForLegacyRun only to fail afterwards. +func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, triggerUser *user_model.User, jobsToRerun []*actions_model.ActionRunJob) (*actions_model.ActionRunAttempt, error) { + if err := validateRerun(ctx, run, repo, triggerUser, jobsToRerun); err != nil { + return nil, err + } + + if run.LatestAttemptID == 0 { + if err := createOriginalAttemptForLegacyRun(ctx, run); err != nil { + return nil, fmt.Errorf("create attempt for legacy run: %w", err) + } + } + + plan, err := buildRerunPlan(ctx, run, triggerUser, jobsToRerun) + if err != nil { + return nil, err + } + return execRerunPlan(ctx, plan) +} + +func validateRerun(ctx context.Context, run *actions_model.ActionRun, repo *repo_model.Repository, triggerUser *user_model.User, jobsToRerun []*actions_model.ActionRunJob) error { + if !run.Status.IsDone() { + return util.NewInvalidArgumentErrorf("this workflow run is not done") + } + if repo == nil { + return util.NewInvalidArgumentErrorf("repo is required") + } + if run.RepoID != repo.ID { + return util.NewInvalidArgumentErrorf("run %d does not belong to repo %d", run.ID, repo.ID) + } + for _, job := range jobsToRerun { + if job.RunID != run.ID { + return util.NewInvalidArgumentErrorf("job %d does not belong to workflow run %d", job.ID, run.ID) + } + } + if triggerUser == nil { + return util.NewInvalidArgumentErrorf("trigger user is required") + } + cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions) + cfg := cfgUnit.ActionsConfig() + if cfg.IsWorkflowDisabled(run.WorkflowID) { + return util.NewInvalidArgumentErrorf("workflow %s is disabled", run.WorkflowID) + } + + // Legacy runs (LatestAttemptID == 0) conceptually have only attempt 1, so they can never be at the cap. + // For non-legacy runs, look up the latest attempt and reject when its number is already at the configured cap. + if run.LatestAttemptID > 0 { + latestAttempt, has, err := run.GetLatestAttempt(ctx) + if err != nil { + return fmt.Errorf("GetLatestAttempt: %w", err) + } + if has && latestAttempt.Attempt >= setting.Actions.MaxRerunAttempts { + return util.NewInvalidArgumentErrorf("workflow run has reached the maximum of %d attempts", setting.Actions.MaxRerunAttempts) + } + } + + return nil +} + +// rerunPlan is a read-only snapshot of the inputs needed to execute a rerun. +// It holds no to-be-persisted entities and no intermediate evaluation results; +// execRerunPlan constructs and evaluates the new ActionRunAttempt itself. +type rerunPlan struct { + run *actions_model.ActionRun + templateAttempt *actions_model.ActionRunAttempt + templateJobs actions_model.ActionJobList + rerunJobIDs container.Set[string] + triggerUser *user_model.User +} + +// buildRerunPlan constructs a rerunPlan for the given workflow run without writing to the database. +// jobsToRerun contains only the user-requested target jobs. An empty jobsToRerun means the entire run should be rerun. +// It loads the latest attempt as a template and expands jobsToRerun to include all transitive downstream dependents. +// The construction of new-attempt and concurrency evaluation are deferred to execRerunPlan so that the plan remains a pure input snapshot. +func buildRerunPlan(ctx context.Context, run *actions_model.ActionRun, triggerUser *user_model.User, jobsToRerun []*actions_model.ActionRunJob) (*rerunPlan, error) { + if err := run.LoadAttributes(ctx); err != nil { + return nil, err + } + + templateAttempt, hasTemplateAttempt, err := run.GetLatestAttempt(ctx) + if err != nil { + return nil, err + } + if !hasTemplateAttempt { + return nil, util.NewNotExistErrorf("latest attempt not found") + } + + templateJobs, err := actions_model.GetRunJobsByRunAndAttemptID(ctx, run.ID, templateAttempt.ID) + if err != nil { + return nil, fmt.Errorf("load template jobs: %w", err) + } + if len(templateJobs) == 0 { + return nil, util.NewNotExistErrorf("no template jobs") + } + + plan := &rerunPlan{ + run: run, + templateAttempt: templateAttempt, + templateJobs: templateJobs, + triggerUser: triggerUser, + } + + if err := plan.expandRerunJobIDs(jobsToRerun); err != nil { + return nil, err + } + + return plan, nil +} + +// execRerunPlan executes the rerun plan built by buildRerunPlan. +// It loads run variables, constructs the new ActionRunAttempt and evaluates run-level concurrency (all outside the transaction to keep the tx short). +// Inside a single database transaction it then inserts the new attempt, clones all template jobs, evaluates job-level concurrency for rerun jobs, +// and updates the run's latest_attempt_id. +// Jobs not in the rerun set are cloned as pass-through: their status is preserved and SourceTaskID points to the original task so the UI can still display their results. +// The attempt's final status is derived only from the rerun jobs, not the pass-through jobs. +// Notifications and commit statuses are sent after the transaction commits. +func execRerunPlan(ctx context.Context, plan *rerunPlan) (*actions_model.ActionRunAttempt, error) { + vars, err := actions_model.GetVariablesOfRun(ctx, plan.run) + if err != nil { + return nil, fmt.Errorf("get run %d variables: %w", plan.run.ID, err) + } + + newAttempt := &actions_model.ActionRunAttempt{ + RepoID: plan.run.RepoID, + RunID: plan.run.ID, + Attempt: plan.templateAttempt.Attempt + 1, + TriggerUserID: plan.triggerUser.ID, + Status: actions_model.StatusWaiting, + } + + if plan.run.RawConcurrency != "" { + var rawConcurrency model.RawConcurrency + if err := yaml.Unmarshal([]byte(plan.run.RawConcurrency), &rawConcurrency); err != nil { + return nil, fmt.Errorf("unmarshal raw concurrency: %w", err) + } + if err := EvaluateRunConcurrencyFillModel(ctx, plan.run, newAttempt, &rawConcurrency, vars, nil); err != nil { + return nil, err + } + } + + var newJobs, newJobsToRerun actions_model.ActionJobList + var cancelledConcurrencyJobs []*actions_model.ActionRunJob + + err = db.WithTx(ctx, func(ctx context.Context) error { + newAttemptStatus, jobsToCancel, err := PrepareToStartRunWithConcurrency(ctx, newAttempt) + if err != nil { + return err + } + cancelledConcurrencyJobs = append(cancelledConcurrencyJobs, jobsToCancel...) + newAttempt.Status = newAttemptStatus + shouldBlock := newAttemptStatus == actions_model.StatusBlocked + + if err := db.Insert(ctx, newAttempt); err != nil { + if _, getErr := actions_model.GetRunAttemptByRunIDAndAttemptNum(ctx, plan.run.ID, newAttempt.Attempt); getErr == nil { + return util.NewAlreadyExistErrorf("workflow run attempt %d for run %d already exists", newAttempt.Attempt, plan.run.ID) + } + return err + } + + plan.run.LatestAttemptID = newAttempt.ID + if err := actions_model.UpdateRun(ctx, plan.run, "latest_attempt_id"); err != nil { + return err + } + + hasWaitingJobs := false + newJobs = make(actions_model.ActionJobList, 0, len(plan.templateJobs)) + newJobsToRerun = make(actions_model.ActionJobList, 0, len(plan.rerunJobIDs)) + for _, templateJob := range plan.templateJobs { + newJob := cloneRunJobForAttempt(templateJob, newAttempt) + if plan.rerunJobIDs.Contains(templateJob.JobID) { + shouldBlockJob := shouldBlock || plan.hasRerunDependency(templateJob) + + newJob.Status = util.Iif(shouldBlockJob, actions_model.StatusBlocked, actions_model.StatusWaiting) + newJob.TaskID = 0 + newJob.SourceTaskID = 0 + newJob.Started = 0 + newJob.Stopped = 0 + newJob.ConcurrencyGroup = "" + newJob.ConcurrencyCancel = false + newJob.IsConcurrencyEvaluated = false + + if newJob.RawConcurrency != "" && !shouldBlockJob { + if err := EvaluateJobConcurrencyFillModel(ctx, plan.run, newAttempt, newJob, vars, nil); err != nil { + return fmt.Errorf("evaluate job concurrency: %w", err) + } + newJob.Status, jobsToCancel, err = PrepareToStartJobWithConcurrency(ctx, newJob) + if err != nil { + return fmt.Errorf("prepare to start job with concurrency: %w", err) + } + cancelledConcurrencyJobs = append(cancelledConcurrencyJobs, jobsToCancel...) + } + + newJobsToRerun = append(newJobsToRerun, newJob) + } else { + newJob.TaskID = 0 + newJob.SourceTaskID = templateJob.EffectiveTaskID() + newJob.Started = templateJob.Started + newJob.Stopped = templateJob.Stopped + } + + if err := db.Insert(ctx, newJob); err != nil { + return err + } + hasWaitingJobs = hasWaitingJobs || newJob.Status == actions_model.StatusWaiting + newJobs = append(newJobs, newJob) + } + + newAttempt.Status = actions_model.AggregateJobStatus(newJobsToRerun) + if err := actions_model.UpdateRunAttempt(ctx, newAttempt, "status"); err != nil { + return err + } + + if hasWaitingJobs { + if err := actions_model.IncreaseTaskVersion(ctx, plan.run.OwnerID, plan.run.RepoID); err != nil { + return err + } + } + + return nil + }) + if err != nil { + return nil, err + } + + if err := plan.run.LoadAttributes(ctx); err != nil { + return nil, err + } + + NotifyWorkflowJobsAndRunsStatusUpdate(ctx, cancelledConcurrencyJobs) + EmitJobsIfReadyByJobs(cancelledConcurrencyJobs) + + CreateCommitStatusForRunJobs(ctx, plan.run, newJobs...) + NotifyWorkflowJobsAndRunsStatusUpdate(ctx, newJobsToRerun) + + return newAttempt, nil +} + +func (p *rerunPlan) expandRerunJobIDs(jobsToRerun []*actions_model.ActionRunJob) error { + templateJobIDs := make(container.Set[string]) + for _, job := range p.templateJobs { + templateJobIDs.Add(job.JobID) + } + + if len(jobsToRerun) == 0 { + p.rerunJobIDs = templateJobIDs + return nil + } + + rerunJobIDs := make(container.Set[string]) + for _, job := range jobsToRerun { + if !templateJobIDs.Contains(job.JobID) { + return util.NewInvalidArgumentErrorf("job %q does not exist in the latest attempt", job.JobID) + } + rerunJobIDs.Add(job.JobID) + } for { found := false - for _, j := range allJobs { - if rerunJobsIDSet.Contains(j.JobID) { + for _, job := range p.templateJobs { + if rerunJobIDs.Contains(job.JobID) { continue } - for _, need := range j.Needs { - if rerunJobsIDSet.Contains(need) { + for _, need := range job.Needs { + if rerunJobIDs.Contains(need) { found = true - rerunJobs = append(rerunJobs, j) - rerunJobsIDSet.Add(j.JobID) + rerunJobIDs.Add(job.JobID) break } } @@ -66,152 +321,100 @@ func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.A } } - return rerunJobs + p.rerunJobIDs = rerunJobIDs + return nil } -// prepareRunRerun validates the run, resets its state, handles concurrency, persists the -// updated run, and fires a status-update notification. -// It returns isRunBlocked (true when the run itself is held by a concurrency group). -func prepareRunRerun(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) (isRunBlocked bool, err error) { - if !run.Status.IsDone() { - return false, util.NewInvalidArgumentErrorf("this workflow run is not done") - } - - cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions) - - // Rerun is not allowed when workflow is disabled. - cfg := cfgUnit.ActionsConfig() - if cfg.IsWorkflowDisabled(run.WorkflowID) { - return false, util.NewInvalidArgumentErrorf("workflow %s is disabled", run.WorkflowID) - } - - // Reset run's timestamps and status. - run.PreviousDuration = run.Duration() - run.Started = 0 - run.Stopped = 0 - run.Status = actions_model.StatusWaiting - - vars, err := actions_model.GetVariablesOfRun(ctx, run) - if err != nil { - return false, fmt.Errorf("get run %d variables: %w", run.ID, err) - } - - if run.RawConcurrency != "" { - var rawConcurrency model.RawConcurrency - if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil { - return false, fmt.Errorf("unmarshal raw concurrency: %w", err) +func (p *rerunPlan) hasRerunDependency(job *actions_model.ActionRunJob) bool { + for _, need := range job.Needs { + if p.rerunJobIDs.Contains(need) { + return true } + } + return false +} - if err := EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars, nil); err != nil { - return false, err - } +func cloneRunJobForAttempt(templateJob *actions_model.ActionRunJob, attempt *actions_model.ActionRunAttempt) *actions_model.ActionRunJob { + return &actions_model.ActionRunJob{ + RunID: templateJob.RunID, + RunAttemptID: attempt.ID, + RepoID: templateJob.RepoID, + OwnerID: templateJob.OwnerID, + CommitSHA: templateJob.CommitSHA, + IsForkPullRequest: templateJob.IsForkPullRequest, + Name: templateJob.Name, + Attempt: attempt.Attempt, + WorkflowPayload: slices.Clone(templateJob.WorkflowPayload), + JobID: templateJob.JobID, + AttemptJobID: templateJob.AttemptJobID, + Needs: slices.Clone(templateJob.Needs), + RunsOn: slices.Clone(templateJob.RunsOn), + Status: templateJob.Status, + RawConcurrency: templateJob.RawConcurrency, + IsConcurrencyEvaluated: templateJob.IsConcurrencyEvaluated, + ConcurrencyGroup: templateJob.ConcurrencyGroup, + ConcurrencyCancel: templateJob.ConcurrencyCancel, + TokenPermissions: templateJob.TokenPermissions, + } +} - run.Status, err = PrepareToStartRunWithConcurrency(ctx, run) +// createOriginalAttemptForLegacyRun creates a real attempt=1 for a legacy run and updates the existing legacy jobs and artifacts in place +// so the original execution becomes attempt-aware before the rerun plan is built and all subsequent logic can use real attempts. +// Tasks are not modified: they reference jobs by JobID, so updating jobs implicitly carries the new attempt linkage. +func createOriginalAttemptForLegacyRun(ctx context.Context, run *actions_model.ActionRun) error { + return db.WithTx(ctx, func(ctx context.Context) error { + jobs, err := actions_model.GetRunJobsByRunAndAttemptID(ctx, run.ID, 0) if err != nil { - return false, err + return fmt.Errorf("load legacy run jobs: %w", err) + } + if len(jobs) == 0 { + return fmt.Errorf("run %d has no jobs", run.ID) } - } - if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil { - return false, err - } + originalAttempt := &actions_model.ActionRunAttempt{ + RepoID: run.RepoID, + RunID: run.ID, + Attempt: 1, + TriggerUserID: run.TriggerUserID, - if err := run.LoadAttributes(ctx); err != nil { - return false, err - } + // Legacy concurrency fields on ActionRun are intentionally NOT backfilled onto this original attempt. + // They only matter while a run is actively being scheduled, and backfilling them for completed legacy runs + // would add migration/runtime cost without changing any future concurrency behavior. - for _, job := range jobs { - job.Run = run - } + Status: run.Status, + Created: run.Created, + Started: run.Started, + Stopped: run.Stopped, + } - notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) + // Use NoAutoTime so xorm does not overwrite Created with the current time on insert. + if _, err := db.GetEngine(ctx).NoAutoTime().Insert(originalAttempt); err != nil { + if _, getErr := actions_model.GetRunAttemptByRunIDAndAttemptNum(ctx, run.ID, originalAttempt.Attempt); getErr == nil { + return util.NewAlreadyExistErrorf("workflow run attempt %d for run %d already exists", originalAttempt.Attempt, run.ID) + } + return err + } - return run.Status == actions_model.StatusBlocked, nil -} - -// RerunWorkflowRunJobs reruns the given jobs of a workflow run. -// jobsToRerun must include all jobs to be rerun (the target job and its transitively dependent jobs). -// A job is blocked (waiting for dependencies) if the run itself is blocked or if any of its -// needs are also being rerun. -func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, jobsToRerun []*actions_model.ActionRunJob) error { - if len(jobsToRerun) == 0 { - return nil - } - - isRunBlocked, err := prepareRunRerun(ctx, repo, run, jobsToRerun) - if err != nil { - return err - } - - rerunJobIDs := make(container.Set[string]) - for _, j := range jobsToRerun { - rerunJobIDs.Add(j.JobID) - } - - for _, job := range jobsToRerun { - shouldBlockJob := isRunBlocked - if !shouldBlockJob { - for _, need := range job.Needs { - if rerunJobIDs.Contains(need) { - shouldBlockJob = true - break - } + // backfill attempt related fields for jobs + for i, job := range jobs { + job.RunAttemptID = originalAttempt.ID + job.Attempt = originalAttempt.Attempt + job.AttemptJobID = int64(i + 1) + if _, err := db.GetEngine(ctx).ID(job.ID).Cols("run_attempt_id", "attempt", "attempt_job_id").Update(job); err != nil { + return fmt.Errorf("backfill legacy run jobs: %w", err) } } - if err := rerunWorkflowJob(ctx, job, shouldBlockJob); err != nil { - return err - } - } - return nil -} - -func rerunWorkflowJob(ctx context.Context, job *actions_model.ActionRunJob, shouldBlock bool) error { - status := job.Status - if !status.IsDone() { - return nil - } - - job.TaskID = 0 - job.Status = util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting) - job.Started = 0 - job.Stopped = 0 - job.ConcurrencyGroup = "" - job.ConcurrencyCancel = false - job.IsConcurrencyEvaluated = false - - if err := job.LoadRun(ctx); err != nil { - return err - } - if err := job.Run.LoadAttributes(ctx); err != nil { - return err - } - - vars, err := actions_model.GetVariablesOfRun(ctx, job.Run) - if err != nil { - return fmt.Errorf("get run %d variables: %w", job.Run.ID, err) - } - - if job.RawConcurrency != "" && !shouldBlock { - if err := EvaluateJobConcurrencyFillModel(ctx, job.Run, job, vars, nil); err != nil { - return fmt.Errorf("evaluate job concurrency: %w", err) - } - - job.Status, err = PrepareToStartJobWithConcurrency(ctx, job) - if err != nil { - return err - } - } - - if err := db.WithTx(ctx, func(ctx context.Context) error { - updateCols := []string{"task_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated"} - _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, updateCols...) - return err - }); err != nil { - return err - } - - CreateCommitStatusForRunJobs(ctx, job.Run, job) - notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) - return nil + // backfill "run_attempt_id" field for artifacts + if _, err := db.GetEngine(ctx). + Where("run_id=? AND run_attempt_id=0", run.ID). + Cols("run_attempt_id"). + Update(&actions_model.ActionArtifact{RunAttemptID: originalAttempt.ID}); err != nil { + return fmt.Errorf("backfill legacy artifacts: %w", err) + } + + // update "latest_attempt_id" for the run + run.LatestAttemptID = originalAttempt.ID + return actions_model.UpdateRun(ctx, run, "latest_attempt_id") + }) } diff --git a/services/actions/rerun_test.go b/services/actions/rerun_test.go index 3b4dc5483f4..30772980619 100644 --- a/services/actions/rerun_test.go +++ b/services/actions/rerun_test.go @@ -4,54 +4,17 @@ package actions import ( - "context" "testing" actions_model "code.gitea.io/gitea/models/actions" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestGetAllRerunJobs(t *testing.T) { - job1 := &actions_model.ActionRunJob{JobID: "job1"} - job2 := &actions_model.ActionRunJob{JobID: "job2", Needs: []string{"job1"}} - job3 := &actions_model.ActionRunJob{JobID: "job3", Needs: []string{"job2"}} - job4 := &actions_model.ActionRunJob{JobID: "job4", Needs: []string{"job2", "job3"}} - - jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4} - - testCases := []struct { - job *actions_model.ActionRunJob - rerunJobs []*actions_model.ActionRunJob - }{ - { - job1, - []*actions_model.ActionRunJob{job1, job2, job3, job4}, - }, - { - job2, - []*actions_model.ActionRunJob{job2, job3, job4}, - }, - { - job3, - []*actions_model.ActionRunJob{job3, job4}, - }, - { - job4, - []*actions_model.ActionRunJob{job4}, - }, - } - - for _, tc := range testCases { - rerunJobs := GetAllRerunJobs(tc.job, jobs) - assert.ElementsMatch(t, tc.rerunJobs, rerunJobs) - } -} - -func TestGetFailedRerunJobs(t *testing.T) { - // IDs must be non-zero to distinguish jobs in the dedup set. +func TestGetFailedJobsForRerun(t *testing.T) { makeJob := func(id int64, jobID string, status actions_model.Status, needs ...string) *actions_model.ActionRunJob { return &actions_model.ActionRunJob{ID: id, JobID: jobID, Status: status, Needs: needs} } @@ -61,7 +24,7 @@ func TestGetFailedRerunJobs(t *testing.T) { makeJob(1, "job1", actions_model.StatusSuccess), makeJob(2, "job2", actions_model.StatusSkipped, "job1"), } - assert.Empty(t, GetFailedRerunJobs(jobs)) + assert.Empty(t, GetFailedJobsForRerun(jobs)) }) t.Run("single failed job with no dependents", func(t *testing.T) { @@ -69,56 +32,50 @@ func TestGetFailedRerunJobs(t *testing.T) { job2 := makeJob(2, "job2", actions_model.StatusSuccess) jobs := []*actions_model.ActionRunJob{job1, job2} - result := GetFailedRerunJobs(jobs) + result := GetFailedJobsForRerun(jobs) assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1}, result) }) - t.Run("failed job pulls in downstream dependents", func(t *testing.T) { - // job1 failed; job2 depends on job1 (skipped); job3 depends on job2 (skipped) + t.Run("failed job does not pull in downstream dependents", func(t *testing.T) { job1 := makeJob(1, "job1", actions_model.StatusFailure) job2 := makeJob(2, "job2", actions_model.StatusSkipped, "job1") job3 := makeJob(3, "job3", actions_model.StatusSkipped, "job2") job4 := makeJob(4, "job4", actions_model.StatusSuccess) // unrelated, must not appear jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4} - result := GetFailedRerunJobs(jobs) - assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2, job3}, result) + result := GetFailedJobsForRerun(jobs) + assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1}, result) }) - t.Run("multiple independent failed jobs each pull in their own dependents", func(t *testing.T) { - // job1 failed -> job3 depends on job1 - // job2 failed -> job4 depends on job2 + t.Run("multiple failed jobs are returned directly", func(t *testing.T) { job1 := makeJob(1, "job1", actions_model.StatusFailure) job2 := makeJob(2, "job2", actions_model.StatusFailure) job3 := makeJob(3, "job3", actions_model.StatusSkipped, "job1") job4 := makeJob(4, "job4", actions_model.StatusSkipped, "job2") jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4} - result := GetFailedRerunJobs(jobs) - assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2, job3, job4}, result) + result := GetFailedJobsForRerun(jobs) + assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2}, result) }) - t.Run("shared downstream dependent is not duplicated", func(t *testing.T) { - // job1 and job2 both failed; job3 depends on both + t.Run("shared downstream dependent is not included", func(t *testing.T) { job1 := makeJob(1, "job1", actions_model.StatusFailure) job2 := makeJob(2, "job2", actions_model.StatusFailure) job3 := makeJob(3, "job3", actions_model.StatusSkipped, "job1", "job2") jobs := []*actions_model.ActionRunJob{job1, job2, job3} - result := GetFailedRerunJobs(jobs) - assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2, job3}, result) - assert.Len(t, result, 3) // job3 must appear exactly once + result := GetFailedJobsForRerun(jobs) + assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2}, result) + assert.Len(t, result, 2) }) - t.Run("successful downstream job of a failed job is still included", func(t *testing.T) { - // job1 failed; job2 succeeded but depends on job1 — downstream is always rerun - // regardless of its own status (GetAllRerunJobs includes all transitive dependents) + t.Run("successful downstream job of a failed job is not included", func(t *testing.T) { job1 := makeJob(1, "job1", actions_model.StatusFailure) job2 := makeJob(2, "job2", actions_model.StatusSuccess, "job1") jobs := []*actions_model.ActionRunJob{job1, job2} - result := GetFailedRerunJobs(jobs) - assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2}, result) + result := GetFailedJobsForRerun(jobs) + assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1}, result) }) } @@ -129,7 +86,7 @@ func TestRerunValidation(t *testing.T) { jobs := []*actions_model.ActionRunJob{ {ID: 1, JobID: "job1"}, } - err := RerunWorkflowRunJobs(context.Background(), nil, runningRun, jobs) + _, err := RerunWorkflowRunJobs(t.Context(), nil, runningRun, &user_model.User{ID: 1}, jobs) require.Error(t, err) assert.ErrorIs(t, err, util.ErrInvalidArgument) }) @@ -138,7 +95,7 @@ func TestRerunValidation(t *testing.T) { jobs := []*actions_model.ActionRunJob{ {ID: 1, JobID: "job1", Status: actions_model.StatusFailure}, } - err := RerunWorkflowRunJobs(context.Background(), nil, runningRun, GetFailedRerunJobs(jobs)) + _, err := RerunWorkflowRunJobs(t.Context(), nil, runningRun, &user_model.User{ID: 1}, GetFailedJobsForRerun(jobs)) require.Error(t, err) assert.ErrorIs(t, err, util.ErrInvalidArgument) }) diff --git a/services/actions/run.go b/services/actions/run.go index e9fcdcaf43d..162e3678aee 100644 --- a/services/actions/run.go +++ b/services/actions/run.go @@ -11,8 +11,8 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/actions/jobparser" "code.gitea.io/gitea/modules/util" - notify_service "code.gitea.io/gitea/services/notify" + act_model "github.com/nektos/act/pkg/model" "go.yaml.in/yaml/v4" ) @@ -34,25 +34,7 @@ func PrepareRunAndInsert(ctx context.Context, content []byte, run *actions_model return fmt.Errorf("ReadWorkflowRawConcurrency: %w", err) } - if wfRawConcurrency != nil { - err = EvaluateRunConcurrencyFillModel(ctx, run, wfRawConcurrency, vars, inputsWithDefaults) - if err != nil { - return fmt.Errorf("EvaluateRunConcurrencyFillModel: %w", err) - } - } - - giteaCtx := GenerateGiteaContext(run, nil) - - jobs, err := jobparser.Parse(content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext()), jobparser.WithInputs(inputsWithDefaults)) - if err != nil { - return fmt.Errorf("parse workflow: %w", err) - } - - if len(jobs) > 0 && jobs[0].RunName != "" { - run.Title = jobs[0].RunName - } - - if err = InsertRun(ctx, run, jobs, vars, inputsWithDefaults); err != nil { + if err = InsertRun(ctx, run, content, vars, inputsWithDefaults, wfRawConcurrency); err != nil { return fmt.Errorf("InsertRun: %w", err) } @@ -64,47 +46,89 @@ func PrepareRunAndInsert(ctx context.Context, content []byte, run *actions_model CreateCommitStatusForRunJobs(ctx, run, allJobs...) - notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) - for _, job := range allJobs { - notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil) - } + NotifyWorkflowJobsAndRunsStatusUpdate(ctx, allJobs) return nil } // InsertRun inserts a run // The title will be cut off at 255 characters if it's longer than 255 characters. -func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobparser.SingleWorkflow, vars map[string]string, inputs map[string]any) error { - return db.WithTx(ctx, func(ctx context.Context) error { +func InsertRun(ctx context.Context, run *actions_model.ActionRun, content []byte, vars map[string]string, inputs map[string]any, wfRawConcurrency *act_model.RawConcurrency) error { + var cancelledConcurrencyJobs []*actions_model.ActionRunJob + if err := db.WithTx(ctx, func(ctx context.Context) error { index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) if err != nil { return err } run.Index = index run.Title = util.EllipsisDisplayString(run.Title, 255) + run.Status = actions_model.StatusWaiting - // check run (workflow-level) concurrency - run.Status, err = PrepareToStartRunWithConcurrency(ctx, run) - if err != nil { - return err + if wfRawConcurrency != nil { + rawConcurrency, err := yaml.Marshal(wfRawConcurrency) + if err != nil { + return fmt.Errorf("marshal raw concurrency: %w", err) + } + run.RawConcurrency = string(rawConcurrency) } + // Insert before parsing jobs or evaluating workflow-level concurrency + // so that run.ID is populated. Expressions referencing github.run_id — + // in run-name, job names, runs-on, or a workflow-level concurrency + // group like `${{ github.head_ref || github.run_id }}` — would otherwise + // interpolate to an empty string. if err := db.Insert(ctx, run); err != nil { return err } - if err := run.LoadRepo(ctx); err != nil { - return err + runAttempt := &actions_model.ActionRunAttempt{ + RepoID: run.RepoID, + RunID: run.ID, + Attempt: 1, + TriggerUserID: run.TriggerUserID, + Status: actions_model.StatusWaiting, } - if err := actions_model.UpdateRepoRunsNumbers(ctx, run.Repo); err != nil { + if wfRawConcurrency != nil { + if err := EvaluateRunConcurrencyFillModel(ctx, run, runAttempt, wfRawConcurrency, vars, inputs); err != nil { + return fmt.Errorf("EvaluateRunConcurrencyFillModel: %w", err) + } + // check run (workflow-level) concurrency + var jobsToCancel []*actions_model.ActionRunJob + runAttempt.Status, jobsToCancel, err = PrepareToStartRunWithConcurrency(ctx, runAttempt) + if err != nil { + return err + } + cancelledConcurrencyJobs = append(cancelledConcurrencyJobs, jobsToCancel...) + } + + if err := db.Insert(ctx, runAttempt); err != nil { + return err + } + run.LatestAttemptID = runAttempt.ID + + giteaCtx := GenerateGiteaContext(ctx, run, runAttempt, nil) + jobs, err := jobparser.Parse(content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext()), jobparser.WithInputs(inputs)) + if err != nil { + return fmt.Errorf("parse workflow: %w", err) + } + titleChanged := len(jobs) > 0 && jobs[0].RunName != "" + if titleChanged { + run.Title = util.EllipsisDisplayString(jobs[0].RunName, 255) + } + + cols := []string{"latest_attempt_id"} + if titleChanged { + cols = append(cols, "title") + } + if err := actions_model.UpdateRun(ctx, run, cols...); err != nil { return err } runJobs := make([]*actions_model.ActionRunJob, 0, len(jobs)) var hasWaitingJobs bool - for _, v := range jobs { + for i, v := range jobs { id, job := v.Job() needs := job.Needs() if err := v.SetJob(id, job.EraseNeeds()); err != nil { @@ -112,18 +136,21 @@ func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobpar } payload, _ := v.Marshal() - shouldBlockJob := len(needs) > 0 || run.NeedApproval || run.Status == actions_model.StatusBlocked + shouldBlockJob := runAttempt.Status == actions_model.StatusBlocked || len(needs) > 0 || run.NeedApproval job.Name = util.EllipsisDisplayString(job.Name, 255) runJob := &actions_model.ActionRunJob{ RunID: run.ID, + RunAttemptID: runAttempt.ID, RepoID: run.RepoID, OwnerID: run.OwnerID, CommitSHA: run.CommitSHA, IsForkPullRequest: run.IsForkPullRequest, Name: job.Name, + Attempt: runAttempt.Attempt, WorkflowPayload: payload, JobID: id, + AttemptJobID: int64(i + 1), Needs: needs, RunsOn: job.RunsOn(), Status: util.Iif(shouldBlockJob, actions_model.StatusBlocked, actions_model.StatusWaiting), @@ -143,7 +170,7 @@ func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobpar // do not evaluate job concurrency when it requires `needs`, the jobs with `needs` will be evaluated later by job emitter if len(needs) == 0 { - err = EvaluateJobConcurrencyFillModel(ctx, run, runJob, vars, inputs) + err = EvaluateJobConcurrencyFillModel(ctx, run, runAttempt, runJob, vars, inputs) if err != nil { return fmt.Errorf("evaluate job concurrency: %w", err) } @@ -152,10 +179,12 @@ func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobpar // If a job needs other jobs ("needs" is not empty), its status is set to StatusBlocked at the entry of the loop // No need to check job concurrency for a blocked job (it will be checked by job emitter later) if runJob.Status == actions_model.StatusWaiting { - runJob.Status, err = PrepareToStartJobWithConcurrency(ctx, runJob) + var jobsToCancel []*actions_model.ActionRunJob + runJob.Status, jobsToCancel, err = PrepareToStartJobWithConcurrency(ctx, runJob) if err != nil { return fmt.Errorf("prepare to start job with concurrency: %w", err) } + cancelledConcurrencyJobs = append(cancelledConcurrencyJobs, jobsToCancel...) } } @@ -167,8 +196,8 @@ func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobpar runJobs = append(runJobs, runJob) } - run.Status = actions_model.AggregateJobStatus(runJobs) - if err := actions_model.UpdateRun(ctx, run, "status"); err != nil { + runAttempt.Status = actions_model.AggregateJobStatus(runJobs) + if err := actions_model.UpdateRunAttempt(ctx, runAttempt, "status"); err != nil { return err } @@ -180,5 +209,12 @@ func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobpar } return nil - }) + }); err != nil { + return err + } + + NotifyWorkflowJobsAndRunsStatusUpdate(ctx, cancelledConcurrencyJobs) + EmitJobsIfReadyByJobs(cancelledConcurrencyJobs) + + return nil } diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 037bf5cddd1..b2dc3f98407 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -67,7 +68,7 @@ func startTasks(ctx context.Context) error { continue } - if err := CreateScheduleTask(ctx, row.Schedule); err != nil { + if err := CreateScheduleTask(ctx, row); err != nil { log.Error("CreateScheduleTask: %v", err) return err } @@ -97,9 +98,12 @@ func startTasks(ctx context.Context) error { return nil } -// CreateScheduleTask creates a scheduled task from a cron action schedule. +// CreateScheduleTask creates a scheduled task from a cron action schedule spec. // It creates an action run based on the schedule, inserts it into the database, and creates commit statuses for each job. -func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) error { +func CreateScheduleTask(ctx context.Context, spec *actions_model.ActionScheduleSpec) error { + cron := spec.Schedule + eventPayload := withScheduleInEventPayload(cron.EventPayload, spec.Spec) + // Create a new action run based on the schedule run := &actions_model.ActionRun{ Title: cron.Title, @@ -110,7 +114,7 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) Ref: cron.Ref, CommitSHA: cron.CommitSHA, Event: cron.Event, - EventPayload: cron.EventPayload, + EventPayload: eventPayload, TriggerEvent: string(webhook_module.HookEventSchedule), ScheduleID: cron.ID, Status: actions_model.StatusWaiting, @@ -126,3 +130,24 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) // Return nil if no errors occurred return nil } + +func withScheduleInEventPayload(eventPayload, schedule string) string { + if schedule == "" || eventPayload == "" { + return eventPayload + } + + event := map[string]any{} + if err := json.Unmarshal([]byte(eventPayload), &event); err != nil { + log.Error("withScheduleInEventPayload: unmarshal: %v", err) + return eventPayload + } + + event["schedule"] = schedule + updatedPayload, err := json.Marshal(event) + if err != nil { + log.Error("withScheduleInEventPayload: marshal: %v", err) + return eventPayload + } + + return string(updatedPayload) +} diff --git a/services/actions/schedule_tasks_test.go b/services/actions/schedule_tasks_test.go new file mode 100644 index 00000000000..770b8426232 --- /dev/null +++ b/services/actions/schedule_tasks_test.go @@ -0,0 +1,41 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "code.gitea.io/gitea/modules/json" + + "github.com/stretchr/testify/assert" +) + +func TestWithScheduleInEventPayload(t *testing.T) { + t.Run("adds schedule to existing payload", func(t *testing.T) { + payload := `{"ref":"refs/heads/main"}` + updated := withScheduleInEventPayload(payload, "*/5 * * * *") + + event := map[string]any{} + assert.NoError(t, json.Unmarshal([]byte(updated), &event)) + assert.Equal(t, "*/5 * * * *", event["schedule"]) + assert.Equal(t, "refs/heads/main", event["ref"]) + }) + + t.Run("keeps empty payload", func(t *testing.T) { + updated := withScheduleInEventPayload("", "37 12 5 1 2") + assert.Empty(t, updated) + }) + + t.Run("keeps payload when schedule empty", func(t *testing.T) { + payload := `{"ref":"refs/heads/main"}` + updated := withScheduleInEventPayload(payload, "") + assert.Equal(t, payload, updated) + }) + + t.Run("keeps payload when malformed JSON", func(t *testing.T) { + payload := `not a json object` + updated := withScheduleInEventPayload(payload, "*/5 * * * *") + assert.Equal(t, payload, updated) + }) +} diff --git a/services/actions/task.go b/services/actions/task.go index 2cb10b6cd8f..9dc3c9a34b7 100644 --- a/services/actions/task.go +++ b/services/actions/task.go @@ -11,7 +11,6 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" secret_model "code.gitea.io/gitea/models/secret" - notify_service "code.gitea.io/gitea/services/notify" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" "google.golang.org/protobuf/types/known/structpb" @@ -78,7 +77,7 @@ func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv return fmt.Errorf("findTaskNeeds: %w", err) } - taskContext, err := generateTaskContext(t) + taskContext, err := generateTaskContext(ctx, t) if err != nil { return fmt.Errorf("generateTaskContext: %w", err) } @@ -102,23 +101,23 @@ func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv } CreateCommitStatusForRunJobs(ctx, job.Run, job) - notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, actionTask) + NotifyWorkflowJobStatusUpdateWithTask(ctx, job, actionTask) // job.Run is loaded inside the transaction before UpdateRunJob sets run.Started, // so Started is zero only on the very first pick-up of that run. if job.Run.Started.IsZero() { - NotifyWorkflowRunStatusUpdateWithReload(ctx, job) + NotifyWorkflowRunStatusUpdateWithReload(ctx, job.RepoID, job.RunID) } return task, true, nil } -func generateTaskContext(t *actions_model.ActionTask) (*structpb.Struct, error) { +func generateTaskContext(ctx context.Context, t *actions_model.ActionTask) (*structpb.Struct, error) { giteaRuntimeToken, err := CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID) if err != nil { return nil, err } - gitCtx := GenerateGiteaContext(t.Job.Run, t.Job) + gitCtx := GenerateGiteaContext(ctx, t.Job.Run, nil, t.Job) gitCtx["token"] = t.Token gitCtx["gitea_runtime_token"] = giteaRuntimeToken diff --git a/services/actions/variables.go b/services/actions/variables.go index 57e6af1d9ba..3593caa2c5e 100644 --- a/services/actions/variables.go +++ b/services/actions/variables.go @@ -16,7 +16,7 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data, desc return nil, err } - v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data), description) + v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.NormalizeStringEOL(data), description) if err != nil { return nil, err } @@ -29,7 +29,7 @@ func UpdateVariableNameData(ctx context.Context, variable *actions_model.ActionV return false, err } - variable.Data = util.ReserveLineBreakForTextarea(variable.Data) + variable.Data = util.NormalizeStringEOL(variable.Data) return actions_model.UpdateVariableCols(ctx, variable, "name", "data", "description") } diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index cffefe08ae6..8c28717e5d0 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -338,26 +338,41 @@ Loop: return false, nil, nil, &ErrWontSign{headSigned} } case commitsSigned: - verification := ParseCommitWithSignature(ctx, headCommit) - if !verification.Verified { + verified, err := AllHeadCommitsVerified(ctx, pr, gitRepo) + if err != nil { + return false, nil, nil, err + } + if !verified { return false, nil, nil, &ErrWontSign{commitsSigned} } - // need to work out merge-base - mergeBaseCommit, err := gitrepo.MergeBase(ctx, pr.BaseRepo, baseCommit.ID.String(), headCommit.ID.String()) - if err != nil { - return false, nil, nil, err - } - commitList, err := headCommit.CommitsBeforeUntil(mergeBaseCommit) - if err != nil { - return false, nil, nil, err - } - for _, commit := range commitList { - verification := ParseCommitWithSignature(ctx, commit) - if !verification.Verified { - return false, nil, nil, &ErrWontSign{commitsSigned} - } - } } } return true, signingKey, signer, nil } + +// AllHeadCommitsVerified checks that every new commit in the PR head has a +// verified signature. +func AllHeadCommitsVerified(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) (bool, error) { + baseCommit, err := gitRepo.GetCommit(pr.BaseBranch) + if err != nil { + return false, err + } + headCommit, err := gitRepo.GetCommit(pr.GetGitHeadRefName()) + if err != nil { + return false, err + } + mergeBaseCommit, err := gitrepo.MergeBase(ctx, pr.BaseRepo, baseCommit.ID.String(), headCommit.ID.String()) + if err != nil { + return false, err + } + commitList, err := headCommit.CommitsBeforeUntil(mergeBaseCommit) + if err != nil { + return false, err + } + for _, commit := range commitList { + if !ParseCommitWithSignature(ctx, commit).Verified { + return false, nil + } + } + return true, nil +} diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index b3a988320bc..1629b2b95e5 100644 --- a/services/automerge/automerge.go +++ b/services/automerge/automerge.go @@ -90,7 +90,7 @@ func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull * // StartPRCheckAndAutoMergeBySHA start an automerge check and auto merge task for all pull requests of repository and SHA func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_model.Repository) error { pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool { - return !pr.HasMerged && pr.CanAutoMerge() + return !pr.HasMerged && pr.IsStatusMergeable() }) if err != nil { return err @@ -251,7 +251,7 @@ func handlePullRequestAutoMerge(pullID int64, sha string) { return } - if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil { + if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, scheduledPRM.MergeStyle, false); err != nil { if errors.Is(err, pull_service.ErrNotReadyToMerge) { log.Info("%-v was scheduled to automerge by an unauthorized user", pr) return diff --git a/services/automergequeue/automergequeue.go b/services/automergequeue/automergequeue.go index e8cc4512a7e..8cfdf3a5398 100644 --- a/services/automergequeue/automergequeue.go +++ b/services/automergequeue/automergequeue.go @@ -25,7 +25,7 @@ var AddToQueue = func(pr *issues_model.PullRequest, sha string) { // StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) { - if pull == nil || pull.HasMerged || !pull.CanAutoMerge() { + if pull == nil || pull.HasMerged || !pull.IsStatusMergeable() { return } diff --git a/services/context/api.go b/services/context/api.go index b49bf9b42c6..3f9f3e1cdd2 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -322,24 +322,6 @@ func RepoRefForAPI(next http.Handler) http.Handler { }) } -// HasAPIError returns true if error occurs in form validation. -func (ctx *APIContext) HasAPIError() bool { - hasErr, ok := ctx.Data["HasError"] - if !ok { - return false - } - return hasErr.(bool) -} - -// GetErrMsg returns error message in form validation. -func (ctx *APIContext) GetErrMsg() string { - msg, _ := ctx.Data["ErrorMsg"].(string) - if msg == "" { - msg = "invalid form data" - } - return msg -} - // NotFoundOrServerError use error check function to determine if the error // is about not found. It responds with 404 status code for not found error, // or error context description for logging purpose of 500 server error. @@ -358,10 +340,10 @@ func (ctx *APIContext) IsUserSiteAdmin() bool { // IsUserRepoAdmin returns true if current user is admin in current repo func (ctx *APIContext) IsUserRepoAdmin() bool { - return ctx.Repo.IsAdmin() + return ctx.Repo.Permission.IsAdmin() } // IsUserRepoWriter returns true if current user has "write" privilege in current repo func (ctx *APIContext) IsUserRepoWriter(unitTypes []unit.Type) bool { - return slices.ContainsFunc(unitTypes, ctx.Repo.CanWrite) + return slices.ContainsFunc(unitTypes, ctx.Repo.Permission.CanWrite) } diff --git a/services/context/base_path.go b/services/context/base_path.go index 63e60c8654e..8c353f7acaf 100644 --- a/services/context/base_path.go +++ b/services/context/base_path.go @@ -44,9 +44,9 @@ func (b *Base) PathParamInt(p string) int { // SetPathParam set request path params into routes func (b *Base) SetPathParam(name, value string) { - if strings.HasPrefix(name, ":") { - setting.PanicInDevOrTesting("path param should not start with ':'") - name = name[1:] - } chi.RouteContext(b).URLParams.Add(name, url.PathEscape(value)) } + +func (b *Base) SetPathParamRaw(name, value string) { + chi.RouteContext(b).URLParams.Add(name, value) +} diff --git a/services/context/context.go b/services/context/context.go index d6030808d87..e8b1663b221 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -163,6 +163,7 @@ func Contexter() func(next http.Handler) http.Handler { base := NewBaseContext(resp, req) ctx := NewWebContext(base, rnd, session.GetContextSession(req)) ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) + ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI() ctx.Data["Link"] = ctx.Link // PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules @@ -196,10 +197,6 @@ func Contexter() func(next http.Handler) http.Handler { httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true}) - if setting.Security.XFrameOptions != "unset" { - ctx.Resp.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions) - } - ctx.Data["SystemConfig"] = setting.Config() ctx.Data["ShowTwoFactorRequiredMessage"] = ctx.DoerNeedTwoFactorAuth() @@ -209,7 +206,6 @@ func Contexter() func(next http.Handler) http.Handler { ctx.Data["DisableStars"] = setting.Repository.DisableStars ctx.Data["EnableActions"] = setting.Actions.Enabled && !unit.TypeActions.UnitGlobalDisabled() - ctx.Data["ManifestData"] = setting.ManifestData ctx.Data["AllLangs"] = translation.AllLangs() next.ServeHTTP(ctx.Resp, ctx.Req) diff --git a/services/context/context_response.go b/services/context/context_response.go index d057f8d41eb..1fd7a0b447f 100644 --- a/services/context/context_response.go +++ b/services/context/context_response.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" ) @@ -143,11 +144,9 @@ func (ctx *Context) NotFound(logErr error) { } func (ctx *Context) notFoundInternal(logMsg string, logErr error) { + // TODO: it's safe to show the error message to end users if the error is fully controlled by our error system if logErr != nil { log.Log(2, log.DEBUG, "%s: %v", logMsg, logErr) - if !setting.IsProd { - ctx.Data["ErrorMsg"] = logErr - } } // response simple message if Accept isn't text/html @@ -166,11 +165,17 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) { ctx.Data["IsRepo"] = ctx.Repo.Repository != nil ctx.Data["Title"] = "Page Not Found" + ctx.Data["ErrorMsg"] = "" // FIXME: the template never renders this message, need to fix in the future (and show safe messages to end users) ctx.HTML(http.StatusNotFound, "status/404") } // ServerError displays a 500 (Internal Server Error) page and prints the given error, if any. +// If the error is controlled by our error system, a related 404 page can be displayed instead. func (ctx *Context) ServerError(logMsg string, logErr error) { + if errors.Is(logErr, util.ErrNotExist) { + ctx.notFoundInternal(logMsg, logErr) + return + } ctx.serverErrorInternal(logMsg, logErr) } diff --git a/services/context/context_template.go b/services/context/context_template.go index 2b34681faa4..b63aaf4c3c3 100644 --- a/services/context/context_template.go +++ b/services/context/context_template.go @@ -88,7 +88,7 @@ func (c TemplateContext) AppFullLink(link ...string) template.URL { if len(link) == 0 { return template.URL(s) } - return template.URL(s + strings.TrimPrefix(link[0], "/")) + return template.URL(s + "/" + strings.TrimPrefix(link[0], "/")) } var globalVars = sync.OnceValue(func() (ret struct { @@ -148,8 +148,7 @@ func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML { // * Maybe this approach should be avoided, don't make the config system too complex, just let users use A return template.HTML(` 1 { + url := fmt.Sprintf("%s/actions/runs/%d/attempts/%d", repo.APIURL(), run.ID, attempt.Attempt-1) + previousAttemptURL = &url + } + } + return &api.ActionWorkflowRun{ - ID: run.ID, - URL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID), - HTMLURL: run.HTMLURL(), - RunNumber: run.Index, - StartedAt: run.Started.AsLocalTime(), - CompletedAt: run.Stopped.AsLocalTime(), - Event: string(run.Event), - DisplayTitle: run.Title, - HeadBranch: git.RefName(run.Ref).BranchName(), - HeadSha: run.CommitSHA, - Status: status, - Conclusion: conclusion, - Path: fmt.Sprintf("%s@%s", run.WorkflowID, run.Ref), - Repository: ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}), - TriggerActor: ToUser(ctx, run.TriggerUser, nil), - // We do not have a way to get a different User for the actor than the trigger user - Actor: ToUser(ctx, run.TriggerUser, nil), + ID: run.ID, + URL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID), + PreviousAttemptURL: previousAttemptURL, + HTMLURL: run.HTMLURL(), + RunNumber: run.Index, + RunAttempt: runAttempt, + StartedAt: startedAt, + CompletedAt: completedAt, + Event: run.TriggerEvent, + DisplayTitle: run.Title, + HeadBranch: git.RefName(run.Ref).BranchName(), + HeadSha: run.CommitSHA, + Status: status, + Conclusion: conclusion, + Path: fmt.Sprintf("%s@%s", run.WorkflowID, run.Ref), + Repository: ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}), + TriggerActor: ToUser(ctx, triggerUser, nil), + Actor: ToUser(ctx, actor, nil), }, nil } @@ -329,9 +363,9 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task var runnerName string var steps []*api.ActionWorkflowStep - if job.TaskID != 0 { + if effectiveTaskID := job.EffectiveTaskID(); effectiveTaskID != 0 { if task == nil { - task, _, err = db.GetByID[actions_model.ActionTask](ctx, job.TaskID) + task, _, err = db.GetByID[actions_model.ActionTask](ctx, effectiveTaskID) if err != nil { return nil, err } @@ -797,7 +831,7 @@ func ToOAuth2Application(app *auth.OAuth2Application) *api.OAuth2Application { // ToLFSLock convert a LFSLock to api.LFSLock func ToLFSLock(ctx context.Context, l *git_model.LFSLock) *api.LFSLock { - u, err := user_model.GetPossibleUserByID(ctx, l.OwnerID) + _, u, err := user_model.GetPossibleUserByID(ctx, l.OwnerID) if err != nil { return nil } diff --git a/services/convert/secret.go b/services/convert/secret.go deleted file mode 100644 index dd7b9f0a6a9..00000000000 --- a/services/convert/secret.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package convert - -import ( - secret_model "code.gitea.io/gitea/models/secret" - api "code.gitea.io/gitea/modules/structs" -) - -// ToSecret converts Secret to API format -func ToSecret(secret *secret_model.Secret) *api.Secret { - result := &api.Secret{ - Name: secret.Name, - } - - return result -} diff --git a/services/cron/cron.go b/services/cron/cron.go index 7a4eb21bbbd..a0b3c7b5ac2 100644 --- a/services/cron/cron.go +++ b/services/cron/cron.go @@ -111,7 +111,7 @@ func ListTasks() TaskTable { spec = tags[1] // the second tag is the task spec } next, _ = e.NextRun() - prev, _ = e.LastRun() + prev, _ = e.LastRunStartedAt() } task.lock.Lock() diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 01e57a596ef..09b9b2690c4 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -740,14 +740,3 @@ func (f *AddTimeManuallyForm) Validate(req *http.Request, errs binding.Errors) b type SaveTopicForm struct { Topics []string `binding:"topics;Required;"` } - -// DeadlineForm hold the validation rules for deadlines -type DeadlineForm struct { - DateString string `form:"date" binding:"Required;Size(10)"` -} - -// Validate validates the fields -func (f *DeadlineForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} diff --git a/services/forms/user_form.go b/services/forms/user_form.go index cc514a2e279..3f65e8c551f 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -7,9 +7,13 @@ package forms import ( "mime/multipart" "net/http" + "strings" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/context" @@ -356,14 +360,29 @@ func (f *NewAccessTokenForm) Validate(req *http.Request, errs binding.Errors) bi // EditOAuth2ApplicationForm form for editing oauth2 applications type EditOAuth2ApplicationForm struct { Name string `binding:"Required;MaxSize(255)" form:"application_name"` - RedirectURIs string `binding:"Required;ValidUrlList" form:"redirect_uris"` + RedirectURIs string `binding:"Required" form:"redirect_uris"` ConfidentialClient bool `form:"confidential_client"` SkipSecondaryAuthorization bool `form:"skip_secondary_authorization"` } +func DetectInvalidOAuth2ApplicationRedirectURI(uris []string) (invalidURL string) { + for _, u := range uris { + scheme, _, ok := strings.Cut(u, ":") + valid := ok && (validation.IsValidURL(u) || util.SliceContainsString(setting.OAuth2.CustomSchemes, scheme)) + if !valid { + return u + } + } + return "" +} + // Validate validates the fields func (f *EditOAuth2ApplicationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { ctx := context.GetValidateContext(req) + invalidURI := DetectInvalidOAuth2ApplicationRedirectURI(util.SplitTrimSpace(f.RedirectURIs, "\n")) + if invalidURI != "" { + errs = middleware.ReportValidationError(errs, ctx.Data, "RedirectURIs", binding.ERR_URL, ctx.Locale.TrString("form.url_error", invalidURI)) + } return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } diff --git a/services/forms/user_form_test.go b/services/forms/user_form_test.go index 4246f955b3e..c082f40294b 100644 --- a/services/forms/user_form_test.go +++ b/services/forms/user_form_test.go @@ -14,15 +14,9 @@ import ( ) func TestRegisterForm_IsDomainAllowed_Empty(t *testing.T) { - oldService := setting.Service - defer func() { - setting.Service = oldService - }() - + defer test.MockVariableValue(&setting.Service)() setting.Service.EmailDomainAllowList = nil - form := RegisterForm{} - assert.True(t, form.IsEmailDomainAllowed()) } @@ -87,3 +81,30 @@ func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) { assert.Equal(t, v.valid, form.IsEmailDomainAllowed()) } } + +func TestDetectInvalidOAuth2ApplicationRedirectURI(t *testing.T) { + defer test.MockVariableValue(&setting.OAuth2.CustomSchemes)() + setting.OAuth2.CustomSchemes = []string{"my-app"} + assertValid := func(t *testing.T, s string, valid bool) { + ret := DetectInvalidOAuth2ApplicationRedirectURI([]string{s}) + if valid { + assert.Empty(t, ret) + } else { + assert.Equal(t, s, ret) + } + } + assertValid(t, "my-app:", true) + assertValid(t, "my-app:/foo", true) + assertValid(t, "http://foo", true) + assertValid(t, "https://foo", true) + + assertValid(t, "my-app", false) + assertValid(t, "ftp:", false) + assertValid(t, "ftp://foo", false) + assertValid(t, "https://[invalid", false) + + ret := DetectInvalidOAuth2ApplicationRedirectURI([]string{"my-app:", "http://foo", "https://foo"}) + assert.Empty(t, ret) + ret = DetectInvalidOAuth2ApplicationRedirectURI([]string{"my-app:", "http://foo", "invalid", "https://foo"}) + assert.Equal(t, "invalid", ret) +} diff --git a/services/git/compare.go b/services/git/compare.go index 251a0350585..a8c29801125 100644 --- a/services/git/compare.go +++ b/services/git/compare.go @@ -5,6 +5,7 @@ package git import ( "context" + "errors" "fmt" repo_model "code.gitea.io/gitea/models/repo" @@ -43,23 +44,22 @@ func (ci *CompareInfo) DirectComparison() bool { } // GetCompareInfo generates and returns compare information between base and head branches of repositories. -func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Repository, headGitRepo *git.Repository, baseRef, headRef git.RefName, directComparison, fileOnly bool) (_ *CompareInfo, err error) { - compareInfo := &CompareInfo{ +// It does its best to fill the fields as many as it can. +func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Repository, headGitRepo *git.Repository, baseRef, headRef git.RefName, directComparison, fileOnly bool) (compareInfo CompareInfo, err error) { + baseCommitID, err1 := gitrepo.GetFullCommitID(ctx, baseRepo, baseRef.String()) + headCommitID, err2 := gitrepo.GetFullCommitID(ctx, headRepo, headRef.String()) + compareInfo = CompareInfo{ BaseRepo: baseRepo, BaseRef: baseRef, + BaseCommitID: baseCommitID, HeadRepo: headRepo, HeadGitRepo: headGitRepo, HeadRef: headRef, + HeadCommitID: headCommitID, CompareSeparator: util.Iif(directComparison, "..", "..."), } - - compareInfo.BaseCommitID, err = gitrepo.GetFullCommitID(ctx, baseRepo, baseRef.String()) - if err != nil { - return nil, err - } - compareInfo.HeadCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, headRef.String()) - if err != nil { - return nil, err + if err1 != nil || err2 != nil { + return compareInfo, errors.Join(err1, err2) } // if they are not the same repository, then we need to fetch the base commit into the head repository @@ -68,7 +68,7 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito exist := headGitRepo.IsReferenceExist(compareInfo.BaseCommitID) if !exist { if err := gitrepo.FetchRemoteCommit(ctx, headRepo, baseRepo, compareInfo.BaseCommitID); err != nil { - return nil, fmt.Errorf("FetchRemoteCommit: %w", err) + return compareInfo, fmt.Errorf("FetchRemoteCommit: %w", err) } } } @@ -76,7 +76,7 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito if !directComparison { compareInfo.MergeBase, err = gitrepo.MergeBase(ctx, headRepo, compareInfo.BaseCommitID, compareInfo.HeadCommitID) if err != nil { - return nil, fmt.Errorf("MergeBase: %w", err) + return compareInfo, fmt.Errorf("MergeBase: %w", err) } } else { compareInfo.MergeBase = compareInfo.BaseCommitID @@ -90,7 +90,7 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito // Otherwise, commits newly pushed to the base branch would also be included, which is incorrect. compareInfo.Commits, err = headGitRepo.ShowPrettyFormatLogToList(ctx, compareInfo.MergeBase+".."+compareInfo.HeadCommitID) if err != nil { - return nil, fmt.Errorf("ShowPrettyFormatLogToList: %w", err) + return compareInfo, fmt.Errorf("ShowPrettyFormatLogToList: %w", err) } } else { compareInfo.Commits = []*git.Commit{} @@ -100,8 +100,5 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito // This probably should be removed as we need to use shortstat elsewhere // Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly compareInfo.NumFiles, err = headGitRepo.GetDiffNumChangedFiles(compareInfo.BaseCommitID, compareInfo.HeadCommitID, directComparison) - if err != nil { - return nil, err - } - return compareInfo, nil + return compareInfo, err } diff --git a/services/issue/assignee.go b/services/issue/assignee.go index 5a64c722b30..44024389cfc 100644 --- a/services/issue/assignee.go +++ b/services/issue/assignee.go @@ -7,13 +7,10 @@ import ( "context" issues_model "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/container" notify_service "code.gitea.io/gitea/services/notify" ) @@ -62,267 +59,85 @@ func ToggleAssigneeWithNotify(ctx context.Context, issue *issues_model.Issue, do return removed, comment, err } -// ReviewRequest add or remove a review request from a user for this PR, and make comment for it. -func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, permDoer *access_model.Permission, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) { - err = isValidReviewRequest(ctx, reviewer, doer, isAdd, issue, permDoer) - if err != nil { - return nil, err +// UpdateAssignees is a helper function to add or delete one or multiple issue assignee(s) +// Deleting is done the GitHub way (quote from their api documentation): +// https://developer.github.com/v3/issues/#edit-an-issue +// "assignees" (array): Logins for Users to assign to this issue. +// Pass one or more user logins to replace the set of assignees on this Issue. +// Send an empty array ([]) to clear all assignees from the Issue. +func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) { + uniqueAssignees := container.SetOf(multipleAssignees...) + + // Keep the old assignee thingy for compatibility reasons + if oneAssignee != "" { + uniqueAssignees.Add(oneAssignee) } - if isAdd { - comment, err = issues_model.AddReviewRequest(ctx, issue, reviewer, doer, false) - } else { - comment, err = issues_model.RemoveReviewRequest(ctx, issue, reviewer, doer) - } - - if err != nil { - return nil, err - } - - if comment != nil { - notify_service.PullRequestReviewRequest(ctx, doer, issue, reviewer, isAdd, comment) - } - - return comment, err -} - -// isValidReviewRequest Check permission for ReviewRequest -func isValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *issues_model.Issue, permDoer *access_model.Permission) error { - if reviewer.IsOrganization() { - return issues_model.ErrNotValidReviewRequest{ - Reason: "Organization can't be added as reviewer", - UserID: doer.ID, - RepoID: issue.Repo.ID, + // Loop through all assignees to add them + allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees)) + for _, assigneeName := range uniqueAssignees.Values() { + assignee, err := user_model.GetUserByName(ctx, assigneeName) + if err != nil { + return err } - } - if doer.IsOrganization() { - return issues_model.ErrNotValidReviewRequest{ - Reason: "Organization can't be doer to add reviewer", - UserID: doer.ID, - RepoID: issue.Repo.ID, + + if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) { + return user_model.ErrBlockedUser } + + allNewAssignees = append(allNewAssignees, assignee) } - permReviewer, err := access_model.GetIndividualUserRepoPermission(ctx, issue.Repo, reviewer) - if err != nil { + // Delete all old assignees not passed + if err = DeleteNotPassedAssignee(ctx, issue, doer, allNewAssignees); err != nil { return err } - if permDoer == nil { - permDoer = new(access_model.Permission) - *permDoer, err = access_model.GetDoerRepoPermission(ctx, issue.Repo, doer) + // Add all new assignees + // Update the assignee. The function will check if the user exists, is already + // assigned (which he shouldn't as we deleted all assignees before) and + // has access to the repo. + for _, assignee := range allNewAssignees { + // Extra method to prevent double adding (which would result in removing) + _, err = AddAssigneeIfNotAssigned(ctx, issue, doer, assignee.ID, true) if err != nil { return err } } - lastReview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) - if err != nil && !issues_model.IsErrReviewNotExist(err) { - return err - } - - canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID) - - if isAdd { - if !permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) { - return issues_model.ErrNotValidReviewRequest{ - Reason: "Reviewer can't read", - UserID: doer.ID, - RepoID: issue.Repo.ID, - } - } - - if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 { - return issues_model.ErrNotValidReviewRequest{ - Reason: "poster of pr can't be reviewer", - UserID: doer.ID, - RepoID: issue.Repo.ID, - } - } - - if canDoerChangeReviewRequests { - return nil - } - - if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastReview != nil && lastReview.Type != issues_model.ReviewTypeRequest { - return nil - } - - return issues_model.ErrNotValidReviewRequest{ - Reason: "Doer can't choose reviewer", - UserID: doer.ID, - RepoID: issue.Repo.ID, - } - } - - if canDoerChangeReviewRequests { - return nil - } - - if lastReview != nil && lastReview.Type == issues_model.ReviewTypeRequest && lastReview.ReviewerID == doer.ID { - return nil - } - - return issues_model.ErrNotValidReviewRequest{ - Reason: "Doer can't remove reviewer", - UserID: doer.ID, - RepoID: issue.Repo.ID, - } -} - -// isValidTeamReviewRequest Check permission for ReviewRequest Team -func isValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *issues_model.Issue) error { - if doer.IsOrganization() { - return issues_model.ErrNotValidReviewRequest{ - Reason: "Organization can't be doer to add reviewer", - UserID: doer.ID, - RepoID: issue.Repo.ID, - } - } - - canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID) - - if isAdd { - if issue.Repo.IsPrivate { - hasTeam := organization.HasTeamRepo(ctx, reviewer.OrgID, reviewer.ID, issue.RepoID) - - if !hasTeam { - return issues_model.ErrNotValidReviewRequest{ - Reason: "Reviewing team can't read repo", - UserID: doer.ID, - RepoID: issue.Repo.ID, - } - } - } - - if canDoerChangeReviewRequests { - return nil - } - - return issues_model.ErrNotValidReviewRequest{ - Reason: "Doer can't choose reviewer", - UserID: doer.ID, - RepoID: issue.Repo.ID, - } - } - - if canDoerChangeReviewRequests { - return nil - } - - return issues_model.ErrNotValidReviewRequest{ - Reason: "Doer can't remove reviewer", - UserID: doer.ID, - RepoID: issue.Repo.ID, - } -} - -// TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it. -func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) { - err = isValidTeamReviewRequest(ctx, reviewer, doer, isAdd, issue) - if err != nil { - return nil, err - } - if isAdd { - comment, err = issues_model.AddTeamReviewRequest(ctx, issue, reviewer, doer, false) - } else { - comment, err = issues_model.RemoveTeamReviewRequest(ctx, issue, reviewer, doer) - } - - if err != nil { - return nil, err - } - - if comment == nil || !isAdd { - return nil, nil //nolint:nilnil // return nil because no comment was created or it is a removal - } - - return comment, teamReviewRequestNotify(ctx, issue, doer, reviewer, isAdd, comment) -} - -func ReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewNotifiers []*ReviewRequestNotifier) { - for _, reviewNotifier := range reviewNotifiers { - if reviewNotifier.Reviewer != nil { - notify_service.PullRequestReviewRequest(ctx, issue.Poster, issue, reviewNotifier.Reviewer, reviewNotifier.IsAdd, reviewNotifier.Comment) - } else if reviewNotifier.ReviewTeam != nil { - if err := teamReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifier.ReviewTeam, reviewNotifier.IsAdd, reviewNotifier.Comment); err != nil { - log.Error("teamReviewRequestNotify: %v", err) - } - } - } -} - -// teamReviewRequestNotify notify all user in this team -func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool, comment *issues_model.Comment) error { - // notify all user in this team - if err := comment.LoadIssue(ctx); err != nil { - return err - } - - members, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{ - TeamID: reviewer.ID, - }) - if err != nil { - return err - } - - for _, member := range members { - if member.ID == comment.Issue.PosterID { - continue - } - comment.AssigneeID = member.ID - notify_service.PullRequestReviewRequest(ctx, doer, issue, member, isAdd, comment) - } - return err } -// CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR -func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, posterID int64) bool { - if repo.IsArchived { - return false - } - // The poster of the PR can change the reviewers - if doer.ID == posterID { - return true - } - - // The owner of the repo can change the reviewers - if doer.ID == repo.OwnerID { - return true - } - - // Collaborators of the repo can change the reviewers - isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, doer.ID) +// AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue. +// Also checks for access of assigned user +func AddAssigneeIfNotAssigned(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64, notify bool) (comment *issues_model.Comment, err error) { + assignee, err := user_model.GetUserByID(ctx, assigneeID) if err != nil { - log.Error("IsCollaborator: %v", err) - return false - } - if isCollaborator { - return true + return nil, err } - // If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers - if repo.Owner.IsOrganization() { - teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests) - if err != nil { - log.Error("GetTeamsWithAccessToRepo: %v", err) - return false - } - for _, team := range teams { - if !team.UnitEnabled(ctx, unit.TypePullRequests) { - continue - } - isMember, err := organization.IsTeamMember(ctx, repo.OwnerID, team.ID, doer.ID) - if err != nil { - log.Error("IsTeamMember: %v", err) - continue - } - if isMember { - return true - } - } + // Check if the user is already assigned + isAssigned, err := issues_model.IsUserAssignedToIssue(ctx, issue, assignee) + if err != nil { + return nil, err + } + if isAssigned { + // nothing to do + return nil, nil //nolint:nilnil // return nil because the user is already assigned } - return false + valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull) + if err != nil { + return nil, err + } + if !valid { + return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name} + } + + if notify { + _, comment, err = ToggleAssigneeWithNotify(ctx, issue, doer, assigneeID) + return comment, err + } + _, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID) + return comment, err } diff --git a/services/issue/issue.go b/services/issue/issue.go index 9beb4c46ec4..5b57b2453ea 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -15,7 +15,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" @@ -131,55 +130,6 @@ func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_m return nil } -// UpdateAssignees is a helper function to add or delete one or multiple issue assignee(s) -// Deleting is done the GitHub way (quote from their api documentation): -// https://developer.github.com/v3/issues/#edit-an-issue -// "assignees" (array): Logins for Users to assign to this issue. -// Pass one or more user logins to replace the set of assignees on this Issue. -// Send an empty array ([]) to clear all assignees from the Issue. -func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) { - uniqueAssignees := container.SetOf(multipleAssignees...) - - // Keep the old assignee thingy for compatibility reasons - if oneAssignee != "" { - uniqueAssignees.Add(oneAssignee) - } - - // Loop through all assignees to add them - allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees)) - for _, assigneeName := range uniqueAssignees.Values() { - assignee, err := user_model.GetUserByName(ctx, assigneeName) - if err != nil { - return err - } - - if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) { - return user_model.ErrBlockedUser - } - - allNewAssignees = append(allNewAssignees, assignee) - } - - // Delete all old assignees not passed - if err = DeleteNotPassedAssignee(ctx, issue, doer, allNewAssignees); err != nil { - return err - } - - // Add all new assignees - // Update the assignee. The function will check if the user exists, is already - // assigned (which he shouldn't as we deleted all assignees before) and - // has access to the repo. - for _, assignee := range allNewAssignees { - // Extra method to prevent double adding (which would result in removing) - _, err = AddAssigneeIfNotAssigned(ctx, issue, doer, assignee.ID, true) - if err != nil { - return err - } - } - - return err -} - // DeleteIssue deletes an issue func DeleteIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) error { // load issue before deleting it @@ -214,40 +164,6 @@ func DeleteIssue(ctx context.Context, doer *user_model.User, issue *issues_model return nil } -// AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue. -// Also checks for access of assigned user -func AddAssigneeIfNotAssigned(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64, notify bool) (comment *issues_model.Comment, err error) { - assignee, err := user_model.GetUserByID(ctx, assigneeID) - if err != nil { - return nil, err - } - - // Check if the user is already assigned - isAssigned, err := issues_model.IsUserAssignedToIssue(ctx, issue, assignee) - if err != nil { - return nil, err - } - if isAssigned { - // nothing to do - return nil, nil //nolint:nilnil // return nil because the user is already assigned - } - - valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull) - if err != nil { - return nil, err - } - if !valid { - return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name} - } - - if notify { - _, comment, err = ToggleAssigneeWithNotify(ctx, issue, doer, assigneeID) - return comment, err - } - _, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID) - return comment, err -} - // GetRefEndNamesAndURLs retrieves the ref end names (e.g. refs/heads/branch-name -> branch-name) // and their respective URLs. func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[int64]string, map[int64]string) { diff --git a/services/issue/pull.go b/services/issue/pull.go index f415ebe7595..3fc9c335f93 100644 --- a/services/issue/pull.go +++ b/services/issue/pull.go @@ -133,7 +133,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullReque if u.ID != issue.Poster.ID && !contain(latestReviews, u) { comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster, true) if err != nil { - log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err) + log.Warn("Failed add review user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err) return nil, err } if comment == nil { // comment maybe nil if review type is ReviewTypeRequest @@ -150,7 +150,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullReque for _, t := range uniqTeams { comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster, true) if err != nil { - log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err) + log.Warn("Failed add reviewer team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err) return nil, err } if comment == nil { // comment maybe nil if review type is ReviewTypeRequest diff --git a/services/issue/review_request.go b/services/issue/review_request.go new file mode 100644 index 00000000000..23fe9d171ee --- /dev/null +++ b/services/issue/review_request.go @@ -0,0 +1,283 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issue + +import ( + "context" + + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + notify_service "code.gitea.io/gitea/services/notify" +) + +// ReviewRequest add or remove a review request from a user for this PR, and make comment for it. +func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, permDoer *access_model.Permission, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) { + err = isValidReviewRequest(ctx, reviewer, doer, isAdd, issue, permDoer) + if err != nil { + return nil, err + } + + if isAdd { + comment, err = issues_model.AddReviewRequest(ctx, issue, reviewer, doer, false) + } else { + comment, err = issues_model.RemoveReviewRequest(ctx, issue, reviewer, doer) + } + + if err != nil { + return nil, err + } + + if comment != nil { + notify_service.PullRequestReviewRequest(ctx, doer, issue, reviewer, isAdd, comment) + } + + return comment, err +} + +// isValidReviewRequest Check permission for ReviewRequest +func isValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *issues_model.Issue, permDoer *access_model.Permission) error { + if reviewer.IsOrganization() { + return issues_model.ErrNotValidReviewRequest{ + Reason: "Organization can't be added as reviewer", + UserID: doer.ID, + RepoID: issue.Repo.ID, + } + } + if doer.IsOrganization() { + return issues_model.ErrNotValidReviewRequest{ + Reason: "Organization can't be doer to add reviewer", + UserID: doer.ID, + RepoID: issue.Repo.ID, + } + } + + permReviewer, err := access_model.GetIndividualUserRepoPermission(ctx, issue.Repo, reviewer) + if err != nil { + return err + } + + if permDoer == nil { + permDoer = new(access_model.Permission) + *permDoer, err = access_model.GetDoerRepoPermission(ctx, issue.Repo, doer) + if err != nil { + return err + } + } + + lastReview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) + if err != nil && !issues_model.IsErrReviewNotExist(err) { + return err + } + + canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID) + + if isAdd { + if !permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) { + return issues_model.ErrNotValidReviewRequest{ + Reason: "Reviewer can't read", + UserID: doer.ID, + RepoID: issue.Repo.ID, + } + } + + if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 { + return issues_model.ErrNotValidReviewRequest{ + Reason: "poster of pr can't be reviewer", + UserID: doer.ID, + RepoID: issue.Repo.ID, + } + } + + if canDoerChangeReviewRequests { + return nil + } + + if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastReview != nil && lastReview.Type != issues_model.ReviewTypeRequest { + return nil + } + + return issues_model.ErrNotValidReviewRequest{ + Reason: "Doer can't choose reviewer", + UserID: doer.ID, + RepoID: issue.Repo.ID, + } + } + + if canDoerChangeReviewRequests { + return nil + } + + if lastReview != nil && lastReview.Type == issues_model.ReviewTypeRequest && lastReview.ReviewerID == doer.ID { + return nil + } + + return issues_model.ErrNotValidReviewRequest{ + Reason: "Doer can't remove reviewer", + UserID: doer.ID, + RepoID: issue.Repo.ID, + } +} + +// isValidTeamReviewRequest Check permission for ReviewRequest Team +func isValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *issues_model.Issue) error { + if doer.IsOrganization() { + return issues_model.ErrNotValidReviewRequest{ + Reason: "Organization can't be doer to add reviewer", + UserID: doer.ID, + RepoID: issue.Repo.ID, + } + } + + canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue.PosterID) + + if isAdd { + if issue.Repo.IsPrivate { + hasTeam := organization.HasTeamRepo(ctx, reviewer.OrgID, reviewer.ID, issue.RepoID) + + if !hasTeam { + return issues_model.ErrNotValidReviewRequest{ + Reason: "Reviewing team can't read repo", + UserID: doer.ID, + RepoID: issue.Repo.ID, + } + } + } + + if canDoerChangeReviewRequests { + return nil + } + + return issues_model.ErrNotValidReviewRequest{ + Reason: "Doer can't choose reviewer", + UserID: doer.ID, + RepoID: issue.Repo.ID, + } + } + + if canDoerChangeReviewRequests { + return nil + } + + return issues_model.ErrNotValidReviewRequest{ + Reason: "Doer can't remove reviewer", + UserID: doer.ID, + RepoID: issue.Repo.ID, + } +} + +// TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it. +func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) { + err = isValidTeamReviewRequest(ctx, reviewer, doer, isAdd, issue) + if err != nil { + return nil, err + } + if isAdd { + comment, err = issues_model.AddTeamReviewRequest(ctx, issue, reviewer, doer, false) + } else { + comment, err = issues_model.RemoveTeamReviewRequest(ctx, issue, reviewer, doer) + } + + if err != nil { + return nil, err + } + + if comment == nil || !isAdd { + return nil, nil //nolint:nilnil // return nil because no comment was created or it is a removal + } + + return comment, teamReviewRequestNotify(ctx, issue, doer, reviewer, isAdd, comment) +} + +func ReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewNotifiers []*ReviewRequestNotifier) { + for _, reviewNotifier := range reviewNotifiers { + if reviewNotifier.Reviewer != nil { + notify_service.PullRequestReviewRequest(ctx, issue.Poster, issue, reviewNotifier.Reviewer, reviewNotifier.IsAdd, reviewNotifier.Comment) + } else if reviewNotifier.ReviewTeam != nil { + if err := teamReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifier.ReviewTeam, reviewNotifier.IsAdd, reviewNotifier.Comment); err != nil { + log.Error("teamReviewRequestNotify: %v", err) + } + } + } +} + +// teamReviewRequestNotify notify all user in this team +func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool, comment *issues_model.Comment) error { + // notify all user in this team + if err := comment.LoadIssue(ctx); err != nil { + return err + } + + members, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{ + TeamID: reviewer.ID, + }) + if err != nil { + return err + } + + for _, member := range members { + if member.ID == comment.Issue.PosterID { + continue + } + comment.AssigneeID = member.ID + notify_service.PullRequestReviewRequest(ctx, doer, issue, member, isAdd, comment) + } + + return err +} + +// CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR +func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, posterID int64) bool { + if repo.IsArchived { + return false + } + // The poster of the PR can change the reviewers + if doer.ID == posterID { + return true + } + + // The owner of the repo can change the reviewers + if doer.ID == repo.OwnerID { + return true + } + + // Collaborators of the repo can change the reviewers + isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, doer.ID) + if err != nil { + log.Error("IsCollaborator: %v", err) + return false + } + if isCollaborator { + return true + } + + // If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers + if repo.Owner.IsOrganization() { + teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests) + if err != nil { + log.Error("GetTeamsWithAccessToRepo: %v", err) + return false + } + for _, team := range teams { + if !team.UnitEnabled(ctx, unit.TypePullRequests) { + continue + } + isMember, err := organization.IsTeamMember(ctx, repo.OwnerID, team.ID, doer.ID) + if err != nil { + log.Error("IsTeamMember: %v", err) + continue + } + if isMember { + return true + } + } + } + + return false +} diff --git a/services/mailer/mail_workflow_run.go b/services/mailer/mail_workflow_run.go index 9efaa4182b4..18c13bcc755 100644 --- a/services/mailer/mail_workflow_run.go +++ b/services/mailer/mail_workflow_run.go @@ -37,7 +37,7 @@ func generateMessageIDForActionsWorkflowRunStatusEmail(repo *repo_model.Reposito } func composeAndSendActionsWorkflowRunStatusEmail(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, sender *user_model.User, recipients []*user_model.User) error { - jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, repo.ID, run.ID) if err != nil { return err } diff --git a/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Fmilestones b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Fmilestones new file mode 100644 index 00000000000..039abf0c62a --- /dev/null +++ b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Fmilestones @@ -0,0 +1,21 @@ +Content-Type: application/xml; charset=utf-8 + + + + + 1 + milestone1 + Milestone1 + 2021-09-16 + + active + + + 2 + milestone2 + Milestone2 + 2021-09-17 + + closed + + diff --git a/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest new file mode 100644 index 00000000000..e459a32dd45 --- /dev/null +++ b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest @@ -0,0 +1,10 @@ +Content-Type: application/xml; charset=utf-8 + + + + test + Repository Description + test + git@codebasehq.com:gitea-test/gitea-test/test.git + + diff --git a/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest%2Fcommits%2Fmaster b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest%2Fcommits%2Fmaster new file mode 100644 index 00000000000..e2051975335 --- /dev/null +++ b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest%2Fcommits%2Fmaster @@ -0,0 +1,8 @@ +Content-Type: application/xml; charset=utf-8 + + + + + f32b0a9dfd09a60f616f29158f772cedd89942d2 + + diff --git a/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest%2Fcommits%2Freadme-mr b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest%2Fcommits%2Freadme-mr new file mode 100644 index 00000000000..0581ef3756c --- /dev/null +++ b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest%2Fcommits%2Freadme-mr @@ -0,0 +1,8 @@ +Content-Type: application/xml; charset=utf-8 + + + + + 1287f206b888d4d13540e0a8e1c07458f5420059 + + diff --git a/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest%2Fmerge_requests%2F100 b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest%2Fmerge_requests%2F100 new file mode 100644 index 00000000000..322d1b36b34 --- /dev/null +++ b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest%2Fmerge_requests%2F100 @@ -0,0 +1,22 @@ +Content-Type: application/xml; charset=utf-8 + + + + 100 + readme-mr + master + Readme Change + new + 43 + 2021-09-26T20:25:47+00:00 + 2021-09-26T20:25:47+00:00 + + + Merge Request comment + 300 + 43 + + 2021-09-26T20:25:47+00:00 + + + diff --git a/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest%2Fmerge_requests%3Fcount%3D1%26offset%3D0%26query%3D%2522Target%2BProject%2522%2Bis%2B%2522test%2522 b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest%2Fmerge_requests%3Fcount%3D1%26offset%3D0%26query%3D%2522Target%2BProject%2522%2Bis%2B%2522test%2522 new file mode 100644 index 00000000000..0898908021a --- /dev/null +++ b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftest%2Fmerge_requests%3Fcount%3D1%26offset%3D0%26query%3D%2522Target%2BProject%2522%2Bis%2B%2522test%2522 @@ -0,0 +1,8 @@ +Content-Type: application/xml; charset=utf-8 + + + + + 100 + + diff --git a/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftickets b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftickets new file mode 100644 index 00000000000..dfff5c3ab89 --- /dev/null +++ b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftickets @@ -0,0 +1,41 @@ +Content-Type: application/xml; charset=utf-8 + + + + + 2 + Open Ticket + Feature + 43 + gitea-test-43 + + Feature + + + false + + + + + 2021-09-26T19:19:34+00:00 + 2021-09-26T19:19:14+00:00 + + + 1 + Closed Ticket + Bug + 43 + gitea-test-43 + + Bug + + + true + + + Milestone1 + + 2021-09-26T19:18:55+00:00 + 2021-09-26T19:18:33+00:00 + + diff --git a/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftickets%2F1%2Fnotes b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftickets%2F1%2Fnotes new file mode 100644 index 00000000000..640888fb34d --- /dev/null +++ b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftickets%2F1%2Fnotes @@ -0,0 +1,12 @@ +Content-Type: application/xml; charset=utf-8 + + + + + Closed Ticket Message + 2021-09-26T19:18:33+00:00 + 2021-09-26T19:18:33+00:00 + 200 + 43 + + diff --git a/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftickets%2F2%2Fnotes b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftickets%2F2%2Fnotes new file mode 100644 index 00000000000..60628026d2b --- /dev/null +++ b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftickets%2F2%2Fnotes @@ -0,0 +1,19 @@ +Content-Type: application/xml; charset=utf-8 + + + + + Open Ticket Message + 2021-09-26T19:19:14+00:00 + 2021-09-26T19:19:14+00:00 + 100 + 43 + + + open comment + 2021-09-26T19:19:34+00:00 + 2021-09-26T19:19:34+00:00 + 101 + 43 + + diff --git a/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftickets%2Ftypes b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftickets%2Ftypes new file mode 100644 index 00000000000..d7320dded34 --- /dev/null +++ b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fgitea-test%2Ftickets%2Ftypes @@ -0,0 +1,21 @@ +Content-Type: application/xml; charset=utf-8 + + + + + 1 + Bug + + + 2 + Feature + + + 3 + Enhancement + + + 4 + Task + + diff --git a/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fusers b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fusers new file mode 100644 index 00000000000..c6560f4253f --- /dev/null +++ b/services/migrations/_mock_data/TestCodebaseDownloadRepo/GET_%2Fusers @@ -0,0 +1,12 @@ +Content-Type: application/xml; charset=utf-8 + + + + + gitea-codebase@smack.email + 43 + Test + Gitea + gitea-test-43 + + diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frate_limit b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frate_limit new file mode 100644 index 00000000000..ecc04fe8008 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frate_limit @@ -0,0 +1,22 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: no-cache +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: allows_permissionless_access=true +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A2E9E:5B1BA1:69AF24E6 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4937 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 63 +X-Xss-Protection: 0 + +{"resources":{"core":{"limit":5000,"used":63,"remaining":4937,"reset":1773089427},"search":{"limit":30,"used":0,"remaining":30,"reset":1773085986},"graphql":{"limit":5000,"used":40,"remaining":4960,"reset":1773087278},"integration_manifest":{"limit":5000,"used":0,"remaining":5000,"reset":1773089526},"source_import":{"limit":100,"used":0,"remaining":100,"reset":1773085986},"code_scanning_upload":{"limit":5000,"used":63,"remaining":4937,"reset":1773089427},"code_scanning_autofix":{"limit":10,"used":0,"remaining":10,"reset":1773085986},"actions_runner_registration":{"limit":10000,"used":0,"remaining":10000,"reset":1773089526},"scim":{"limit":15000,"used":0,"remaining":15000,"reset":1773089526},"dependency_snapshots":{"limit":100,"used":0,"remaining":100,"reset":1773085986},"dependency_sbom":{"limit":100,"used":0,"remaining":100,"reset":1773085986},"audit_log":{"limit":1750,"used":0,"remaining":1750,"reset":1773089526},"audit_log_streaming":{"limit":15,"used":0,"remaining":15,"reset":1773089526},"code_search":{"limit":10,"used":0,"remaining":10,"reset":1773085986}},"rate":{"limit":5000,"used":63,"remaining":4937,"reset":1773089427}} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo new file mode 100644 index 00000000000..0c8b08ab86b --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"4bd381e9d79f99cd2153b922e196d49eee23b7a4c10bb22a3ab3ffc6cf78ce39" +Last-Modified: Thu, 02 Mar 2023 14:02:26 GMT +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: metadata=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=scarlet-witch-preview; format=json, github.mercy-preview; param=baptiste-preview.nebula-preview; format=json +X-Github-Request-Id: C4F6:A93E1:6A3367:5B1FCA:69AF24E6 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4935 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 65 +X-Xss-Protection: 0 + +{"id":220672974,"node_id":"MDEwOlJlcG9zaXRvcnkyMjA2NzI5NzQ=","name":"test_repo","full_name":"go-gitea/test_repo","private":false,"owner":{"login":"go-gitea","id":12724356,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2","avatar_url":"https://avatars.githubusercontent.com/u/12724356?v=4","gravatar_id":"","url":"https://api.github.com/users/go-gitea","html_url":"https://github.com/go-gitea","followers_url":"https://api.github.com/users/go-gitea/followers","following_url":"https://api.github.com/users/go-gitea/following{/other_user}","gists_url":"https://api.github.com/users/go-gitea/gists{/gist_id}","starred_url":"https://api.github.com/users/go-gitea/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/go-gitea/subscriptions","organizations_url":"https://api.github.com/users/go-gitea/orgs","repos_url":"https://api.github.com/users/go-gitea/repos","events_url":"https://api.github.com/users/go-gitea/events{/privacy}","received_events_url":"https://api.github.com/users/go-gitea/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/go-gitea/test_repo","description":"Test repository for testing migration from github to gitea","fork":false,"url":"https://api.github.com/repos/go-gitea/test_repo","forks_url":"https://api.github.com/repos/go-gitea/test_repo/forks","keys_url":"https://api.github.com/repos/go-gitea/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/go-gitea/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/go-gitea/test_repo/teams","hooks_url":"https://api.github.com/repos/go-gitea/test_repo/hooks","issue_events_url":"https://api.github.com/repos/go-gitea/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/go-gitea/test_repo/events","assignees_url":"https://api.github.com/repos/go-gitea/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/go-gitea/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/go-gitea/test_repo/tags","blobs_url":"https://api.github.com/repos/go-gitea/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/go-gitea/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/go-gitea/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/go-gitea/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/go-gitea/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/go-gitea/test_repo/languages","stargazers_url":"https://api.github.com/repos/go-gitea/test_repo/stargazers","contributors_url":"https://api.github.com/repos/go-gitea/test_repo/contributors","subscribers_url":"https://api.github.com/repos/go-gitea/test_repo/subscribers","subscription_url":"https://api.github.com/repos/go-gitea/test_repo/subscription","commits_url":"https://api.github.com/repos/go-gitea/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/go-gitea/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/go-gitea/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/go-gitea/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/go-gitea/test_repo/merges","archive_url":"https://api.github.com/repos/go-gitea/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/go-gitea/test_repo/downloads","issues_url":"https://api.github.com/repos/go-gitea/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/go-gitea/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/go-gitea/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/go-gitea/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/go-gitea/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/go-gitea/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/go-gitea/test_repo/deployments","created_at":"2019-11-09T16:49:20Z","updated_at":"2023-03-02T14:02:26Z","pushed_at":"2019-11-12T21:54:19Z","git_url":"git://github.com/go-gitea/test_repo.git","ssh_url":"git@github.com:go-gitea/test_repo.git","clone_url":"https://github.com/go-gitea/test_repo.git","svn_url":"https://github.com/go-gitea/test_repo","homepage":null,"size":1,"stargazers_count":3,"watchers_count":3,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":3,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":["gitea"],"visibility":"public","forks":3,"open_issues":3,"watchers":3,"default_branch":"master","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true},"custom_properties":{"vanta_production_branch_name":"main"},"organization":{"login":"go-gitea","id":12724356,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2","avatar_url":"https://avatars.githubusercontent.com/u/12724356?v=4","gravatar_id":"","url":"https://api.github.com/users/go-gitea","html_url":"https://github.com/go-gitea","followers_url":"https://api.github.com/users/go-gitea/followers","following_url":"https://api.github.com/users/go-gitea/following{/other_user}","gists_url":"https://api.github.com/users/go-gitea/gists{/gist_id}","starred_url":"https://api.github.com/users/go-gitea/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/go-gitea/subscriptions","organizations_url":"https://api.github.com/users/go-gitea/orgs","repos_url":"https://api.github.com/users/go-gitea/repos","events_url":"https://api.github.com/users/go-gitea/events{/privacy}","received_events_url":"https://api.github.com/users/go-gitea/received_events","type":"Organization","user_view_type":"public","site_admin":false},"network_count":3,"subscribers_count":4} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage%3D1%26per_page%3D2 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage%3D1%26per_page%3D2 new file mode 100644 index 00000000000..4f602dfe810 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage%3D1%26per_page%3D2 @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"93e41c3c4bea65e67bf69efd92c3b35de2086bf32d265bba99b2128b469a0f2a" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A3CBA:5B27F2:69AF24E8 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4930 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 70 +X-Xss-Protection: 0 + +[{"id":55441655,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0MTY1NQ==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"+1","created_at":"2019-11-12T20:22:13Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage%3D2%26per_page%3D2 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage%3D2%26per_page%3D2 new file mode 100644 index 00000000000..49afda334dc --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage%3D2%26per_page%3D2 @@ -0,0 +1,25 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: "440897bed2748f095d024d64264317f3f31eadc9413bf26dc0450eecbfbbef7b" +Link: ; rel="prev", ; rel="last", ; rel="first" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A3E9A:5B29CC:69AF24E8 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4929 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 71 +X-Xss-Protection: 0 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Fcomments%3Fdirection%3Dasc%26per_page%3D100%26sort%3Dcreated b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Fcomments%3Fdirection%3Dasc%26per_page%3D100%26sort%3Dcreated new file mode 100644 index 00000000000..1a20a130ded --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Fcomments%3Fdirection%3Dasc%26per_page%3D100%26sort%3Dcreated @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"a6b46e35c9645e8f329af283e0f1cd6709c9d41ebc7ad811607a142f4f6f7332" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read; pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A494E:5B32E2:69AF24EA +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4924 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 76 +X-Xss-Protection: 0 + +[{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments/553111966","html_url":"https://github.com/go-gitea/test_repo/issues/2#issuecomment-553111966","issue_url":"https://api.github.com/repos/go-gitea/test_repo/issues/2","id":553111966,"node_id":"MDEyOklzc3VlQ29tbWVudDU1MzExMTk2Ng==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"created_at":"2019-11-12T21:00:13Z","updated_at":"2019-11-12T21:00:13Z","body":"This is a comment","author_association":"MEMBER","pin":null,"reactions":{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments/553111966/reactions","total_count":1,"+1":1,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null},{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments/553138856","html_url":"https://github.com/go-gitea/test_repo/issues/2#issuecomment-553138856","issue_url":"https://api.github.com/repos/go-gitea/test_repo/issues/2","id":553138856,"node_id":"MDEyOklzc3VlQ29tbWVudDU1MzEzODg1Ng==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"created_at":"2019-11-12T22:07:14Z","updated_at":"2019-11-12T22:07:14Z","body":"A second comment","author_association":"MEMBER","pin":null,"reactions":{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments/553138856/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage%3D1%26per_page%3D2 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage%3D1%26per_page%3D2 new file mode 100644 index 00000000000..c81db801188 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage%3D1%26per_page%3D2 @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"9c80e4756af3227671cb0bde1c1798372e9ab36d1d8c27c5332d14ab201b07bd" +Link: ; rel="next", ; rel="last" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A40BF:5B2B98:69AF24E8 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4928 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 72 +X-Xss-Protection: 0 + +[{"id":55445108,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0NTEwOA==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"heart","created_at":"2019-11-12T21:02:05Z"},{"id":55445150,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0NTE1MA==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"laugh","created_at":"2019-11-12T21:02:35Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage%3D2%26per_page%3D2 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage%3D2%26per_page%3D2 new file mode 100644 index 00000000000..308262c18cd --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage%3D2%26per_page%3D2 @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"2df1254488e1d85bd03b21124ee9d20468fa1297c34aa3d347771f1f6035ee01" +Link: ; rel="prev", ; rel="next", ; rel="last", ; rel="first" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A4345:5B2DBE:69AF24E9 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4927 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 73 +X-Xss-Protection: 0 + +[{"id":55445169,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0NTE2OQ==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"-1","created_at":"2019-11-12T21:02:47Z"},{"id":55445177,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0NTE3Nw==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"confused","created_at":"2019-11-12T21:02:52Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage%3D3%26per_page%3D2 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage%3D3%26per_page%3D2 new file mode 100644 index 00000000000..b82e9fe31df --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage%3D3%26per_page%3D2 @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"bc8bfbd8766b50c610b6d7933bdcb10497b92df72b007e78dac92be18c6e9e71" +Link: ; rel="prev", ; rel="first" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A452E:5B2F62:69AF24E9 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4926 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 74 +X-Xss-Protection: 0 + +[{"id":55445188,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0NTE4OA==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"hooray","created_at":"2019-11-12T21:02:58Z"},{"id":55445441,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0NTQ0MQ==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"+1","created_at":"2019-11-12T21:06:04Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage%3D4%26per_page%3D2 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage%3D4%26per_page%3D2 new file mode 100644 index 00000000000..333a2f69815 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage%3D4%26per_page%3D2 @@ -0,0 +1,25 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: "440897bed2748f095d024d64264317f3f31eadc9413bf26dc0450eecbfbbef7b" +Link: ; rel="prev", ; rel="last", ; rel="first" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A4762:5B313E:69AF24E9 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4925 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 75 +X-Xss-Protection: 0 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage%3D1%26per_page%3D2 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage%3D1%26per_page%3D2 new file mode 100644 index 00000000000..7701572a5d3 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage%3D1%26per_page%3D2 @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: "440897bed2748f095d024d64264317f3f31eadc9413bf26dc0450eecbfbbef7b" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A5190:5B3A2F:69AF24EB +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4919 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 81 +X-Xss-Protection: 0 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage%3D1%26per_page%3D2 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage%3D1%26per_page%3D2 new file mode 100644 index 00000000000..89b2c158b71 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage%3D1%26per_page%3D2 @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"28ed9be77bd4b321f93ff1cf9a8ca1b27a7a6eb9cb1495e138864a1b229b3618" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A5612:5B3DFA:69AF24EC +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4918 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 82 +X-Xss-Protection: 0 + +[{"id":59496724,"node_id":"MDEzOklzc3VlUmVhY3Rpb241OTQ5NjcyNA==","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?u=99b64f0ca6ef63643c7583ab87dd31c52d28e673&v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"heart","created_at":"2020-01-10T08:31:30Z"},{"id":59496731,"node_id":"MDEzOklzc3VlUmVhY3Rpb241OTQ5NjczMQ==","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?u=99b64f0ca6ef63643c7583ab87dd31c52d28e673&v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"+1","created_at":"2020-01-10T08:31:39Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage%3D2%26per_page%3D2 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage%3D2%26per_page%3D2 new file mode 100644 index 00000000000..7795ad40640 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage%3D2%26per_page%3D2 @@ -0,0 +1,25 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: "440897bed2748f095d024d64264317f3f31eadc9413bf26dc0450eecbfbbef7b" +Link: ; rel="prev", ; rel="last", ; rel="first" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A5886:5B402D:69AF24EC +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4917 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 83 +X-Xss-Protection: 0 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage%3D1%26per_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage%3D1%26per_page%3D100 new file mode 100644 index 00000000000..3b181445b3e --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage%3D1%26per_page%3D100 @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"1555b1716eec684789b7de36b017090fb2c1b1f1f3264cfdb120d9af0428b43d" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A4AF3:5B343F:69AF24EA +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4923 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 77 +X-Xss-Protection: 0 + +[{"id":55446208,"node_id":"MDIwOklzc3VlQ29tbWVudFJlYWN0aW9uNTU0NDYyMDg=","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"+1","created_at":"2019-11-12T21:13:22Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage%3D2%26per_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage%3D2%26per_page%3D100 new file mode 100644 index 00000000000..dd9a7682c7e --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage%3D2%26per_page%3D100 @@ -0,0 +1,25 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: "440897bed2748f095d024d64264317f3f31eadc9413bf26dc0450eecbfbbef7b" +Link: ; rel="prev", ; rel="last", ; rel="first" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A4C94:5B35BA:69AF24EA +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4922 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 78 +X-Xss-Protection: 0 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553138856%2Freactions%3Fpage%3D1%26per_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553138856%2Freactions%3Fpage%3D1%26per_page%3D100 new file mode 100644 index 00000000000..7adba95f83f --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553138856%2Freactions%3Fpage%3D1%26per_page%3D100 @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: "440897bed2748f095d024d64264317f3f31eadc9413bf26dc0450eecbfbbef7b" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A4E12:5B36FD:69AF24EA +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4921 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 79 +X-Xss-Protection: 0 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%3Fdirection%3Dasc%26page%3D1%26per_page%3D2%26sort%3Dcreated%26state%3Dall b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%3Fdirection%3Dasc%26page%3D1%26per_page%3D2%26sort%3Dcreated%26state%3Dall new file mode 100644 index 00000000000..73154f442c3 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%3Fdirection%3Dasc%26page%3D1%26per_page%3D2%26sort%3Dcreated%26state%3Dall @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"4d1d8ecb86bafe76a686d250018cc158745dd6bc794ef651eeeeecbfc1a947bc" +Link: ; rel="next", ; rel="last" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A3A9E:5B260C:69AF24E7 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4931 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 69 +X-Xss-Protection: 0 + +[{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/1","repository_url":"https://api.github.com/repos/go-gitea/test_repo","labels_url":"https://api.github.com/repos/go-gitea/test_repo/issues/1/labels{/name}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/issues/1/comments","events_url":"https://api.github.com/repos/go-gitea/test_repo/issues/1/events","html_url":"https://github.com/go-gitea/test_repo/issues/1","id":520479843,"node_id":"MDU6SXNzdWU1MjA0Nzk4NDM=","number":1,"title":"Please add an animated gif icon to the merge button","user":{"login":"guillep2k","id":18600385,"node_id":"MDQ6VXNlcjE4NjAwMzg1","avatar_url":"https://avatars.githubusercontent.com/u/18600385?v=4","gravatar_id":"","url":"https://api.github.com/users/guillep2k","html_url":"https://github.com/guillep2k","followers_url":"https://api.github.com/users/guillep2k/followers","following_url":"https://api.github.com/users/guillep2k/following{/other_user}","gists_url":"https://api.github.com/users/guillep2k/gists{/gist_id}","starred_url":"https://api.github.com/users/guillep2k/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/guillep2k/subscriptions","organizations_url":"https://api.github.com/users/guillep2k/orgs","repos_url":"https://api.github.com/users/guillep2k/repos","events_url":"https://api.github.com/users/guillep2k/events{/privacy}","received_events_url":"https://api.github.com/users/guillep2k/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[{"id":1667254252,"node_id":"MDU6TGFiZWwxNjY3MjU0MjUy","url":"https://api.github.com/repos/go-gitea/test_repo/labels/bug","name":"bug","color":"d73a4a","default":true,"description":"Something isn't working"},{"id":1667254261,"node_id":"MDU6TGFiZWwxNjY3MjU0MjYx","url":"https://api.github.com/repos/go-gitea/test_repo/labels/good%20first%20issue","name":"good first issue","color":"7057ff","default":true,"description":"Good for newcomers"}],"state":"closed","locked":false,"assignees":[],"milestone":{"url":"https://api.github.com/repos/go-gitea/test_repo/milestones/1","html_url":"https://github.com/go-gitea/test_repo/milestone/1","labels_url":"https://api.github.com/repos/go-gitea/test_repo/milestones/1/labels","id":4839941,"node_id":"MDk6TWlsZXN0b25lNDgzOTk0MQ==","number":1,"title":"1.0.0","description":"Milestone 1.0.0","creator":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":1,"closed_issues":1,"state":"closed","created_at":"2019-11-12T19:37:08Z","updated_at":"2019-11-12T21:56:17Z","due_on":"2019-11-11T00:00:00Z","closed_at":"2019-11-12T19:45:49Z"},"comments":0,"created_at":"2019-11-09T17:00:29Z","updated_at":"2019-11-12T20:29:53Z","closed_at":"2019-11-12T20:22:22Z","assignee":null,"author_association":"MEMBER","type":null,"active_lock_reason":null,"sub_issues_summary":{"total":0,"completed":0,"percent_completed":0},"issue_dependencies_summary":{"blocked_by":0,"total_blocked_by":0,"blocking":0,"total_blocking":0},"body":"I just want the merge button to hurt my eyes a little. 😝 ","closed_by":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"reactions":{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/1/reactions","total_count":1,"+1":1,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/go-gitea/test_repo/issues/1/timeline","performed_via_github_app":null,"state_reason":"completed","pinned_comment":null},{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/2","repository_url":"https://api.github.com/repos/go-gitea/test_repo","labels_url":"https://api.github.com/repos/go-gitea/test_repo/issues/2/labels{/name}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/issues/2/comments","events_url":"https://api.github.com/repos/go-gitea/test_repo/issues/2/events","html_url":"https://github.com/go-gitea/test_repo/issues/2","id":521799485,"node_id":"MDU6SXNzdWU1MjE3OTk0ODU=","number":2,"title":"Test issue","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[{"id":1667254257,"node_id":"MDU6TGFiZWwxNjY3MjU0MjU3","url":"https://api.github.com/repos/go-gitea/test_repo/labels/duplicate","name":"duplicate","color":"cfd3d7","default":true,"description":"This issue or pull request already exists"}],"state":"closed","locked":false,"assignees":[],"milestone":{"url":"https://api.github.com/repos/go-gitea/test_repo/milestones/2","html_url":"https://github.com/go-gitea/test_repo/milestone/2","labels_url":"https://api.github.com/repos/go-gitea/test_repo/milestones/2/labels","id":4839942,"node_id":"MDk6TWlsZXN0b25lNDgzOTk0Mg==","number":2,"title":"1.1.0","description":"Milestone 1.1.0","creator":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":0,"closed_issues":2,"state":"closed","created_at":"2019-11-12T19:37:25Z","updated_at":"2019-11-12T21:39:27Z","due_on":"2019-11-12T00:00:00Z","closed_at":"2019-11-12T19:45:46Z"},"comments":2,"created_at":"2019-11-12T21:00:06Z","updated_at":"2019-11-12T22:07:14Z","closed_at":"2019-11-12T21:01:31Z","assignee":null,"author_association":"MEMBER","type":null,"active_lock_reason":null,"sub_issues_summary":{"total":0,"completed":0,"percent_completed":0},"issue_dependencies_summary":{"blocked_by":0,"total_blocked_by":0,"blocking":0,"total_blocking":0},"body":"This is test issue 2, do not touch!","closed_by":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"reactions":{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/2/reactions","total_count":6,"+1":1,"-1":1,"laugh":1,"hooray":1,"confused":1,"heart":1,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/go-gitea/test_repo/issues/2/timeline","performed_via_github_app":null,"state_reason":"completed","pinned_comment":null}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Flabels%3Fpage%3D1%26per_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Flabels%3Fpage%3D1%26per_page%3D100 new file mode 100644 index 00000000000..2a8f7ba319d --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Flabels%3Fpage%3D1%26per_page%3D100 @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"ea38b60f45fc2583351b3e4cc26f7e7a1b0ba2f6545b0e2dac7ffc5eea59c03e" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read; pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A36E8:5B22E4:69AF24E7 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4933 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 67 +X-Xss-Protection: 0 + +[{"id":1667254252,"node_id":"MDU6TGFiZWwxNjY3MjU0MjUy","url":"https://api.github.com/repos/go-gitea/test_repo/labels/bug","name":"bug","color":"d73a4a","default":true,"description":"Something isn't working"},{"id":1667254254,"node_id":"MDU6TGFiZWwxNjY3MjU0MjU0","url":"https://api.github.com/repos/go-gitea/test_repo/labels/documentation","name":"documentation","color":"0075ca","default":true,"description":"Improvements or additions to documentation"},{"id":1667254257,"node_id":"MDU6TGFiZWwxNjY3MjU0MjU3","url":"https://api.github.com/repos/go-gitea/test_repo/labels/duplicate","name":"duplicate","color":"cfd3d7","default":true,"description":"This issue or pull request already exists"},{"id":1667254260,"node_id":"MDU6TGFiZWwxNjY3MjU0MjYw","url":"https://api.github.com/repos/go-gitea/test_repo/labels/enhancement","name":"enhancement","color":"a2eeef","default":true,"description":"New feature or request"},{"id":1667254261,"node_id":"MDU6TGFiZWwxNjY3MjU0MjYx","url":"https://api.github.com/repos/go-gitea/test_repo/labels/good%20first%20issue","name":"good first issue","color":"7057ff","default":true,"description":"Good for newcomers"},{"id":1667254265,"node_id":"MDU6TGFiZWwxNjY3MjU0MjY1","url":"https://api.github.com/repos/go-gitea/test_repo/labels/help%20wanted","name":"help wanted","color":"008672","default":true,"description":"Extra attention is needed"},{"id":1667254269,"node_id":"MDU6TGFiZWwxNjY3MjU0MjY5","url":"https://api.github.com/repos/go-gitea/test_repo/labels/invalid","name":"invalid","color":"e4e669","default":true,"description":"This doesn't seem right"},{"id":1667254273,"node_id":"MDU6TGFiZWwxNjY3MjU0Mjcz","url":"https://api.github.com/repos/go-gitea/test_repo/labels/question","name":"question","color":"d876e3","default":true,"description":"Further information is requested"},{"id":1667254276,"node_id":"MDU6TGFiZWwxNjY3MjU0Mjc2","url":"https://api.github.com/repos/go-gitea/test_repo/labels/wontfix","name":"wontfix","color":"ffffff","default":true,"description":"This will not be worked on"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fmilestones%3Fpage%3D1%26per_page%3D100%26state%3Dall b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fmilestones%3Fpage%3D1%26per_page%3D100%26state%3Dall new file mode 100644 index 00000000000..0eda560b560 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fmilestones%3Fpage%3D1%26per_page%3D100%26state%3Dall @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"c2c4c54255928bb9bebc8c65c989b904b06ffb4eb763f804d70dbb40473d3122" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: issues=read; pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A356B:5B2172:69AF24E7 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4934 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 66 +X-Xss-Protection: 0 + +[{"url":"https://api.github.com/repos/go-gitea/test_repo/milestones/1","html_url":"https://github.com/go-gitea/test_repo/milestone/1","labels_url":"https://api.github.com/repos/go-gitea/test_repo/milestones/1/labels","id":4839941,"node_id":"MDk6TWlsZXN0b25lNDgzOTk0MQ==","number":1,"title":"1.0.0","description":"Milestone 1.0.0","creator":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":1,"closed_issues":1,"state":"closed","created_at":"2019-11-12T19:37:08Z","updated_at":"2019-11-12T21:56:17Z","due_on":"2019-11-11T00:00:00Z","closed_at":"2019-11-12T19:45:49Z"},{"url":"https://api.github.com/repos/go-gitea/test_repo/milestones/2","html_url":"https://github.com/go-gitea/test_repo/milestone/2","labels_url":"https://api.github.com/repos/go-gitea/test_repo/milestones/2/labels","id":4839942,"node_id":"MDk6TWlsZXN0b25lNDgzOTk0Mg==","number":2,"title":"1.1.0","description":"Milestone 1.1.0","creator":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":0,"closed_issues":2,"state":"closed","created_at":"2019-11-12T19:37:25Z","updated_at":"2019-11-12T21:39:27Z","due_on":"2019-11-12T00:00:00Z","closed_at":"2019-11-12T19:45:46Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers new file mode 100644 index 00000000000..ecf6a3a5331 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"db6d3f5496df397024cfe34bfce143f4940ddf0e169ffcb7c24343afcd0ce6b1" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A628A:5B48F4:69AF24EE +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4912 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 88 +X-Xss-Protection: 0 + +{"users":[],"teams":[]} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315859956%2Fcomments%3Fper_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315859956%2Fcomments%3Fper_page%3D100 new file mode 100644 index 00000000000..60c33488ec4 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315859956%2Fcomments%3Fper_page%3D100 @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: "7b5b784c0a881ecba5ae21871c054e30f6d6fef4c73d12a917530925ea2b258a" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A5D81:5B4466:69AF24ED +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4915 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 85 +X-Xss-Protection: 0 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315860062%2Fcomments%3Fper_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315860062%2Fcomments%3Fper_page%3D100 new file mode 100644 index 00000000000..190df1eac92 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315860062%2Fcomments%3Fper_page%3D100 @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: "7b5b784c0a881ecba5ae21871c054e30f6d6fef4c73d12a917530925ea2b258a" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A5F1B:5B45DB:69AF24ED +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4914 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 86 +X-Xss-Protection: 0 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315861440%2Fcomments%3Fper_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315861440%2Fcomments%3Fper_page%3D100 new file mode 100644 index 00000000000..7fd19617147 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315861440%2Fcomments%3Fper_page%3D100 @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: "7b5b784c0a881ecba5ae21871c054e30f6d6fef4c73d12a917530925ea2b258a" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A60C3:5B4760:69AF24ED +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4913 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 87 +X-Xss-Protection: 0 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%3Fper_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%3Fper_page%3D100 new file mode 100644 index 00000000000..6ff0bf9ce77 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%3Fper_page%3D100 @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"3f634d6cfe0d88ce43a5d40e74ca0d79ec9c242bee77582ad8d889491a72fefc" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A5A92:5B41D6:69AF24EC +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4916 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 84 +X-Xss-Protection: 0 + +[{"id":315859956,"node_id":"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3MzE1ODU5OTU2","user":{"login":"jolheiser","id":42128690,"node_id":"MDQ6VXNlcjQyMTI4Njkw","avatar_url":"https://avatars.githubusercontent.com/u/42128690?u=0ee1052506846129445fa12a76cd9ad9d305de71&v=4","gravatar_id":"","url":"https://api.github.com/users/jolheiser","html_url":"https://github.com/jolheiser","followers_url":"https://api.github.com/users/jolheiser/followers","following_url":"https://api.github.com/users/jolheiser/following{/other_user}","gists_url":"https://api.github.com/users/jolheiser/gists{/gist_id}","starred_url":"https://api.github.com/users/jolheiser/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jolheiser/subscriptions","organizations_url":"https://api.github.com/users/jolheiser/orgs","repos_url":"https://api.github.com/users/jolheiser/repos","events_url":"https://api.github.com/users/jolheiser/events{/privacy}","received_events_url":"https://api.github.com/users/jolheiser/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"","state":"APPROVED","html_url":"https://github.com/go-gitea/test_repo/pull/3#pullrequestreview-315859956","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/3","author_association":"MEMBER","_links":{"html":{"href":"https://github.com/go-gitea/test_repo/pull/3#pullrequestreview-315859956"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/3"}},"submitted_at":"2019-11-12T21:35:24Z","commit_id":"076160cf0b039f13e5eff19619932d181269414b"},{"id":315860062,"node_id":"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3MzE1ODYwMDYy","user":{"login":"zeripath","id":1824502,"node_id":"MDQ6VXNlcjE4MjQ1MDI=","avatar_url":"https://avatars.githubusercontent.com/u/1824502?u=fcd8a9dba8714edf6ac3f87596eb72149911c720&v=4","gravatar_id":"","url":"https://api.github.com/users/zeripath","html_url":"https://github.com/zeripath","followers_url":"https://api.github.com/users/zeripath/followers","following_url":"https://api.github.com/users/zeripath/following{/other_user}","gists_url":"https://api.github.com/users/zeripath/gists{/gist_id}","starred_url":"https://api.github.com/users/zeripath/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zeripath/subscriptions","organizations_url":"https://api.github.com/users/zeripath/orgs","repos_url":"https://api.github.com/users/zeripath/repos","events_url":"https://api.github.com/users/zeripath/events{/privacy}","received_events_url":"https://api.github.com/users/zeripath/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"","state":"APPROVED","html_url":"https://github.com/go-gitea/test_repo/pull/3#pullrequestreview-315860062","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/3","author_association":"MEMBER","_links":{"html":{"href":"https://github.com/go-gitea/test_repo/pull/3#pullrequestreview-315860062"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/3"}},"submitted_at":"2019-11-12T21:35:36Z","commit_id":"076160cf0b039f13e5eff19619932d181269414b"},{"id":315861440,"node_id":"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3MzE1ODYxNDQw","user":{"login":"lafriks","id":165205,"node_id":"MDQ6VXNlcjE2NTIwNQ==","avatar_url":"https://avatars.githubusercontent.com/u/165205?u=efe2335d2197f524c25caa7abdfcb90b77eb8d98&v=4","gravatar_id":"","url":"https://api.github.com/users/lafriks","html_url":"https://github.com/lafriks","followers_url":"https://api.github.com/users/lafriks/followers","following_url":"https://api.github.com/users/lafriks/following{/other_user}","gists_url":"https://api.github.com/users/lafriks/gists{/gist_id}","starred_url":"https://api.github.com/users/lafriks/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lafriks/subscriptions","organizations_url":"https://api.github.com/users/lafriks/orgs","repos_url":"https://api.github.com/users/lafriks/repos","events_url":"https://api.github.com/users/lafriks/events{/privacy}","received_events_url":"https://api.github.com/users/lafriks/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"","state":"APPROVED","html_url":"https://github.com/go-gitea/test_repo/pull/3#pullrequestreview-315861440","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/3","author_association":"MEMBER","_links":{"html":{"href":"https://github.com/go-gitea/test_repo/pull/3#pullrequestreview-315861440"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/3"}},"submitted_at":"2019-11-12T21:38:00Z","commit_id":"076160cf0b039f13e5eff19619932d181269414b"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Frequested_reviewers b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Frequested_reviewers new file mode 100644 index 00000000000..d6b38603cb7 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Frequested_reviewers @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"db6d3f5496df397024cfe34bfce143f4940ddf0e169ffcb7c24343afcd0ce6b1" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A7048:5B54F3:69AF24F0 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4905 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 95 +X-Xss-Protection: 0 + +{"users":[],"teams":[]} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338338740%2Fcomments%3Fper_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338338740%2Fcomments%3Fper_page%3D100 new file mode 100644 index 00000000000..d6554c42e99 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338338740%2Fcomments%3Fper_page%3D100 @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"8f714d80f3d155fff562fefb57d0e91261d5647dfc0e5d2f68f5567937814326" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A6731:5B4CF6:69AF24EE +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4910 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 90 +X-Xss-Protection: 0 + +[{"id":363017488,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDM2MzAxNzQ4OA==","url":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments/363017488","pull_request_review_id":338338740,"diff_hunk":"@@ -1,2 +1,4 @@\n # test_repo\n Test repository for testing migration from github to gitea\n+","path":"README.md","position":3,"original_position":3,"commit_id":"2be9101c543658591222acbee3eb799edfc3853d","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"This is a good pull request.","created_at":"2020-01-04T05:33:06Z","updated_at":"2020-01-04T05:33:18Z","html_url":"https://github.com/go-gitea/test_repo/pull/4#discussion_r363017488","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments/363017488"},"html":{"href":"https://github.com/go-gitea/test_repo/pull/4#discussion_r363017488"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4"}},"original_commit_id":"2be9101c543658591222acbee3eb799edfc3853d","reactions":{"url":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments/363017488/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0}}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338339651%2Fcomments%3Fper_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338339651%2Fcomments%3Fper_page%3D100 new file mode 100644 index 00000000000..27d366192b2 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338339651%2Fcomments%3Fper_page%3D100 @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: "7b5b784c0a881ecba5ae21871c054e30f6d6fef4c73d12a917530925ea2b258a" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A6B06:5B5056:69AF24EF +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4908 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 92 +X-Xss-Protection: 0 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338349019%2Fcomments%3Fper_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338349019%2Fcomments%3Fper_page%3D100 new file mode 100644 index 00000000000..86af31a7552 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338349019%2Fcomments%3Fper_page%3D100 @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"69b86138edd30116a19b2236faee0e90afecfda0f12903fb6f38e180a06d1daf" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A6C8F:5B5195:69AF24EF +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4907 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 93 +X-Xss-Protection: 0 + +[{"id":363029944,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDM2MzAyOTk0NA==","url":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments/363029944","pull_request_review_id":338349019,"diff_hunk":"@@ -19,3 +19,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n SOFTWARE.\n+","path":"LICENSE","position":4,"original_position":4,"commit_id":"2be9101c543658591222acbee3eb799edfc3853d","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"test a single comment.","created_at":"2020-01-04T11:21:41Z","updated_at":"2020-01-04T11:21:41Z","html_url":"https://github.com/go-gitea/test_repo/pull/4#discussion_r363029944","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments/363029944"},"html":{"href":"https://github.com/go-gitea/test_repo/pull/4#discussion_r363029944"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4"}},"original_commit_id":"2be9101c543658591222acbee3eb799edfc3853d","reactions":{"url":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments/363029944/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0}}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page%3D100 new file mode 100644 index 00000000000..c751d619841 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page%3D100 @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"35421809939e0816567e6576339fcf4854de7ec5978b09c6b00ed708e0ba3fe0" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A6442:5B4A5B:69AF24EE +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4911 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 89 +X-Xss-Protection: 0 + +[{"id":338338740,"node_id":"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3MzM4MzM4NzQw","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?u=99b64f0ca6ef63643c7583ab87dd31c52d28e673&v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"","state":"APPROVED","html_url":"https://github.com/go-gitea/test_repo/pull/4#pullrequestreview-338338740","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4","author_association":"MEMBER","_links":{"html":{"href":"https://github.com/go-gitea/test_repo/pull/4#pullrequestreview-338338740"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4"}},"submitted_at":"2020-01-04T05:33:18Z","commit_id":"2be9101c543658591222acbee3eb799edfc3853d"},{"id":338339651,"node_id":"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3MzM4MzM5NjUx","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?u=99b64f0ca6ef63643c7583ab87dd31c52d28e673&v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"Don't add more reviews","state":"CHANGES_REQUESTED","html_url":"https://github.com/go-gitea/test_repo/pull/4#pullrequestreview-338339651","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4","author_association":"MEMBER","_links":{"html":{"href":"https://github.com/go-gitea/test_repo/pull/4#pullrequestreview-338339651"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4"}},"submitted_at":"2020-01-04T06:07:06Z","commit_id":"2be9101c543658591222acbee3eb799edfc3853d"},{"id":338349019,"node_id":"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3MzM4MzQ5MDE5","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?u=99b64f0ca6ef63643c7583ab87dd31c52d28e673&v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"","state":"COMMENTED","html_url":"https://github.com/go-gitea/test_repo/pull/4#pullrequestreview-338349019","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4","author_association":"MEMBER","_links":{"html":{"href":"https://github.com/go-gitea/test_repo/pull/4#pullrequestreview-338349019"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4"}},"submitted_at":"2020-01-04T11:21:41Z","commit_id":"2be9101c543658591222acbee3eb799edfc3853d"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363017488%2Freactions%3Fpage%3D1%26per_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363017488%2Freactions%3Fpage%3D1%26per_page%3D100 new file mode 100644 index 00000000000..6c6ab19d115 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363017488%2Freactions%3Fpage%3D1%26per_page%3D100 @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: "440897bed2748f095d024d64264317f3f31eadc9413bf26dc0450eecbfbbef7b" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A6956:5B4EEB:69AF24EF +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4909 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 91 +X-Xss-Protection: 0 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363029944%2Freactions%3Fpage%3D1%26per_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363029944%2Freactions%3Fpage%3D1%26per_page%3D100 new file mode 100644 index 00000000000..5a1ba878305 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363029944%2Freactions%3Fpage%3D1%26per_page%3D100 @@ -0,0 +1,24 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: "440897bed2748f095d024d64264317f3f31eadc9413bf26dc0450eecbfbbef7b" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; param=squirrel-girl-preview +X-Github-Request-Id: C4F6:A93E1:6A6EC5:5B536E:69AF24EF +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4906 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 94 +X-Xss-Protection: 0 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%3Fdirection%3Dasc%26page%3D1%26per_page%3D2%26sort%3Dcreated%26state%3Dall b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%3Fdirection%3Dasc%26page%3D1%26per_page%3D2%26sort%3Dcreated%26state%3Dall new file mode 100644 index 00000000000..c67a36d1028 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%3Fdirection%3Dasc%26page%3D1%26per_page%3D2%26sort%3Dcreated%26state%3Dall @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"5d8135a4b8204fb8548ef5b2153ecdecb0d676e2907ecd115101ba728a27c4d2" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: pull_requests=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A4FA3:5B384B:69AF24EB +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4920 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 80 +X-Xss-Protection: 0 + +[{"url":"https://api.github.com/repos/go-gitea/test_repo/pulls/3","id":340118745,"node_id":"MDExOlB1bGxSZXF1ZXN0MzQwMTE4NzQ1","html_url":"https://github.com/go-gitea/test_repo/pull/3","diff_url":"https://github.com/go-gitea/test_repo/pull/3.diff","patch_url":"https://github.com/go-gitea/test_repo/pull/3.patch","issue_url":"https://api.github.com/repos/go-gitea/test_repo/issues/3","number":3,"state":"closed","locked":false,"title":"Update README.md","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"add warning to readme","created_at":"2019-11-12T21:21:43Z","updated_at":"2019-11-12T21:39:28Z","closed_at":"2019-11-12T21:39:27Z","merged_at":"2019-11-12T21:39:27Z","merge_commit_sha":"f32b0a9dfd09a60f616f29158f772cedd89942d2","assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":1667254254,"node_id":"MDU6TGFiZWwxNjY3MjU0MjU0","url":"https://api.github.com/repos/go-gitea/test_repo/labels/documentation","name":"documentation","color":"0075ca","default":true,"description":"Improvements or additions to documentation"}],"milestone":{"url":"https://api.github.com/repos/go-gitea/test_repo/milestones/2","html_url":"https://github.com/go-gitea/test_repo/milestone/2","labels_url":"https://api.github.com/repos/go-gitea/test_repo/milestones/2/labels","id":4839942,"node_id":"MDk6TWlsZXN0b25lNDgzOTk0Mg==","number":2,"title":"1.1.0","description":"Milestone 1.1.0","creator":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":0,"closed_issues":2,"state":"closed","created_at":"2019-11-12T19:37:25Z","updated_at":"2019-11-12T21:39:27Z","due_on":"2019-11-12T00:00:00Z","closed_at":"2019-11-12T19:45:46Z"},"draft":false,"commits_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/3/commits","review_comments_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/3/comments","review_comment_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments{/number}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/issues/3/comments","statuses_url":"https://api.github.com/repos/go-gitea/test_repo/statuses/076160cf0b039f13e5eff19619932d181269414b","head":{"label":"mrsdizzie:master","ref":"master","sha":"076160cf0b039f13e5eff19619932d181269414b","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"repo":{"id":221313794,"node_id":"MDEwOlJlcG9zaXRvcnkyMjEzMTM3OTQ=","name":"test_repo","full_name":"mrsdizzie/test_repo","private":false,"owner":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/mrsdizzie/test_repo","description":"Test repository for testing migration from github to gitea","fork":true,"url":"https://api.github.com/repos/mrsdizzie/test_repo","forks_url":"https://api.github.com/repos/mrsdizzie/test_repo/forks","keys_url":"https://api.github.com/repos/mrsdizzie/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mrsdizzie/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mrsdizzie/test_repo/teams","hooks_url":"https://api.github.com/repos/mrsdizzie/test_repo/hooks","issue_events_url":"https://api.github.com/repos/mrsdizzie/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/mrsdizzie/test_repo/events","assignees_url":"https://api.github.com/repos/mrsdizzie/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/mrsdizzie/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/mrsdizzie/test_repo/tags","blobs_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mrsdizzie/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/mrsdizzie/test_repo/languages","stargazers_url":"https://api.github.com/repos/mrsdizzie/test_repo/stargazers","contributors_url":"https://api.github.com/repos/mrsdizzie/test_repo/contributors","subscribers_url":"https://api.github.com/repos/mrsdizzie/test_repo/subscribers","subscription_url":"https://api.github.com/repos/mrsdizzie/test_repo/subscription","commits_url":"https://api.github.com/repos/mrsdizzie/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/mrsdizzie/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/mrsdizzie/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/mrsdizzie/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/mrsdizzie/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mrsdizzie/test_repo/merges","archive_url":"https://api.github.com/repos/mrsdizzie/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mrsdizzie/test_repo/downloads","issues_url":"https://api.github.com/repos/mrsdizzie/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/mrsdizzie/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/mrsdizzie/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/mrsdizzie/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mrsdizzie/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/mrsdizzie/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/mrsdizzie/test_repo/deployments","created_at":"2019-11-12T21:17:42Z","updated_at":"2019-11-12T21:18:46Z","pushed_at":"2019-11-12T21:53:39Z","git_url":"git://github.com/mrsdizzie/test_repo.git","ssh_url":"git@github.com:mrsdizzie/test_repo.git","clone_url":"https://github.com/mrsdizzie/test_repo.git","svn_url":"https://github.com/mrsdizzie/test_repo","homepage":null,"size":3,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"go-gitea:master","ref":"master","sha":"72866af952e98d02a73003501836074b286a78f6","user":{"login":"go-gitea","id":12724356,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2","avatar_url":"https://avatars.githubusercontent.com/u/12724356?v=4","gravatar_id":"","url":"https://api.github.com/users/go-gitea","html_url":"https://github.com/go-gitea","followers_url":"https://api.github.com/users/go-gitea/followers","following_url":"https://api.github.com/users/go-gitea/following{/other_user}","gists_url":"https://api.github.com/users/go-gitea/gists{/gist_id}","starred_url":"https://api.github.com/users/go-gitea/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/go-gitea/subscriptions","organizations_url":"https://api.github.com/users/go-gitea/orgs","repos_url":"https://api.github.com/users/go-gitea/repos","events_url":"https://api.github.com/users/go-gitea/events{/privacy}","received_events_url":"https://api.github.com/users/go-gitea/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":220672974,"node_id":"MDEwOlJlcG9zaXRvcnkyMjA2NzI5NzQ=","name":"test_repo","full_name":"go-gitea/test_repo","private":false,"owner":{"login":"go-gitea","id":12724356,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2","avatar_url":"https://avatars.githubusercontent.com/u/12724356?v=4","gravatar_id":"","url":"https://api.github.com/users/go-gitea","html_url":"https://github.com/go-gitea","followers_url":"https://api.github.com/users/go-gitea/followers","following_url":"https://api.github.com/users/go-gitea/following{/other_user}","gists_url":"https://api.github.com/users/go-gitea/gists{/gist_id}","starred_url":"https://api.github.com/users/go-gitea/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/go-gitea/subscriptions","organizations_url":"https://api.github.com/users/go-gitea/orgs","repos_url":"https://api.github.com/users/go-gitea/repos","events_url":"https://api.github.com/users/go-gitea/events{/privacy}","received_events_url":"https://api.github.com/users/go-gitea/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/go-gitea/test_repo","description":"Test repository for testing migration from github to gitea","fork":false,"url":"https://api.github.com/repos/go-gitea/test_repo","forks_url":"https://api.github.com/repos/go-gitea/test_repo/forks","keys_url":"https://api.github.com/repos/go-gitea/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/go-gitea/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/go-gitea/test_repo/teams","hooks_url":"https://api.github.com/repos/go-gitea/test_repo/hooks","issue_events_url":"https://api.github.com/repos/go-gitea/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/go-gitea/test_repo/events","assignees_url":"https://api.github.com/repos/go-gitea/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/go-gitea/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/go-gitea/test_repo/tags","blobs_url":"https://api.github.com/repos/go-gitea/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/go-gitea/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/go-gitea/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/go-gitea/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/go-gitea/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/go-gitea/test_repo/languages","stargazers_url":"https://api.github.com/repos/go-gitea/test_repo/stargazers","contributors_url":"https://api.github.com/repos/go-gitea/test_repo/contributors","subscribers_url":"https://api.github.com/repos/go-gitea/test_repo/subscribers","subscription_url":"https://api.github.com/repos/go-gitea/test_repo/subscription","commits_url":"https://api.github.com/repos/go-gitea/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/go-gitea/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/go-gitea/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/go-gitea/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/go-gitea/test_repo/merges","archive_url":"https://api.github.com/repos/go-gitea/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/go-gitea/test_repo/downloads","issues_url":"https://api.github.com/repos/go-gitea/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/go-gitea/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/go-gitea/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/go-gitea/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/go-gitea/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/go-gitea/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/go-gitea/test_repo/deployments","created_at":"2019-11-09T16:49:20Z","updated_at":"2023-03-02T14:02:26Z","pushed_at":"2019-11-12T21:54:19Z","git_url":"git://github.com/go-gitea/test_repo.git","ssh_url":"git@github.com:go-gitea/test_repo.git","clone_url":"https://github.com/go-gitea/test_repo.git","svn_url":"https://github.com/go-gitea/test_repo","homepage":null,"size":1,"stargazers_count":3,"watchers_count":3,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":3,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":["gitea"],"visibility":"public","forks":3,"open_issues":3,"watchers":3,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/3"},"html":{"href":"https://github.com/go-gitea/test_repo/pull/3"},"issue":{"href":"https://api.github.com/repos/go-gitea/test_repo/issues/3"},"comments":{"href":"https://api.github.com/repos/go-gitea/test_repo/issues/3/comments"},"review_comments":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/3/comments"},"review_comment":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/3/commits"},"statuses":{"href":"https://api.github.com/repos/go-gitea/test_repo/statuses/076160cf0b039f13e5eff19619932d181269414b"}},"author_association":"MEMBER","auto_merge":null,"assignee":null,"active_lock_reason":null},{"url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4","id":340131577,"node_id":"MDExOlB1bGxSZXF1ZXN0MzQwMTMxNTc3","html_url":"https://github.com/go-gitea/test_repo/pull/4","diff_url":"https://github.com/go-gitea/test_repo/pull/4.diff","patch_url":"https://github.com/go-gitea/test_repo/pull/4.patch","issue_url":"https://api.github.com/repos/go-gitea/test_repo/issues/4","number":4,"state":"open","locked":false,"title":"Test branch","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"do not merge this PR","created_at":"2019-11-12T21:54:18Z","updated_at":"2025-03-16T15:46:20Z","closed_at":null,"merged_at":null,"merge_commit_sha":"565d1208f5fffdc1c5ae1a2436491eb9a5e4ebae","assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":1667254252,"node_id":"MDU6TGFiZWwxNjY3MjU0MjUy","url":"https://api.github.com/repos/go-gitea/test_repo/labels/bug","name":"bug","color":"d73a4a","default":true,"description":"Something isn't working"}],"milestone":{"url":"https://api.github.com/repos/go-gitea/test_repo/milestones/1","html_url":"https://github.com/go-gitea/test_repo/milestone/1","labels_url":"https://api.github.com/repos/go-gitea/test_repo/milestones/1/labels","id":4839941,"node_id":"MDk6TWlsZXN0b25lNDgzOTk0MQ==","number":1,"title":"1.0.0","description":"Milestone 1.0.0","creator":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":1,"closed_issues":1,"state":"closed","created_at":"2019-11-12T19:37:08Z","updated_at":"2019-11-12T21:56:17Z","due_on":"2019-11-11T00:00:00Z","closed_at":"2019-11-12T19:45:49Z"},"draft":false,"commits_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4/commits","review_comments_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4/comments","review_comment_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments{/number}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/issues/4/comments","statuses_url":"https://api.github.com/repos/go-gitea/test_repo/statuses/2be9101c543658591222acbee3eb799edfc3853d","head":{"label":"mrsdizzie:test-branch","ref":"test-branch","sha":"2be9101c543658591222acbee3eb799edfc3853d","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"repo":{"id":221313794,"node_id":"MDEwOlJlcG9zaXRvcnkyMjEzMTM3OTQ=","name":"test_repo","full_name":"mrsdizzie/test_repo","private":false,"owner":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/mrsdizzie/test_repo","description":"Test repository for testing migration from github to gitea","fork":true,"url":"https://api.github.com/repos/mrsdizzie/test_repo","forks_url":"https://api.github.com/repos/mrsdizzie/test_repo/forks","keys_url":"https://api.github.com/repos/mrsdizzie/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mrsdizzie/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mrsdizzie/test_repo/teams","hooks_url":"https://api.github.com/repos/mrsdizzie/test_repo/hooks","issue_events_url":"https://api.github.com/repos/mrsdizzie/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/mrsdizzie/test_repo/events","assignees_url":"https://api.github.com/repos/mrsdizzie/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/mrsdizzie/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/mrsdizzie/test_repo/tags","blobs_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mrsdizzie/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/mrsdizzie/test_repo/languages","stargazers_url":"https://api.github.com/repos/mrsdizzie/test_repo/stargazers","contributors_url":"https://api.github.com/repos/mrsdizzie/test_repo/contributors","subscribers_url":"https://api.github.com/repos/mrsdizzie/test_repo/subscribers","subscription_url":"https://api.github.com/repos/mrsdizzie/test_repo/subscription","commits_url":"https://api.github.com/repos/mrsdizzie/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/mrsdizzie/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/mrsdizzie/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/mrsdizzie/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/mrsdizzie/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mrsdizzie/test_repo/merges","archive_url":"https://api.github.com/repos/mrsdizzie/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mrsdizzie/test_repo/downloads","issues_url":"https://api.github.com/repos/mrsdizzie/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/mrsdizzie/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/mrsdizzie/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/mrsdizzie/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mrsdizzie/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/mrsdizzie/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/mrsdizzie/test_repo/deployments","created_at":"2019-11-12T21:17:42Z","updated_at":"2019-11-12T21:18:46Z","pushed_at":"2019-11-12T21:53:39Z","git_url":"git://github.com/mrsdizzie/test_repo.git","ssh_url":"git@github.com:mrsdizzie/test_repo.git","clone_url":"https://github.com/mrsdizzie/test_repo.git","svn_url":"https://github.com/mrsdizzie/test_repo","homepage":null,"size":3,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"go-gitea:master","ref":"master","sha":"f32b0a9dfd09a60f616f29158f772cedd89942d2","user":{"login":"go-gitea","id":12724356,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2","avatar_url":"https://avatars.githubusercontent.com/u/12724356?v=4","gravatar_id":"","url":"https://api.github.com/users/go-gitea","html_url":"https://github.com/go-gitea","followers_url":"https://api.github.com/users/go-gitea/followers","following_url":"https://api.github.com/users/go-gitea/following{/other_user}","gists_url":"https://api.github.com/users/go-gitea/gists{/gist_id}","starred_url":"https://api.github.com/users/go-gitea/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/go-gitea/subscriptions","organizations_url":"https://api.github.com/users/go-gitea/orgs","repos_url":"https://api.github.com/users/go-gitea/repos","events_url":"https://api.github.com/users/go-gitea/events{/privacy}","received_events_url":"https://api.github.com/users/go-gitea/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":220672974,"node_id":"MDEwOlJlcG9zaXRvcnkyMjA2NzI5NzQ=","name":"test_repo","full_name":"go-gitea/test_repo","private":false,"owner":{"login":"go-gitea","id":12724356,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2","avatar_url":"https://avatars.githubusercontent.com/u/12724356?v=4","gravatar_id":"","url":"https://api.github.com/users/go-gitea","html_url":"https://github.com/go-gitea","followers_url":"https://api.github.com/users/go-gitea/followers","following_url":"https://api.github.com/users/go-gitea/following{/other_user}","gists_url":"https://api.github.com/users/go-gitea/gists{/gist_id}","starred_url":"https://api.github.com/users/go-gitea/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/go-gitea/subscriptions","organizations_url":"https://api.github.com/users/go-gitea/orgs","repos_url":"https://api.github.com/users/go-gitea/repos","events_url":"https://api.github.com/users/go-gitea/events{/privacy}","received_events_url":"https://api.github.com/users/go-gitea/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/go-gitea/test_repo","description":"Test repository for testing migration from github to gitea","fork":false,"url":"https://api.github.com/repos/go-gitea/test_repo","forks_url":"https://api.github.com/repos/go-gitea/test_repo/forks","keys_url":"https://api.github.com/repos/go-gitea/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/go-gitea/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/go-gitea/test_repo/teams","hooks_url":"https://api.github.com/repos/go-gitea/test_repo/hooks","issue_events_url":"https://api.github.com/repos/go-gitea/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/go-gitea/test_repo/events","assignees_url":"https://api.github.com/repos/go-gitea/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/go-gitea/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/go-gitea/test_repo/tags","blobs_url":"https://api.github.com/repos/go-gitea/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/go-gitea/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/go-gitea/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/go-gitea/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/go-gitea/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/go-gitea/test_repo/languages","stargazers_url":"https://api.github.com/repos/go-gitea/test_repo/stargazers","contributors_url":"https://api.github.com/repos/go-gitea/test_repo/contributors","subscribers_url":"https://api.github.com/repos/go-gitea/test_repo/subscribers","subscription_url":"https://api.github.com/repos/go-gitea/test_repo/subscription","commits_url":"https://api.github.com/repos/go-gitea/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/go-gitea/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/go-gitea/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/go-gitea/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/go-gitea/test_repo/merges","archive_url":"https://api.github.com/repos/go-gitea/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/go-gitea/test_repo/downloads","issues_url":"https://api.github.com/repos/go-gitea/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/go-gitea/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/go-gitea/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/go-gitea/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/go-gitea/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/go-gitea/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/go-gitea/test_repo/deployments","created_at":"2019-11-09T16:49:20Z","updated_at":"2023-03-02T14:02:26Z","pushed_at":"2019-11-12T21:54:19Z","git_url":"git://github.com/go-gitea/test_repo.git","ssh_url":"git@github.com:go-gitea/test_repo.git","clone_url":"https://github.com/go-gitea/test_repo.git","svn_url":"https://github.com/go-gitea/test_repo","homepage":null,"size":1,"stargazers_count":3,"watchers_count":3,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":3,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":["gitea"],"visibility":"public","forks":3,"open_issues":3,"watchers":3,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4"},"html":{"href":"https://github.com/go-gitea/test_repo/pull/4"},"issue":{"href":"https://api.github.com/repos/go-gitea/test_repo/issues/4"},"comments":{"href":"https://api.github.com/repos/go-gitea/test_repo/issues/4/comments"},"review_comments":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4/comments"},"review_comment":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4/commits"},"statuses":{"href":"https://api.github.com/repos/go-gitea/test_repo/statuses/2be9101c543658591222acbee3eb799edfc3853d"}},"author_association":"MEMBER","auto_merge":null,"assignee":null,"active_lock_reason":null}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Freleases%3Fpage%3D1%26per_page%3D100 b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Freleases%3Fpage%3D1%26per_page%3D100 new file mode 100644 index 00000000000..7ec652e41b4 --- /dev/null +++ b/services/migrations/_mock_data/TestGitHubDownloadRepo/GET_%2Fapi%2Fv3%2Frepos%2Fgo-gitea%2Ftest_repo%2Freleases%3Fpage%3D1%26per_page%3D100 @@ -0,0 +1,23 @@ +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset +Cache-Control: private, max-age=60, s-maxage=60 +Content-Security-Policy: default-src 'none' +Content-Type: application/json; charset=utf-8 +Etag: W/"931091ce17d88742881c4964d7ec028088be47e87af3f507a83795eaf54056ac" +Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000; includeSubdomains; preload +Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With +X-Accepted-Github-Permissions: contents=read +X-Content-Type-Options: nosniff +X-Frame-Options: deny +X-Github-Api-Version-Selected: 2022-11-28 +X-Github-Media-Type: github.v3; format=json +X-Github-Request-Id: C4F6:A93E1:6A38A8:5B2462:69AF24E7 +X-Ratelimit-Limit: 5000 +X-Ratelimit-Remaining: 4932 +X-Ratelimit-Reset: 1773089427 +X-Ratelimit-Resource: core +X-Ratelimit-Used: 68 +X-Xss-Protection: 0 + +[{"url":"https://api.github.com/repos/go-gitea/test_repo/releases/21419432","assets_url":"https://api.github.com/repos/go-gitea/test_repo/releases/21419432/assets","upload_url":"https://uploads.github.com/repos/go-gitea/test_repo/releases/21419432/assets{?name,label}","html_url":"https://github.com/go-gitea/test_repo/releases/tag/v0.9.99","id":21419432,"author":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"node_id":"MDc6UmVsZWFzZTIxNDE5NDMy","tag_name":"v0.9.99","target_commitish":"master","name":"First Release","draft":false,"immutable":false,"prerelease":false,"created_at":"2019-11-09T16:49:21Z","updated_at":"2019-11-12T20:12:10Z","published_at":"2019-11-12T20:12:10Z","assets":[],"tarball_url":"https://api.github.com/repos/go-gitea/test_repo/tarball/v0.9.99","zipball_url":"https://api.github.com/repos/go-gitea/test_repo/zipball/v0.9.99","body":"A test release"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo new file mode 100644 index 00000000000..6f40be01f7e --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo @@ -0,0 +1,7 @@ +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff + +{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..64b2192d67a --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 1322 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 2 + +[{"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"content":"gitea","created_at":"2020-09-01T00:15:14Z"},{"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"content":"confused","created_at":"2020-09-01T00:15:19Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F10%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F10%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..da0620892fb --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F10%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 4 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 0 + +null \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F11%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F11%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..da0620892fb --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F11%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 4 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 0 + +null \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F12%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F12%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..da0620892fb --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F12%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 4 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 0 + +null \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F13%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F13%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..da0620892fb --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F13%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 4 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 0 + +null \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..da0620892fb --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 4 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 0 + +null \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F3%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F3%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..da0620892fb --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F3%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 4 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 0 + +null \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F4%2Fcomments%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F4%2Fcomments%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..365bd500d28 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F4%2Fcomments%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 1834 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 2 + +[{"id":116550,"html_url":"https://gitea.com/gitea/test_repo/issues/4#issuecomment-116550","pull_request_url":"","issue_url":"https://gitea.com/gitea/test_repo/issues/4","user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"original_author":"","original_author_id":0,"body":"a really good question!\n\nIt is the used as TESTSET for gitea2gitea repo migration function","assets":[],"created_at":"2020-09-01T15:49:30Z","updated_at":"2020-09-02T18:21:05Z"},{"id":116552,"html_url":"https://gitea.com/gitea/test_repo/issues/4#issuecomment-116552","pull_request_url":"","issue_url":"https://gitea.com/gitea/test_repo/issues/4","user":{"id":-1,"login":"Ghost","login_name":"","source_id":0,"full_name":"","email":"-1+ghost@noreply.gitea.com","avatar_url":"https://gitea.com/assets/img/avatar_default.png","html_url":"https://gitea.com/Ghost","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"1970-01-01T00:00:00Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"Ghost"},"original_author":"","original_author_id":0,"body":"Oh!","assets":[],"created_at":"2020-09-01T15:49:53Z","updated_at":"2020-09-01T15:49:53Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..802314b5703 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 1319 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 2 + +[{"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"content":"gitea","created_at":"2020-09-01T19:36:40Z"},{"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"content":"laugh","created_at":"2020-09-01T19:36:45Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F5%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F5%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..da1a14de1ae --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F5%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 1317 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 2 + +[{"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"content":"+1","created_at":"2020-09-01T16:07:06Z"},{"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"content":"hooray","created_at":"2020-09-01T16:07:11Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F6%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F6%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..da0620892fb --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F6%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 4 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 0 + +null \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F7%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F7%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..da0620892fb --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F7%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 4 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 0 + +null \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F8%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F8%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..da0620892fb --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F8%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 4 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 0 + +null \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F9%2Freactions%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F9%2Freactions%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..da0620892fb --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2F9%2Freactions%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 4 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 0 + +null \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2Fcomments%2F116550%2Freactions b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2Fcomments%2F116550%2Freactions new file mode 100644 index 00000000000..054c0660d39 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2Fcomments%2F116550%2Freactions @@ -0,0 +1,8 @@ +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 4 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff + +null \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2Fcomments%2F116552%2Freactions b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2Fcomments%2F116552%2Freactions new file mode 100644 index 00000000000..054c0660d39 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%2Fcomments%2F116552%2Freactions @@ -0,0 +1,8 @@ +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 4 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff + +null \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%3Flimit%3D2%26page%3D3%26state%3Dall%26type%3Dissues b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%3Flimit%3D2%26page%3D3%26state%3Dall%26type%3Dissues new file mode 100644 index 00000000000..c0b1ef2821b --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%3Flimit%3D2%26page%3D3%26state%3Dall%26type%3Dissues @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: Link, X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Type: application/json;charset=utf-8 +Link: ; rel="next",; rel="last",; rel="first",; rel="prev" +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 7 + +[{"id":30475,"url":"https://gitea.com/api/v1/repos/gitea/test_repo/issues/4","html_url":"https://gitea.com/gitea/test_repo/issues/4","number":4,"user":{"id":-1,"login":"Ghost","login_name":"","source_id":0,"full_name":"","email":"-1+ghost@noreply.gitea.com","avatar_url":"https://gitea.com/assets/img/avatar_default.png","html_url":"https://gitea.com/Ghost","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"1970-01-01T00:00:00Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"Ghost"},"original_author":"","original_author_id":0,"title":"what is this repo about?","body":"","ref":"","assets":[],"labels":[{"id":3733,"name":"Question","exclusive":false,"is_archived":false,"color":"fbca04","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3733"}],"milestone":{"id":1300,"title":"V1","description":"Generate Content","state":"closed","open_issues":0,"closed_issues":4,"created_at":"1970-01-01T00:00:00Z","updated_at":"1970-01-01T00:00:00Z","closed_at":"2020-09-01T18:36:46Z","due_on":null},"assignee":null,"assignees":null,"state":"closed","is_locked":true,"comments":2,"created_at":"2020-09-01T15:48:41Z","updated_at":"2020-09-01T15:50:00Z","closed_at":"2020-09-01T15:49:34Z","due_date":null,"time_estimate":0,"pull_request":null,"repository":{"id":16268,"name":"test_repo","owner":"gitea","full_name":"gitea/test_repo"},"pin_order":0},{"id":30471,"url":"https://gitea.com/api/v1/repos/gitea/test_repo/issues/2","html_url":"https://gitea.com/gitea/test_repo/issues/2","number":2,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"original_author":"","original_author_id":0,"title":"Spam","body":":(","ref":"","assets":[],"labels":[{"id":3732,"name":"Invalid","exclusive":false,"is_archived":false,"color":"d4c5f9","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3732"}],"milestone":null,"assignee":null,"assignees":null,"state":"closed","is_locked":false,"comments":2,"created_at":"2020-09-01T00:23:00Z","updated_at":"2020-09-01T14:11:37Z","closed_at":"2020-09-01T14:11:37Z","due_date":null,"time_estimate":0,"pull_request":null,"repository":{"id":16268,"name":"test_repo","owner":"gitea","full_name":"gitea/test_repo"},"pin_order":0}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%3Flimit%3D50%26page%3D1%26state%3Dall%26type%3Dissues b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%3Flimit%3D50%26page%3D1%26state%3Dall%26type%3Dissues new file mode 100644 index 00000000000..aef3e266b0b --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fissues%3Flimit%3D50%26page%3D1%26state%3Dall%26type%3Dissues @@ -0,0 +1,9 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 7 + +[{"id":30481,"url":"https://gitea.com/api/v1/repos/gitea/test_repo/issues/10","html_url":"https://gitea.com/gitea/test_repo/issues/10","number":10,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"original_author":"","original_author_id":0,"title":"A'm I allowed to fork it?","body":"yes but do not create pull requests anymore","ref":"","assets":[],"labels":[{"id":3733,"name":"Question","exclusive":false,"is_archived":false,"color":"fbca04","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3733"}],"milestone":null,"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2020-09-01T17:48:14Z","updated_at":"2020-09-01T17:48:14Z","closed_at":null,"due_date":null,"time_estimate":0,"pull_request":null,"repository":{"id":16268,"name":"test_repo","owner":"gitea","full_name":"gitea/test_repo"},"pin_order":0},{"id":30480,"url":"https://gitea.com/api/v1/repos/gitea/test_repo/issues/9","html_url":"https://gitea.com/gitea/test_repo/issues/9","number":9,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"original_author":"","original_author_id":0,"title":"Idears","body":"this is an example for an open issue - they just cant be all closed ;)","ref":"","assets":[],"labels":[{"id":3735,"name":"Enhancement","exclusive":false,"is_archived":false,"color":"207de5","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3735"}],"milestone":{"id":1301,"title":"V2 Finalize","description":"","state":"open","open_issues":1,"closed_issues":2,"created_at":"1970-01-01T00:00:00Z","updated_at":"2022-11-13T05:29:15Z","closed_at":null,"due_on":"2020-09-04T23:59:59Z"},"assignee":null,"assignees":null,"state":"open","is_locked":false,"comments":0,"created_at":"2020-09-01T17:47:11Z","updated_at":"2020-09-01T17:47:17Z","closed_at":null,"due_date":null,"time_estimate":0,"pull_request":null,"repository":{"id":16268,"name":"test_repo","owner":"gitea","full_name":"gitea/test_repo"},"pin_order":0},{"id":30477,"url":"https://gitea.com/api/v1/repos/gitea/test_repo/issues/6","html_url":"https://gitea.com/gitea/test_repo/issues/6","number":6,"user":{"id":9,"login":"techknowlogick","login_name":"","source_id":0,"full_name":"","email":"9+techknowlogick@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/9b588dd0b384d6f6ae841c5d62302033","html_url":"https://gitea.com/techknowlogick","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-01-14T06:48:35Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"https://techknowlogick.com","description":"","visibility":"public","followers_count":15,"following_count":1,"starred_repos_count":51,"username":"techknowlogick"},"original_author":"","original_author_id":0,"title":"Please add a tag (or a release)","body":"","ref":"","assets":[],"labels":[],"milestone":null,"assignee":null,"assignees":null,"state":"closed","is_locked":false,"comments":1,"created_at":"2020-09-01T16:07:01Z","updated_at":"2020-09-01T17:26:02Z","closed_at":"2020-09-01T17:26:02Z","due_date":null,"time_estimate":0,"pull_request":null,"repository":{"id":16268,"name":"test_repo","owner":"gitea","full_name":"gitea/test_repo"},"pin_order":0},{"id":30476,"url":"https://gitea.com/api/v1/repos/gitea/test_repo/issues/5","html_url":"https://gitea.com/gitea/test_repo/issues/5","number":5,"user":{"id":9,"login":"techknowlogick","login_name":"","source_id":0,"full_name":"","email":"9+techknowlogick@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/9b588dd0b384d6f6ae841c5d62302033","html_url":"https://gitea.com/techknowlogick","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-01-14T06:48:35Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"https://techknowlogick.com","description":"","visibility":"public","followers_count":15,"following_count":1,"starred_repos_count":51,"username":"techknowlogick"},"original_author":"","original_author_id":0,"title":"Need more contributors to this repo","body":"I volunteer as one","ref":"","assets":[],"labels":[],"milestone":{"id":1301,"title":"V2 Finalize","description":"","state":"open","open_issues":1,"closed_issues":2,"created_at":"1970-01-01T00:00:00Z","updated_at":"2022-11-13T05:29:15Z","closed_at":null,"due_on":"2020-09-04T23:59:59Z"},"assignee":null,"assignees":null,"state":"closed","is_locked":false,"comments":1,"created_at":"2020-09-01T16:06:30Z","updated_at":"2020-09-01T17:46:09Z","closed_at":"2020-09-01T17:46:09Z","due_date":null,"time_estimate":0,"pull_request":null,"repository":{"id":16268,"name":"test_repo","owner":"gitea","full_name":"gitea/test_repo"},"pin_order":0},{"id":30475,"url":"https://gitea.com/api/v1/repos/gitea/test_repo/issues/4","html_url":"https://gitea.com/gitea/test_repo/issues/4","number":4,"user":{"id":-1,"login":"Ghost","login_name":"","source_id":0,"full_name":"","email":"-1+ghost@noreply.gitea.com","avatar_url":"https://gitea.com/assets/img/avatar_default.png","html_url":"https://gitea.com/Ghost","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"1970-01-01T00:00:00Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"Ghost"},"original_author":"","original_author_id":0,"title":"what is this repo about?","body":"","ref":"","assets":[],"labels":[{"id":3733,"name":"Question","exclusive":false,"is_archived":false,"color":"fbca04","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3733"}],"milestone":{"id":1300,"title":"V1","description":"Generate Content","state":"closed","open_issues":0,"closed_issues":4,"created_at":"1970-01-01T00:00:00Z","updated_at":"1970-01-01T00:00:00Z","closed_at":"2020-09-01T18:36:46Z","due_on":null},"assignee":null,"assignees":null,"state":"closed","is_locked":true,"comments":2,"created_at":"2020-09-01T15:48:41Z","updated_at":"2020-09-01T15:50:00Z","closed_at":"2020-09-01T15:49:34Z","due_date":null,"time_estimate":0,"pull_request":null,"repository":{"id":16268,"name":"test_repo","owner":"gitea","full_name":"gitea/test_repo"},"pin_order":0},{"id":30471,"url":"https://gitea.com/api/v1/repos/gitea/test_repo/issues/2","html_url":"https://gitea.com/gitea/test_repo/issues/2","number":2,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"original_author":"","original_author_id":0,"title":"Spam","body":":(","ref":"","assets":[],"labels":[{"id":3732,"name":"Invalid","exclusive":false,"is_archived":false,"color":"d4c5f9","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3732"}],"milestone":null,"assignee":null,"assignees":null,"state":"closed","is_locked":false,"comments":2,"created_at":"2020-09-01T00:23:00Z","updated_at":"2020-09-01T14:11:37Z","closed_at":"2020-09-01T14:11:37Z","due_date":null,"time_estimate":0,"pull_request":null,"repository":{"id":16268,"name":"test_repo","owner":"gitea","full_name":"gitea/test_repo"},"pin_order":0},{"id":30470,"url":"https://gitea.com/api/v1/repos/gitea/test_repo/issues/1","html_url":"https://gitea.com/gitea/test_repo/issues/1","number":1,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"original_author":"","original_author_id":0,"title":"Here Is no content!","body":"","ref":"","assets":[],"labels":[{"id":3734,"name":"Valid","exclusive":false,"is_archived":false,"color":"53e917","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3734"}],"milestone":{"id":1300,"title":"V1","description":"Generate Content","state":"closed","open_issues":0,"closed_issues":4,"created_at":"1970-01-01T00:00:00Z","updated_at":"1970-01-01T00:00:00Z","closed_at":"2020-09-01T18:36:46Z","due_on":null},"assignee":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"assignees":[{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"}],"state":"closed","is_locked":false,"comments":0,"created_at":"2020-09-01T00:15:11Z","updated_at":"2020-09-01T17:26:25Z","closed_at":"2020-09-01T17:26:25Z","due_date":null,"time_estimate":0,"pull_request":null,"repository":{"id":16268,"name":"test_repo","owner":"gitea","full_name":"gitea/test_repo"},"pin_order":0}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Flabels%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Flabels%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..db9a0c05749 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Flabels%3Flimit%3D50%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 1025 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 6 + +[{"id":3730,"name":"Bug","exclusive":false,"is_archived":false,"color":"e11d21","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3730"},{"id":3735,"name":"Enhancement","exclusive":false,"is_archived":false,"color":"207de5","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3735"},{"id":3731,"name":"Feature","exclusive":false,"is_archived":false,"color":"0052cc","description":"a feature request","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3731"},{"id":3732,"name":"Invalid","exclusive":false,"is_archived":false,"color":"d4c5f9","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3732"},{"id":3733,"name":"Question","exclusive":false,"is_archived":false,"color":"fbca04","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3733"},{"id":3734,"name":"Valid","exclusive":false,"is_archived":false,"color":"53e917","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3734"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fmilestones%3Flimit%3D50%26page%3D1%26state%3Dall b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fmilestones%3Flimit%3D50%26page%3D1%26state%3Dall new file mode 100644 index 00000000000..49aa331b81b --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fmilestones%3Flimit%3D50%26page%3D1%26state%3Dall @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 452 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 2 + +[{"id":1300,"title":"V1","description":"Generate Content","state":"closed","open_issues":0,"closed_issues":4,"created_at":"1970-01-01T00:00:00Z","updated_at":"1970-01-01T00:00:00Z","closed_at":"2020-09-01T18:36:46Z","due_on":null},{"id":1301,"title":"V2 Finalize","description":"","state":"open","open_issues":1,"closed_issues":2,"created_at":"1970-01-01T00:00:00Z","updated_at":"2022-11-13T05:29:15Z","closed_at":null,"due_on":"2020-09-04T23:59:59Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%2F7%2Freviews%2F1770%2Fcomments b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%2F7%2Freviews%2F1770%2Fcomments new file mode 100644 index 00000000000..e4d090ddc87 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%2F7%2Freviews%2F1770%2Fcomments @@ -0,0 +1,8 @@ +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 1225 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff + +[{"id":116561,"body":"is one `\\newline` to less?","user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"resolver":null,"pull_request_review_id":1770,"created_at":"2020-09-01T16:12:58Z","updated_at":"2024-06-03T01:18:36Z","path":"README.md","commit_id":"187ece0cb6631e2858a6872e5733433bb3ca3b03","original_commit_id":"","diff_hunk":"@@ -2,3 +2,3 @@\n \n-Test repository for testing migration from gitea 2 gitea\n\\ No newline at end of file\n+Test repository for testing migration from gitea 2 gitea","position":4,"original_position":0,"html_url":"https://gitea.com/gitea/test_repo/pulls/7#issuecomment-116561","pull_request_url":"https://gitea.com/gitea/test_repo/pulls/7"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%2F7%2Freviews%2F1771%2Fcomments b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%2F7%2Freviews%2F1771%2Fcomments new file mode 100644 index 00000000000..a8ae76d7157 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%2F7%2Freviews%2F1771%2Fcomments @@ -0,0 +1,8 @@ +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 2 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%2F7%2Freviews%2F1772%2Fcomments b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%2F7%2Freviews%2F1772%2Fcomments new file mode 100644 index 00000000000..a8ae76d7157 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%2F7%2Freviews%2F1772%2Fcomments @@ -0,0 +1,8 @@ +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 2 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%2F7%2Freviews%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%2F7%2Freviews%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..5aee56e7ac3 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%2F7%2Freviews%3Flimit%3D50%26page%3D1 @@ -0,0 +1,9 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 3 + +[{"id":1770,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"team":null,"state":"COMMENT","body":"","commit_id":"187ece0cb6631e2858a6872e5733433bb3ca3b03","stale":false,"official":false,"dismissed":true,"comments_count":1,"submitted_at":"2020-09-01T16:12:58Z","updated_at":"2021-04-18T22:00:49Z","html_url":"https://gitea.com/gitea/test_repo/pulls/7#issuecomment-116562","pull_request_url":"https://gitea.com/gitea/test_repo/pulls/7"},{"id":1771,"user":{"id":9,"login":"techknowlogick","login_name":"","source_id":0,"full_name":"","email":"9+techknowlogick@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/9b588dd0b384d6f6ae841c5d62302033","html_url":"https://gitea.com/techknowlogick","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-01-14T06:48:35Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"https://techknowlogick.com","description":"","visibility":"public","followers_count":15,"following_count":1,"starred_repos_count":51,"username":"techknowlogick"},"team":null,"state":"REQUEST_CHANGES","body":"I think this needs some changes","commit_id":"187ece0cb6631e2858a6872e5733433bb3ca3b03","stale":false,"official":false,"dismissed":true,"comments_count":0,"submitted_at":"2020-09-01T17:06:47Z","updated_at":"2021-04-18T22:00:49Z","html_url":"https://gitea.com/gitea/test_repo/pulls/7#issuecomment-116563","pull_request_url":"https://gitea.com/gitea/test_repo/pulls/7"},{"id":1772,"user":{"id":9,"login":"techknowlogick","login_name":"","source_id":0,"full_name":"","email":"9+techknowlogick@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/9b588dd0b384d6f6ae841c5d62302033","html_url":"https://gitea.com/techknowlogick","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-01-14T06:48:35Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"https://techknowlogick.com","description":"","visibility":"public","followers_count":15,"following_count":1,"starred_repos_count":51,"username":"techknowlogick"},"team":null,"state":"APPROVED","body":"looks good","commit_id":"187ece0cb6631e2858a6872e5733433bb3ca3b03","stale":false,"official":true,"dismissed":true,"comments_count":0,"submitted_at":"2020-09-01T17:19:51Z","updated_at":"2021-04-18T22:00:49Z","html_url":"https://gitea.com/gitea/test_repo/pulls/7#issuecomment-116564","pull_request_url":"https://gitea.com/gitea/test_repo/pulls/7"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%3Flimit%3D3%26page%3D1%26state%3Dall b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%3Flimit%3D3%26page%3D1%26state%3Dall new file mode 100644 index 00000000000..cdf1f0d2f00 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%3Flimit%3D3%26page%3D1%26state%3Dall @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: Link, X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Type: application/json;charset=utf-8 +Link: ; rel="next",; rel="last" +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 6 + +[{"id":4955,"url":"https://gitea.com/gitea/test_repo/pulls/13","number":13,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"title":"extend","body":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"requested_reviewers":null,"requested_reviewers_teams":null,"state":"open","draft":false,"is_locked":true,"comments":1,"review_comments":0,"html_url":"https://gitea.com/gitea/test_repo/pulls/13","diff_url":"https://gitea.com/gitea/test_repo/pulls/13.diff","patch_url":"https://gitea.com/gitea/test_repo/pulls/13.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"allow_maintainer_edit":false,"base":{"label":"master","ref":"master","sha":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"head":{"label":"6543-patch-1","ref":"6543-patch-1","sha":"0ba7693bfd50d26df7f1b7414e937786c5efb05d","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"merge_base":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","due_date":null,"created_at":"2020-09-01T18:03:54Z","updated_at":"2020-09-01T18:04:26Z","closed_at":null,"pin_order":0},{"id":4954,"url":"https://gitea.com/gitea/test_repo/pulls/12","number":12,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"title":"Dont Touch","body":"\r\nadd dont touch note","labels":[],"milestone":{"id":1301,"title":"V2 Finalize","description":"","state":"open","open_issues":1,"closed_issues":2,"created_at":"1970-01-01T00:00:00Z","updated_at":"2022-11-13T05:29:15Z","closed_at":null,"due_on":"2020-09-04T23:59:59Z"},"assignee":{"id":9,"login":"techknowlogick","login_name":"","source_id":0,"full_name":"","email":"9+techknowlogick@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/9b588dd0b384d6f6ae841c5d62302033","html_url":"https://gitea.com/techknowlogick","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-01-14T06:48:35Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"https://techknowlogick.com","description":"","visibility":"public","followers_count":15,"following_count":1,"starred_repos_count":51,"username":"techknowlogick"},"assignees":[{"id":9,"login":"techknowlogick","login_name":"","source_id":0,"full_name":"","email":"9+techknowlogick@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/9b588dd0b384d6f6ae841c5d62302033","html_url":"https://gitea.com/techknowlogick","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-01-14T06:48:35Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"https://techknowlogick.com","description":"","visibility":"public","followers_count":15,"following_count":1,"starred_repos_count":51,"username":"techknowlogick"}],"requested_reviewers":null,"requested_reviewers_teams":null,"state":"closed","draft":false,"is_locked":false,"comments":3,"review_comments":3,"html_url":"https://gitea.com/gitea/test_repo/pulls/12","diff_url":"https://gitea.com/gitea/test_repo/pulls/12.diff","patch_url":"https://gitea.com/gitea/test_repo/pulls/12.patch","mergeable":true,"merged":true,"merged_at":"2020-09-01T17:55:34Z","merge_commit_sha":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","merged_by":null,"allow_maintainer_edit":false,"base":{"label":"master","ref":"master","sha":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"head":{"label":"Add-Dont-Touch-Note","ref":"refs/pull/12/head","sha":"b6ab5d9ae000b579a5fff03f92c486da4ddf48b6","repo_id":16280,"repo":{"id":16280,"owner":{"id":9756,"login":"6543-forks","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/c3948ab3b9b62e070e87a22681909dee","html_url":"https://gitea.com/6543-forks","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2020-09-01T17:33:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"@6543's fork org","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"6543-forks"},"name":"test_repo","full_name":"6543-forks/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":true,"template":false,"parent":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]},"mirror":false,"size":67,"language":"","languages_url":"https://gitea.com/api/v1/repos/6543-forks/test_repo/languages","html_url":"https://gitea.com/6543-forks/test_repo","url":"https://gitea.com/api/v1/repos/6543-forks/test_repo","link":"","ssh_url":"git@gitea.com:6543-forks/test_repo.git","clone_url":"https://gitea.com/6543-forks/test_repo.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"master","archived":false,"created_at":"2020-09-01T17:39:26Z","updated_at":"2020-09-01T17:57:07Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":false,"has_wiki":false,"has_pull_requests":false,"has_projects":false,"projects_mode":"all","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":false,"allow_rebase":false,"allow_rebase_explicit":false,"allow_squash_merge":false,"allow_fast_forward_only_merge":false,"allow_rebase_update":false,"allow_manual_merge":true,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":[],"licenses":[]}},"merge_base":"d9e165e4c7ab6b701f0205d0ffb637e5d2856297","due_date":null,"created_at":"2020-09-01T17:52:39Z","updated_at":"2020-09-02T05:10:25Z","closed_at":"2020-09-01T17:55:33Z","pin_order":0},{"id":4953,"url":"https://gitea.com/gitea/test_repo/pulls/11","number":11,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"title":"add-xkcd-2199","body":"","labels":[{"id":3734,"name":"Valid","exclusive":false,"is_archived":false,"color":"53e917","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3734"}],"milestone":null,"assignee":null,"assignees":null,"requested_reviewers":null,"requested_reviewers_teams":null,"state":"open","draft":false,"is_locked":false,"comments":0,"review_comments":0,"html_url":"https://gitea.com/gitea/test_repo/pulls/11","diff_url":"https://gitea.com/gitea/test_repo/pulls/11.diff","patch_url":"https://gitea.com/gitea/test_repo/pulls/11.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"allow_maintainer_edit":false,"base":{"label":"master","ref":"master","sha":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"head":{"label":"add-xkcd-2199","ref":"add-xkcd-2199","sha":"6bbd02573205288faa95d25e917812b2815a37e5","repo_id":16280,"repo":{"id":16280,"owner":{"id":9756,"login":"6543-forks","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/c3948ab3b9b62e070e87a22681909dee","html_url":"https://gitea.com/6543-forks","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2020-09-01T17:33:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"@6543's fork org","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"6543-forks"},"name":"test_repo","full_name":"6543-forks/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":true,"template":false,"parent":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]},"mirror":false,"size":67,"language":"","languages_url":"https://gitea.com/api/v1/repos/6543-forks/test_repo/languages","html_url":"https://gitea.com/6543-forks/test_repo","url":"https://gitea.com/api/v1/repos/6543-forks/test_repo","link":"","ssh_url":"git@gitea.com:6543-forks/test_repo.git","clone_url":"https://gitea.com/6543-forks/test_repo.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"master","archived":false,"created_at":"2020-09-01T17:39:26Z","updated_at":"2020-09-01T17:57:07Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":false,"has_wiki":false,"has_pull_requests":false,"has_projects":false,"projects_mode":"all","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":false,"allow_rebase":false,"allow_rebase_explicit":false,"allow_squash_merge":false,"allow_fast_forward_only_merge":false,"allow_rebase_update":false,"allow_manual_merge":true,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":[],"licenses":[]}},"merge_base":"b6ab5d9ae000b579a5fff03f92c486da4ddf48b6","due_date":null,"created_at":"2020-09-01T17:52:28Z","updated_at":"2020-09-01T17:52:29Z","closed_at":null,"pin_order":0}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%3Flimit%3D50%26page%3D1%26state%3Dall b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%3Flimit%3D50%26page%3D1%26state%3Dall new file mode 100644 index 00000000000..bee7f36719e --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Fpulls%3Flimit%3D50%26page%3D1%26state%3Dall @@ -0,0 +1,9 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 6 + +[{"id":4955,"url":"https://gitea.com/gitea/test_repo/pulls/13","number":13,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"title":"extend","body":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"requested_reviewers":null,"requested_reviewers_teams":null,"state":"open","draft":false,"is_locked":true,"comments":1,"review_comments":0,"html_url":"https://gitea.com/gitea/test_repo/pulls/13","diff_url":"https://gitea.com/gitea/test_repo/pulls/13.diff","patch_url":"https://gitea.com/gitea/test_repo/pulls/13.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"allow_maintainer_edit":false,"base":{"label":"master","ref":"master","sha":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"head":{"label":"6543-patch-1","ref":"6543-patch-1","sha":"0ba7693bfd50d26df7f1b7414e937786c5efb05d","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"merge_base":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","due_date":null,"created_at":"2020-09-01T18:03:54Z","updated_at":"2020-09-01T18:04:26Z","closed_at":null,"pin_order":0},{"id":4954,"url":"https://gitea.com/gitea/test_repo/pulls/12","number":12,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"title":"Dont Touch","body":"\r\nadd dont touch note","labels":[],"milestone":{"id":1301,"title":"V2 Finalize","description":"","state":"open","open_issues":1,"closed_issues":2,"created_at":"1970-01-01T00:00:00Z","updated_at":"2022-11-13T05:29:15Z","closed_at":null,"due_on":"2020-09-04T23:59:59Z"},"assignee":{"id":9,"login":"techknowlogick","login_name":"","source_id":0,"full_name":"","email":"9+techknowlogick@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/9b588dd0b384d6f6ae841c5d62302033","html_url":"https://gitea.com/techknowlogick","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-01-14T06:48:35Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"https://techknowlogick.com","description":"","visibility":"public","followers_count":15,"following_count":1,"starred_repos_count":51,"username":"techknowlogick"},"assignees":[{"id":9,"login":"techknowlogick","login_name":"","source_id":0,"full_name":"","email":"9+techknowlogick@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/9b588dd0b384d6f6ae841c5d62302033","html_url":"https://gitea.com/techknowlogick","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-01-14T06:48:35Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"https://techknowlogick.com","description":"","visibility":"public","followers_count":15,"following_count":1,"starred_repos_count":51,"username":"techknowlogick"}],"requested_reviewers":null,"requested_reviewers_teams":null,"state":"closed","draft":false,"is_locked":false,"comments":3,"review_comments":3,"html_url":"https://gitea.com/gitea/test_repo/pulls/12","diff_url":"https://gitea.com/gitea/test_repo/pulls/12.diff","patch_url":"https://gitea.com/gitea/test_repo/pulls/12.patch","mergeable":true,"merged":true,"merged_at":"2020-09-01T17:55:34Z","merge_commit_sha":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","merged_by":null,"allow_maintainer_edit":false,"base":{"label":"master","ref":"master","sha":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"head":{"label":"Add-Dont-Touch-Note","ref":"refs/pull/12/head","sha":"b6ab5d9ae000b579a5fff03f92c486da4ddf48b6","repo_id":16280,"repo":{"id":16280,"owner":{"id":9756,"login":"6543-forks","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/c3948ab3b9b62e070e87a22681909dee","html_url":"https://gitea.com/6543-forks","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2020-09-01T17:33:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"@6543's fork org","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"6543-forks"},"name":"test_repo","full_name":"6543-forks/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":true,"template":false,"parent":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]},"mirror":false,"size":67,"language":"","languages_url":"https://gitea.com/api/v1/repos/6543-forks/test_repo/languages","html_url":"https://gitea.com/6543-forks/test_repo","url":"https://gitea.com/api/v1/repos/6543-forks/test_repo","link":"","ssh_url":"git@gitea.com:6543-forks/test_repo.git","clone_url":"https://gitea.com/6543-forks/test_repo.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"master","archived":false,"created_at":"2020-09-01T17:39:26Z","updated_at":"2020-09-01T17:57:07Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":false,"has_wiki":false,"has_pull_requests":false,"has_projects":false,"projects_mode":"all","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":false,"allow_rebase":false,"allow_rebase_explicit":false,"allow_squash_merge":false,"allow_fast_forward_only_merge":false,"allow_rebase_update":false,"allow_manual_merge":true,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":[],"licenses":[]}},"merge_base":"d9e165e4c7ab6b701f0205d0ffb637e5d2856297","due_date":null,"created_at":"2020-09-01T17:52:39Z","updated_at":"2020-09-02T05:10:25Z","closed_at":"2020-09-01T17:55:33Z","pin_order":0},{"id":4953,"url":"https://gitea.com/gitea/test_repo/pulls/11","number":11,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"title":"add-xkcd-2199","body":"","labels":[{"id":3734,"name":"Valid","exclusive":false,"is_archived":false,"color":"53e917","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3734"}],"milestone":null,"assignee":null,"assignees":null,"requested_reviewers":null,"requested_reviewers_teams":null,"state":"open","draft":false,"is_locked":false,"comments":0,"review_comments":0,"html_url":"https://gitea.com/gitea/test_repo/pulls/11","diff_url":"https://gitea.com/gitea/test_repo/pulls/11.diff","patch_url":"https://gitea.com/gitea/test_repo/pulls/11.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"allow_maintainer_edit":false,"base":{"label":"master","ref":"master","sha":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"head":{"label":"add-xkcd-2199","ref":"add-xkcd-2199","sha":"6bbd02573205288faa95d25e917812b2815a37e5","repo_id":16280,"repo":{"id":16280,"owner":{"id":9756,"login":"6543-forks","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/c3948ab3b9b62e070e87a22681909dee","html_url":"https://gitea.com/6543-forks","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2020-09-01T17:33:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"@6543's fork org","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"6543-forks"},"name":"test_repo","full_name":"6543-forks/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":true,"template":false,"parent":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]},"mirror":false,"size":67,"language":"","languages_url":"https://gitea.com/api/v1/repos/6543-forks/test_repo/languages","html_url":"https://gitea.com/6543-forks/test_repo","url":"https://gitea.com/api/v1/repos/6543-forks/test_repo","link":"","ssh_url":"git@gitea.com:6543-forks/test_repo.git","clone_url":"https://gitea.com/6543-forks/test_repo.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"master","archived":false,"created_at":"2020-09-01T17:39:26Z","updated_at":"2020-09-01T17:57:07Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":false,"has_wiki":false,"has_pull_requests":false,"has_projects":false,"projects_mode":"all","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":false,"allow_rebase":false,"allow_rebase_explicit":false,"allow_squash_merge":false,"allow_fast_forward_only_merge":false,"allow_rebase_update":false,"allow_manual_merge":true,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":[],"licenses":[]}},"merge_base":"b6ab5d9ae000b579a5fff03f92c486da4ddf48b6","due_date":null,"created_at":"2020-09-01T17:52:28Z","updated_at":"2020-09-01T17:52:29Z","closed_at":null,"pin_order":0},{"id":4952,"url":"https://gitea.com/gitea/test_repo/pulls/8","number":8,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"title":"add garbage for close pull","body":"well you'll see","labels":[],"milestone":null,"assignee":null,"assignees":null,"requested_reviewers":null,"requested_reviewers_teams":null,"state":"closed","draft":false,"is_locked":false,"comments":0,"review_comments":0,"html_url":"https://gitea.com/gitea/test_repo/pulls/8","diff_url":"https://gitea.com/gitea/test_repo/pulls/8.diff","patch_url":"https://gitea.com/gitea/test_repo/pulls/8.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"allow_maintainer_edit":false,"base":{"label":"master","ref":"master","sha":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"head":{"label":"garbage-patch","ref":"refs/pull/8/head","sha":"a3427235639a33d2d749e76f076e7619acc75341","repo_id":16280,"repo":{"id":16280,"owner":{"id":9756,"login":"6543-forks","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/c3948ab3b9b62e070e87a22681909dee","html_url":"https://gitea.com/6543-forks","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2020-09-01T17:33:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"@6543's fork org","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"6543-forks"},"name":"test_repo","full_name":"6543-forks/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":true,"template":false,"parent":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]},"mirror":false,"size":67,"language":"","languages_url":"https://gitea.com/api/v1/repos/6543-forks/test_repo/languages","html_url":"https://gitea.com/6543-forks/test_repo","url":"https://gitea.com/api/v1/repos/6543-forks/test_repo","link":"","ssh_url":"git@gitea.com:6543-forks/test_repo.git","clone_url":"https://gitea.com/6543-forks/test_repo.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":0,"release_counter":0,"default_branch":"master","archived":false,"created_at":"2020-09-01T17:39:26Z","updated_at":"2020-09-01T17:57:07Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":false,"has_wiki":false,"has_pull_requests":false,"has_projects":false,"projects_mode":"all","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":false,"allow_rebase":false,"allow_rebase_explicit":false,"allow_squash_merge":false,"allow_fast_forward_only_merge":false,"allow_rebase_update":false,"allow_manual_merge":true,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":[],"licenses":[]}},"merge_base":"d9e165e4c7ab6b701f0205d0ffb637e5d2856297","due_date":null,"created_at":"2020-09-01T17:43:20Z","updated_at":"2020-09-01T17:48:41Z","closed_at":"2020-09-01T17:48:29Z","pin_order":0},{"id":4951,"url":"https://gitea.com/gitea/test_repo/pulls/7","number":7,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"title":"Prepare for Release V1","body":"@techknowlogick you might have a look at it?\n\nclose #6","labels":[{"id":3735,"name":"Enhancement","exclusive":false,"is_archived":false,"color":"207de5","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3735"}],"milestone":{"id":1300,"title":"V1","description":"Generate Content","state":"closed","open_issues":0,"closed_issues":4,"created_at":"1970-01-01T00:00:00Z","updated_at":"1970-01-01T00:00:00Z","closed_at":"2020-09-01T18:36:46Z","due_on":null},"assignee":null,"assignees":null,"requested_reviewers":null,"requested_reviewers_teams":null,"state":"closed","draft":false,"is_locked":false,"comments":4,"review_comments":3,"html_url":"https://gitea.com/gitea/test_repo/pulls/7","diff_url":"https://gitea.com/gitea/test_repo/pulls/7.diff","patch_url":"https://gitea.com/gitea/test_repo/pulls/7.patch","mergeable":true,"merged":true,"merged_at":"2020-09-01T17:26:02Z","merge_commit_sha":"d9e165e4c7ab6b701f0205d0ffb637e5d2856297","merged_by":null,"allow_maintainer_edit":false,"base":{"label":"master","ref":"master","sha":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"head":{"label":"prepare-v1","ref":"refs/pull/7/head","sha":"187ece0cb6631e2858a6872e5733433bb3ca3b03","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"merge_base":"9396b697d905d1bcb5380befdf4d7e52c6a7ceb2","due_date":null,"created_at":"2020-09-01T16:10:04Z","updated_at":"2020-09-01T17:26:08Z","closed_at":"2020-09-01T17:26:02Z","pin_order":0},{"id":4949,"url":"https://gitea.com/gitea/test_repo/pulls/3","number":3,"user":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"title":"Readme: use '2'","body":"","labels":[{"id":3735,"name":"Enhancement","exclusive":false,"is_archived":false,"color":"207de5","description":"","url":"https://gitea.com/api/v1/repos/gitea/test_repo/labels/3735"}],"milestone":{"id":1300,"title":"V1","description":"Generate Content","state":"closed","open_issues":0,"closed_issues":4,"created_at":"1970-01-01T00:00:00Z","updated_at":"1970-01-01T00:00:00Z","closed_at":"2020-09-01T18:36:46Z","due_on":null},"assignee":null,"assignees":null,"requested_reviewers":null,"requested_reviewers_teams":null,"state":"closed","draft":false,"is_locked":false,"comments":0,"review_comments":0,"html_url":"https://gitea.com/gitea/test_repo/pulls/3","diff_url":"https://gitea.com/gitea/test_repo/pulls/3.diff","patch_url":"https://gitea.com/gitea/test_repo/pulls/3.patch","mergeable":true,"merged":true,"merged_at":"2020-09-01T00:27:14Z","merge_commit_sha":"9396b697d905d1bcb5380befdf4d7e52c6a7ceb2","merged_by":null,"allow_maintainer_edit":false,"base":{"label":"master","ref":"master","sha":"827aa28a907853e5ddfa40c8f9bc52471a2685fd","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"head":{"label":"readme_nit","ref":"refs/pull/3/head","sha":"c273a16d4c3b2d745df690005dabe79cc6504ac3","repo_id":16268,"repo":{"id":16268,"owner":{"id":3,"login":"gitea","login_name":"","source_id":0,"full_name":"","email":"","avatar_url":"https://gitea.com/avatars/35dea380390772b3130aafbac7ca49e6","html_url":"https://gitea.com/gitea","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2018-11-29T03:16:17Z","restricted":false,"active":false,"prohibit_login":false,"location":"Git Universe","website":"https://gitea.com","description":"Git with a cup of tea","visibility":"public","followers_count":100,"following_count":0,"starred_repos_count":0,"username":"gitea"},"name":"test_repo","full_name":"gitea/test_repo","description":"Test repository for testing migration from gitea to gitea","empty":false,"private":false,"fork":false,"template":false,"mirror":false,"size":68,"language":"","languages_url":"https://gitea.com/api/v1/repos/gitea/test_repo/languages","html_url":"https://gitea.com/gitea/test_repo","url":"https://gitea.com/api/v1/repos/gitea/test_repo","link":"","ssh_url":"git@gitea.com:gitea/test_repo.git","clone_url":"https://gitea.com/gitea/test_repo.git","original_url":"","website":"","stars_count":1,"forks_count":2,"watchers_count":10,"open_issues_count":2,"open_pr_counter":2,"release_counter":2,"default_branch":"master","archived":false,"created_at":"2020-09-01T00:12:27Z","updated_at":"2020-09-01T18:03:41Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":false,"push":false,"pull":true},"has_code":true,"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"has_pull_requests":true,"has_projects":true,"projects_mode":"","has_releases":true,"has_packages":false,"has_actions":false,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":false,"allow_rebase_update":true,"allow_manual_merge":false,"autodetect_manual_merge":false,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","topics":["gitea","test","migration","ci"],"licenses":[]}},"merge_base":"a016fd754759b2cdfe5cad1cdf638c7e6b281940","due_date":null,"created_at":"2020-09-01T00:27:03Z","updated_at":"2020-09-01T15:54:30Z","closed_at":"2020-09-01T00:27:14Z","pin_order":0}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Freleases%3Flimit%3D50%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Freleases%3Flimit%3D50%26page%3D1 new file mode 100644 index 00000000000..8757df58ac0 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Freleases%3Flimit%3D50%26page%3D1 @@ -0,0 +1,9 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 2 + +[{"id":167250,"tag_name":"v2-rc1","target_commitish":"master","name":"Second Release","body":"this repo has:\r\n* reactions\r\n* wiki\r\n* issues (open/closed)\r\n* pulls (open/closed/merged) (external/internal)\r\n* pull reviews\r\n* projects\r\n* milestones\r\n* labels\r\n* releases\r\n\r\nto test migration against","url":"https://gitea.com/api/v1/repos/gitea/test_repo/releases/167250","html_url":"https://gitea.com/gitea/test_repo/releases/tag/v2-rc1","tarball_url":"https://gitea.com/gitea/test_repo/archive/v2-rc1.tar.gz","zipball_url":"https://gitea.com/gitea/test_repo/archive/v2-rc1.zip","upload_url":"https://gitea.com/api/v1/repos/gitea/test_repo/releases/167250/assets","draft":false,"prerelease":true,"created_at":"2020-09-01T18:02:43Z","published_at":"2020-09-01T18:02:43Z","author":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"assets":[]},{"id":167249,"tag_name":"V1","target_commitish":"master","name":"First Release","body":"as title","url":"https://gitea.com/api/v1/repos/gitea/test_repo/releases/167249","html_url":"https://gitea.com/gitea/test_repo/releases/tag/V1","tarball_url":"https://gitea.com/gitea/test_repo/archive/V1.tar.gz","zipball_url":"https://gitea.com/gitea/test_repo/archive/V1.zip","upload_url":"https://gitea.com/api/v1/repos/gitea/test_repo/releases/167249/assets","draft":false,"prerelease":false,"created_at":"2020-09-01T17:30:32Z","published_at":"2020-09-01T17:30:32Z","author":{"id":689,"login":"6543","login_name":"","source_id":0,"full_name":"6543","email":"689+6543@noreply.gitea.com","avatar_url":"https://gitea.com/avatars/aeb6c290f1988daefa7421c5409e80dc","html_url":"https://gitea.com/6543","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2019-07-17T21:08:41Z","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","website":"https://mh.obermui.de","description":"gitea instance: https://code.obermui.de","visibility":"public","followers_count":12,"following_count":7,"starred_repos_count":19,"username":"6543"},"assets":[]}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Ftopics%3Flimit%3D0%26page%3D1 b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Ftopics%3Flimit%3D0%26page%3D1 new file mode 100644 index 00000000000..75117adfa06 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Fgitea%2Ftest_repo%2Ftopics%3Flimit%3D0%26page%3D1 @@ -0,0 +1,10 @@ +Access-Control-Expose-Headers: X-Total-Count +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 44 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff +X-Total-Count: 4 + +{"topics":["ci","gitea","migration","test"]} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Fsettings%2Fapi b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Fsettings%2Fapi new file mode 100644 index 00000000000..44c71a31926 --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Fsettings%2Fapi @@ -0,0 +1,8 @@ +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 154 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff + +{"max_response_items":50,"default_paging_num":10,"default_git_trees_per_page":1000,"default_max_blob_size":10485760,"default_max_response_size":104857600} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Fversion b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Fversion new file mode 100644 index 00000000000..4c24027bf7b --- /dev/null +++ b/services/migrations/_mock_data/TestGiteaDownloadRepo/GET_%2Fapi%2Fv1%2Fversion @@ -0,0 +1,8 @@ +Alt-Svc: h3=":443"; ma=2592000 +Cache-Control: max-age=0, private, must-revalidate, no-transform +Content-Length: 40 +Content-Type: application/json;charset=utf-8 +Vary: Origin +X-Content-Type-Options: nosniff + +{"version":"1.26.0+dev-489-gc9a038bc4e"} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026 new file mode 100644 index 00000000000..72b97a2bb60 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026 @@ -0,0 +1,21 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"4b614e9f590cbbbc47e0f5a026615034" +Gitlab-Lb: haproxy-main-23-lb-gprd +Gitlab-Sv: api-gke-us-east1-c +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 4 +Ratelimit-Remaining: 1996 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee77dc47c27f-VIE","version":"1"} +X-Runtime: 0.143727 + +{"id":15578026,"description":"Test repository for testing migration from gitlab to gitea","name":"test_repo","name_with_namespace":"gitea / test_repo","path":"test_repo","path_with_namespace":"gitea/test_repo","created_at":"2019-11-28T08:20:33.019Z","default_branch":"master","tag_list":["migration","test"],"topics":["migration","test"],"ssh_url_to_repo":"git@gitlab.com:gitea/test_repo.git","http_url_to_repo":"https://gitlab.com/gitea/test_repo.git","web_url":"https://gitlab.com/gitea/test_repo","readme_url":"https://gitlab.com/gitea/test_repo/-/blob/master/README.md","forks_count":1,"avatar_url":null,"star_count":0,"last_activity_at":"2025-11-25T09:21:43.130Z","visibility":"public","namespace":{"id":3181312,"name":"gitea","path":"gitea","kind":"group","full_path":"gitea","parent_id":null,"avatar_url":"/uploads/-/system/group/avatar/3181312/gitea.png","web_url":"https://gitlab.com/groups/gitea"},"container_registry_image_prefix":"registry.gitlab.com/gitea/test_repo","_links":{"self":"https://gitlab.com/api/v4/projects/15578026","issues":"https://gitlab.com/api/v4/projects/15578026/issues","merge_requests":"https://gitlab.com/api/v4/projects/15578026/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/15578026/repository/branches","labels":"https://gitlab.com/api/v4/projects/15578026/labels","events":"https://gitlab.com/api/v4/projects/15578026/events","members":"https://gitlab.com/api/v4/projects/15578026/members","cluster_agents":"https://gitlab.com/api/v4/projects/15578026/cluster_agents"},"marked_for_deletion_at":null,"marked_for_deletion_on":null,"packages_enabled":true,"empty_repo":false,"archived":false,"resolve_outdated_diff_discussions":false,"repository_object_format":"sha1","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"container_registry_enabled":true,"service_desk_enabled":true,"can_create_merge_request_in":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"enabled","analytics_access_level":"enabled","container_registry_access_level":"enabled","security_and_compliance_access_level":"private","releases_access_level":"enabled","environments_access_level":"enabled","feature_flags_access_level":"enabled","infrastructure_access_level":"enabled","monitor_access_level":"enabled","model_experiments_access_level":"enabled","model_registry_access_level":"enabled","package_registry_access_level":"public","emails_disabled":false,"emails_enabled":true,"show_diff_preview_in_email":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":1241334,"import_status":"none","open_issues_count":0,"description_html":"\u003cp data-sourcepos=\"1:1-1:58\" dir=\"auto\"\u003eTest repository for testing migration from gitlab to gitea\u003c/p\u003e","updated_at":"2025-11-25T09:21:43.130Z","ci_config_path":null,"public_jobs":true,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"allow_merge_on_skipped_pipeline":null,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":true,"printing_merge_request_link_enabled":true,"merge_method":"ff","squash_option":"default_off","enforce_auth_checks_on_uploads":true,"suggestion_commit_message":null,"merge_commit_template":null,"squash_commit_template":null,"issue_branch_template":null,"warn_about_potentially_unwanted_characters":true,"autoclose_referenced_issues":true,"max_artifacts_size":null,"external_authorization_classification_label":"","requirements_enabled":false,"requirements_access_level":"enabled","security_and_compliance_enabled":false,"compliance_frameworks":[],"permissions":{"project_access":null,"group_access":null}} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F1%2Faward_emoji%3Fpage%3D1%26per_page%3D2 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F1%2Faward_emoji%3Fpage%3D1%26per_page%3D2 new file mode 100644 index 00000000000..136de6525ec --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F1%2Faward_emoji%3Fpage%3D1%26per_page%3D2 @@ -0,0 +1,28 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"5d658c62fd97f12d795c95ef106690e4" +Gitlab-Lb: haproxy-main-33-lb-gprd +Gitlab-Sv: api-gke-us-east1-d +Link: ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 9 +Ratelimit-Remaining: 1991 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee819eddc27f-VIE","version":"1"} +X-Next-Page: +X-Page: 1 +X-Per-Page: 2 +X-Prev-Page: +X-Runtime: 0.068866 +X-Total: 2 +X-Total-Pages: 1 + +[{"id":3009580,"name":"thumbsup","user":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:43:40.322Z","updated_at":"2019-11-28T08:43:40.322Z","awardable_id":27687675,"awardable_type":"Issue","url":null},{"id":3009585,"name":"open_mouth","user":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:44:01.902Z","updated_at":"2019-11-28T08:44:01.902Z","awardable_id":27687675,"awardable_type":"Issue","url":null}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F1%2Faward_emoji%3Fpage%3D2%26per_page%3D2 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F1%2Faward_emoji%3Fpage%3D2%26per_page%3D2 new file mode 100644 index 00000000000..8350faf5f2a --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F1%2Faward_emoji%3Fpage%3D2%26per_page%3D2 @@ -0,0 +1,30 @@ +Accept-Ranges: bytes +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +Gitlab-Lb: haproxy-main-30-lb-gprd +Gitlab-Sv: api-gke-us-east1-d +Link: ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 10 +Ratelimit-Remaining: 1990 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee83389cc27f-VIE","version":"1"} +X-Next-Page: +X-Page: 2 +X-Per-Page: 2 +X-Prev-Page: +X-Runtime: 0.064390 +X-Total: 2 +X-Total-Pages: 1 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Faward_emoji%3Fpage%3D1%26per_page%3D2 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Faward_emoji%3Fpage%3D1%26per_page%3D2 new file mode 100644 index 00000000000..264afd44262 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Faward_emoji%3Fpage%3D1%26per_page%3D2 @@ -0,0 +1,28 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"c14d66cdce11232dc1358893d1c5fb88" +Gitlab-Lb: haproxy-main-29-lb-gprd +Gitlab-Sv: api-gke-us-east1-c +Link: ; rel="next", ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 11 +Ratelimit-Remaining: 1989 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee850a9ac27f-VIE","version":"1"} +X-Next-Page: 2 +X-Page: 1 +X-Per-Page: 2 +X-Prev-Page: +X-Runtime: 0.079850 +X-Total: 6 +X-Total-Pages: 3 + +[{"id":3009627,"name":"thumbsup","user":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:46:42.657Z","updated_at":"2019-11-28T08:46:42.657Z","awardable_id":27687706,"awardable_type":"Issue","url":null},{"id":3009628,"name":"thumbsdown","user":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:46:43.471Z","updated_at":"2019-11-28T08:46:43.471Z","awardable_id":27687706,"awardable_type":"Issue","url":null}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Faward_emoji%3Fpage%3D2%26per_page%3D2 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Faward_emoji%3Fpage%3D2%26per_page%3D2 new file mode 100644 index 00000000000..d15f53ca553 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Faward_emoji%3Fpage%3D2%26per_page%3D2 @@ -0,0 +1,28 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"3330f45ad310a47e7d50a94586f0384b" +Gitlab-Lb: haproxy-main-53-lb-gprd +Gitlab-Sv: api-gke-us-east1-c +Link: ; rel="prev", ; rel="next", ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 12 +Ratelimit-Remaining: 1988 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee86fcb5c27f-VIE","version":"1"} +X-Next-Page: 3 +X-Page: 2 +X-Per-Page: 2 +X-Prev-Page: 1 +X-Runtime: 0.073056 +X-Total: 6 +X-Total-Pages: 3 + +[{"id":3009632,"name":"laughing","user":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:47:14.381Z","updated_at":"2019-11-28T08:47:14.381Z","awardable_id":27687706,"awardable_type":"Issue","url":null},{"id":3009634,"name":"tada","user":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:47:18.254Z","updated_at":"2019-11-28T08:47:18.254Z","awardable_id":27687706,"awardable_type":"Issue","url":null}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Faward_emoji%3Fpage%3D3%26per_page%3D2 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Faward_emoji%3Fpage%3D3%26per_page%3D2 new file mode 100644 index 00000000000..2932e00ac43 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Faward_emoji%3Fpage%3D3%26per_page%3D2 @@ -0,0 +1,28 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"1b206b5cf267607532b738fd49085d11" +Gitlab-Lb: haproxy-main-20-lb-gprd +Gitlab-Sv: api-gke-us-east1-c +Link: ; rel="prev", ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 13 +Ratelimit-Remaining: 1987 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee88de9fc27f-VIE","version":"1"} +X-Next-Page: +X-Page: 3 +X-Per-Page: 2 +X-Prev-Page: 2 +X-Runtime: 0.079463 +X-Total: 6 +X-Total-Pages: 3 + +[{"id":3009636,"name":"confused","user":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:47:27.248Z","updated_at":"2019-11-28T08:47:27.248Z","awardable_id":27687706,"awardable_type":"Issue","url":null},{"id":3009640,"name":"hearts","user":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:47:33.059Z","updated_at":"2019-11-28T08:47:33.059Z","awardable_id":27687706,"awardable_type":"Issue","url":null}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Faward_emoji%3Fpage%3D4%26per_page%3D2 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Faward_emoji%3Fpage%3D4%26per_page%3D2 new file mode 100644 index 00000000000..46669b631bf --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Faward_emoji%3Fpage%3D4%26per_page%3D2 @@ -0,0 +1,30 @@ +Accept-Ranges: bytes +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +Gitlab-Lb: haproxy-main-60-lb-gprd +Gitlab-Sv: api-gke-us-east1-d +Link: ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 14 +Ratelimit-Remaining: 1986 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee8aa8bac27f-VIE","version":"1"} +X-Next-Page: +X-Page: 4 +X-Per-Page: 2 +X-Prev-Page: +X-Runtime: 0.073394 +X-Total: 6 +X-Total-Pages: 3 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Fdiscussions%3Fpage%3D1%26per_page%3D100 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Fdiscussions%3Fpage%3D1%26per_page%3D100 new file mode 100644 index 00000000000..dc3ff6f3c86 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Fdiscussions%3Fpage%3D1%26per_page%3D100 @@ -0,0 +1,28 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"3a60a8124f040aa4a8458c91b49f4b0f" +Gitlab-Lb: haproxy-main-29-lb-gprd +Gitlab-Sv: api-gke-us-east1-c +Link: ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 15 +Ratelimit-Remaining: 1985 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee8c4a95c27f-VIE","version":"1"} +X-Next-Page: +X-Page: 1 +X-Per-Page: 100 +X-Prev-Page: +X-Runtime: 0.170698 +X-Total: 4 +X-Total-Pages: 1 + +[{"id":"617967369d98d8b73b6105a40318fe839f931a24","individual_note":true,"resolvable":false,"notes":[{"id":251637434,"type":null,"body":"This is a comment","author":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:44:52.501Z","updated_at":"2019-11-28T08:44:52.501Z","system":false,"noteable_id":27687706,"noteable_type":"Issue","project_id":15578026,"resolvable":false,"confidential":false,"internal":false,"imported":false,"imported_from":"none","noteable_iid":2,"commands_changes":{}}]},{"id":"b92d74daee411a17d844041bcd3c267ade58f680","individual_note":true,"resolvable":false,"notes":[{"id":251637528,"type":null,"body":"changed milestone to %2","author":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:45:02.329Z","updated_at":"2019-11-28T08:45:02.335Z","system":true,"noteable_id":27687706,"noteable_type":"Issue","project_id":15578026,"resolvable":false,"confidential":false,"internal":false,"imported":false,"imported_from":"none","noteable_iid":2,"commands_changes":{}}]},{"id":"6010f567d2b58758ef618070372c97891ac75349","individual_note":true,"resolvable":false,"notes":[{"id":251637892,"type":null,"body":"closed","author":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:45:45.007Z","updated_at":"2019-11-28T08:45:45.010Z","system":true,"noteable_id":27687706,"noteable_type":"Issue","project_id":15578026,"resolvable":false,"confidential":false,"internal":false,"imported":false,"imported_from":"none","noteable_iid":2,"commands_changes":{}}]},{"id":"632d0cbfd6a1a08f38aaf9ef7715116f4b188ebb","individual_note":true,"resolvable":false,"notes":[{"id":251637999,"type":null,"body":"A second comment","author":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"created_at":"2019-11-28T08:45:53.501Z","updated_at":"2019-11-28T08:45:53.501Z","system":false,"noteable_id":27687706,"noteable_type":"Issue","project_id":15578026,"resolvable":false,"confidential":false,"internal":false,"imported":false,"imported_from":"none","noteable_iid":2,"commands_changes":{}}]}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Fresource_state_events%3Fpage%3D1%26per_page%3D100 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Fresource_state_events%3Fpage%3D1%26per_page%3D100 new file mode 100644 index 00000000000..6ddcf3fa6d2 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%2F2%2Fresource_state_events%3Fpage%3D1%26per_page%3D100 @@ -0,0 +1,30 @@ +Accept-Ranges: bytes +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +Gitlab-Lb: haproxy-main-60-lb-gprd +Gitlab-Sv: api-gke-us-east1-d +Link: ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 16 +Ratelimit-Remaining: 1984 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee8ead54c27f-VIE","version":"1"} +X-Next-Page: +X-Page: 1 +X-Per-Page: 100 +X-Prev-Page: +X-Runtime: 0.095668 +X-Total: 0 +X-Total-Pages: 1 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%3Fpage%3D1%26per_page%3D2%26sort%3Dasc%26state%3Dall b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%3Fpage%3D1%26per_page%3D2%26sort%3Dasc%26state%3Dall new file mode 100644 index 00000000000..7df08ca458e --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fissues%3Fpage%3D1%26per_page%3D2%26sort%3Dasc%26state%3Dall @@ -0,0 +1,28 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"b12dae3f9a06df734eaeea56b1becb28" +Gitlab-Lb: haproxy-main-09-lb-gprd +Gitlab-Sv: api-gke-us-east1-d +Link: ; rel="next", ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 8 +Ratelimit-Remaining: 1992 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee7f8c9ac27f-VIE","version":"1"} +X-Next-Page: 2 +X-Page: 1 +X-Per-Page: 2 +X-Prev-Page: +X-Runtime: 0.141229 +X-Total: 3 +X-Total-Pages: 2 + +[{"id":27687675,"iid":1,"project_id":15578026,"title":"Please add an animated gif icon to the merge button","description":"I just want the merge button to hurt my eyes a little. :stuck_out_tongue_closed_eyes:","state":"closed","created_at":"2019-11-28T08:43:35.459Z","updated_at":"2019-11-28T08:46:23.304Z","closed_at":"2019-11-28T08:46:23.275Z","closed_by":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"labels":["bug","discussion"],"milestone":{"id":1082926,"iid":1,"project_id":15578026,"title":"1.0.0","description":"","state":"closed","created_at":"2019-11-28T08:42:30.301Z","updated_at":"2019-11-28T15:57:52.401Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/1"},"assignees":[],"author":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"type":"ISSUE","assignee":null,"user_notes_count":0,"merge_requests_count":0,"upvotes":1,"downvotes":0,"start_date":null,"due_date":null,"confidential":false,"discussion_locked":null,"issue_type":"issue","web_url":"https://gitlab.com/gitea/test_repo/-/work_items/1","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"task_completion_status":{"count":0,"completed_count":0},"blocking_issues_count":0,"has_tasks":true,"task_status":"0 of 0 checklist items completed","_links":{"self":"https://gitlab.com/api/v4/projects/15578026/issues/1","notes":"https://gitlab.com/api/v4/projects/15578026/issues/1/notes","award_emoji":"https://gitlab.com/api/v4/projects/15578026/issues/1/award_emoji","project":"https://gitlab.com/api/v4/projects/15578026","closed_as_duplicate_of":null},"references":{"short":"#1","relative":"#1","full":"gitea/test_repo#1"},"severity":"UNKNOWN","moved_to_id":null,"imported":false,"imported_from":"none","service_desk_reply_to":null},{"id":27687706,"iid":2,"project_id":15578026,"title":"Test issue","description":"This is test issue 2, do not touch!","state":"closed","created_at":"2019-11-28T08:44:46.277Z","updated_at":"2019-11-28T08:45:44.987Z","closed_at":"2019-11-28T08:45:44.959Z","closed_by":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"labels":["duplicate"],"milestone":{"id":1082927,"iid":2,"project_id":15578026,"title":"1.1.0","description":"","state":"active","created_at":"2019-11-28T08:42:44.575Z","updated_at":"2019-11-28T08:42:44.575Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/2"},"assignees":[],"author":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"type":"ISSUE","assignee":null,"user_notes_count":2,"merge_requests_count":0,"upvotes":1,"downvotes":1,"start_date":null,"due_date":null,"confidential":false,"discussion_locked":null,"issue_type":"issue","web_url":"https://gitlab.com/gitea/test_repo/-/work_items/2","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"task_completion_status":{"count":0,"completed_count":0},"blocking_issues_count":0,"has_tasks":true,"task_status":"0 of 0 checklist items completed","_links":{"self":"https://gitlab.com/api/v4/projects/15578026/issues/2","notes":"https://gitlab.com/api/v4/projects/15578026/issues/2/notes","award_emoji":"https://gitlab.com/api/v4/projects/15578026/issues/2/award_emoji","project":"https://gitlab.com/api/v4/projects/15578026","closed_as_duplicate_of":null},"references":{"short":"#2","relative":"#2","full":"gitea/test_repo#2"},"severity":"UNKNOWN","moved_to_id":null,"imported":false,"imported_from":"none","service_desk_reply_to":null}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Flabels%3Fpage%3D1%26per_page%3D100 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Flabels%3Fpage%3D1%26per_page%3D100 new file mode 100644 index 00000000000..f5f8643b88b --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Flabels%3Fpage%3D1%26per_page%3D100 @@ -0,0 +1,28 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"d7c1b7d1a56d73d88c746a4e1241b673" +Gitlab-Lb: haproxy-main-32-lb-gprd +Gitlab-Sv: api-gke-us-east1-c +Link: ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 6 +Ratelimit-Remaining: 1994 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee7bc888c27f-VIE","version":"1"} +X-Next-Page: +X-Page: 1 +X-Per-Page: 100 +X-Prev-Page: +X-Runtime: 0.104606 +X-Total: 9 +X-Total-Pages: 1 + +[{"id":12959095,"name":"bug","description":null,"text_color":"#FFFFFF","description_html":"","color":"#d9534f","archived":false,"subscribed":false,"priority":null,"is_project_label":true},{"id":12959097,"name":"confirmed","description":null,"text_color":"#FFFFFF","description_html":"","color":"#d9534f","archived":false,"subscribed":false,"priority":null,"is_project_label":true},{"id":12959096,"name":"critical","description":null,"text_color":"#FFFFFF","description_html":"","color":"#d9534f","archived":false,"subscribed":false,"priority":null,"is_project_label":true},{"id":12959100,"name":"discussion","description":null,"text_color":"#FFFFFF","description_html":"","color":"#428bca","archived":false,"subscribed":false,"priority":null,"is_project_label":true},{"id":12959098,"name":"documentation","description":null,"text_color":"#1F1E24","description_html":"","color":"#f0ad4e","archived":false,"subscribed":false,"priority":null,"is_project_label":true},{"id":12959554,"name":"duplicate","description":null,"text_color":"#FFFFFF","description_html":"","color":"#7F8C8D","archived":false,"subscribed":false,"priority":null,"is_project_label":true},{"id":12959102,"name":"enhancement","description":null,"text_color":"#FFFFFF","description_html":"","color":"#5cb85c","archived":false,"subscribed":false,"priority":null,"is_project_label":true},{"id":12959101,"name":"suggestion","description":null,"text_color":"#FFFFFF","description_html":"","color":"#428bca","archived":false,"subscribed":false,"priority":null,"is_project_label":true},{"id":12959099,"name":"support","description":null,"text_color":"#1F1E24","description_html":"","color":"#f0ad4e","archived":false,"subscribed":false,"priority":null,"is_project_label":true}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F1%2Fapprovals b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F1%2Fapprovals new file mode 100644 index 00000000000..4b0c4f6b33c --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F1%2Fapprovals @@ -0,0 +1,21 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"03275c2a6cc732959835fa9ad779f0ae" +Gitlab-Lb: haproxy-main-14-lb-gprd +Gitlab-Sv: api-gke-us-east1-c +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 22 +Ratelimit-Remaining: 1978 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee9a6be9c27f-VIE","version":"1"} +X-Runtime: 0.110926 + +{"id":43486906,"iid":1,"project_id":15578026,"title":"Update README.md","description":"add warning to readme","state":"merged","created_at":"2019-11-28T08:54:41.034Z","updated_at":"2019-11-28T16:02:08.377Z","merge_status":"can_be_merged","approved":true,"approvals_required":0,"approvals_left":0,"require_password_to_approve":false,"approved_by":[{"user":{"id":527793,"username":"axifive","public_email":"","name":"Alexey Terentyev","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/b5eee878c9129969b55d221a823fd15e55aad8dc15d521f4170e3c93728e02b6?s=80\u0026d=identicon","web_url":"https://gitlab.com/axifive"},"approved_at":"2019-11-28T12:58:33.257Z"},{"user":{"id":4102996,"username":"zeripath","public_email":"","name":"zeripath","state":"active","locked":false,"avatar_url":"https://secure.gravatar.com/avatar/3bad2cdad37aa0bbb3ad276ce8f77e32a1a9567a7083f0866d8df8ed0e92e5b5?s=80\u0026d=identicon","web_url":"https://gitlab.com/zeripath"},"approved_at":"2019-11-28T13:10:47.321Z"}],"suggested_approvers":[],"approvers":[],"approver_groups":[],"user_has_approved":false,"user_can_approve":false,"approval_rules_left":[],"has_approval_rules":true,"merge_request_approvers_available":false,"multiple_approval_rules_available":false,"invalid_approvers_rules":[]} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F2%2Fapprovals b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F2%2Fapprovals new file mode 100644 index 00000000000..64778374d79 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F2%2Fapprovals @@ -0,0 +1,21 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"7b17d8be4001a23ef2e3265138046ebd" +Gitlab-Lb: haproxy-main-36-lb-gprd +Gitlab-Sv: api-gke-us-east1-d +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 23 +Ratelimit-Remaining: 1977 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee9c6e8ac27f-VIE","version":"1"} +X-Runtime: 0.178166 + +{"id":43524600,"iid":2,"project_id":15578026,"title":"Test branch","description":"do not merge this PR","state":"opened","created_at":"2019-11-28T15:56:54.104Z","updated_at":"2020-04-19T19:24:21.108Z","merge_status":"can_be_merged","approved":true,"approvals_required":0,"approvals_left":0,"require_password_to_approve":false,"approved_by":[{"user":{"id":4575606,"username":"real6543","public_email":"","name":"6543","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/4575606/avatar.png","web_url":"https://gitlab.com/real6543"},"approved_at":"2020-04-19T19:24:21.089Z"}],"suggested_approvers":[],"approvers":[],"approver_groups":[{"group":{"id":3181312,"web_url":"https://gitlab.com/groups/gitea","name":"gitea","path":"gitea","description":"Mirror of Gitea source code repositories","visibility":"public","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"maintainer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":false,"emails_enabled":true,"show_diff_preview_in_email":true,"mentions_disabled":null,"lfs_enabled":true,"archived":false,"math_rendering_limits_enabled":true,"lock_math_rendering_limits_enabled":false,"default_branch":null,"default_branch_protection":2,"default_branch_protection_defaults":{"allowed_to_push":[{"access_level":40}],"allow_force_push":false,"allowed_to_merge":[{"access_level":40}]},"avatar_url":"https://gitlab.com/uploads/-/system/group/avatar/3181312/gitea.png","request_access_enabled":true,"full_name":"gitea","full_path":"gitea","created_at":"2018-07-04T16:32:10.176Z","parent_id":null,"organization_id":1,"shared_runners_setting":"enabled","max_artifacts_size":null,"marked_for_deletion_on":null,"ldap_cn":null,"ldap_access":null,"wiki_access_level":"enabled"}}],"user_has_approved":false,"user_can_approve":false,"approval_rules_left":[],"has_approval_rules":true,"merge_request_approvers_available":false,"multiple_approval_rules_available":false,"invalid_approvers_rules":[]} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F3 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F3 new file mode 100644 index 00000000000..6ccdcb02440 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F3 @@ -0,0 +1,21 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"e083f0fed8ec0f3bd66c07010daf4918" +Gitlab-Lb: haproxy-main-45-lb-gprd +Gitlab-Sv: api-gke-us-east1-d +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 20 +Ratelimit-Remaining: 1980 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee964ec6c27f-VIE","version":"1"} +X-Runtime: 0.140863 + +{"id":324656914,"iid":3,"project_id":15578026,"title":"Test branch","description":"do not merge this PR","state":"closed","created_at":"2024-09-03T07:52:08.078Z","updated_at":"2024-09-03T08:09:34.155Z","merged_by":null,"merge_user":null,"merged_at":null,"closed_by":{"id":2005797,"username":"oliverpool","public_email":"","name":"oliverpool","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/2005797/avatar.png","web_url":"https://gitlab.com/oliverpool"},"closed_at":"2024-09-03T07:52:28.488Z","target_branch":"master","source_branch":"feat/test","user_notes_count":1,"upvotes":1,"downvotes":0,"author":{"id":2005797,"username":"oliverpool","public_email":"","name":"oliverpool","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/2005797/avatar.png","web_url":"https://gitlab.com/oliverpool"},"assignees":[],"assignee":null,"reviewers":[],"source_project_id":61363672,"target_project_id":15578026,"labels":[],"draft":false,"imported":false,"imported_from":"none","work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","detailed_merge_status":"not_open","merge_after":null,"sha":"9f733b96b98a4175276edf6a2e1231489c3bdd23","merge_commit_sha":null,"squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":true,"prepared_at":"2024-09-03T08:09:34.153Z","allow_collaboration":true,"allow_maintainer_to_push":true,"reference":"!3","references":{"short":"!3","relative":"!3","full":"gitea/test_repo!3"},"web_url":"https://gitlab.com/gitea/test_repo/-/merge_requests/3","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"squash_on_merge":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null,"subscribed":false,"changes_count":"1","latest_build_started_at":null,"latest_build_finished_at":null,"first_deployed_to_production_at":null,"pipeline":null,"head_pipeline":null,"diff_refs":{"base_sha":"c59c9b451acca9d106cc19d61d87afe3fbbb8b83","head_sha":"9f733b96b98a4175276edf6a2e1231489c3bdd23","start_sha":"c59c9b451acca9d106cc19d61d87afe3fbbb8b83"},"merge_error":null,"first_contribution":true,"user":{"can_merge":false}} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F3%2Faward_emoji%3Fpage%3D1%26per_page%3D2 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F3%2Faward_emoji%3Fpage%3D1%26per_page%3D2 new file mode 100644 index 00000000000..4a748d30b26 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F3%2Faward_emoji%3Fpage%3D1%26per_page%3D2 @@ -0,0 +1,28 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"ef98d22b26e55b4da238d1ae4fc66140" +Gitlab-Lb: haproxy-main-58-lb-gprd +Gitlab-Sv: api-gke-us-east1-b +Link: ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 21 +Ratelimit-Remaining: 1979 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee98da1fc27f-VIE","version":"1"} +X-Next-Page: +X-Page: 1 +X-Per-Page: 2 +X-Prev-Page: +X-Runtime: 0.057694 +X-Total: 1 +X-Total-Pages: 1 + +[{"id":28081326,"name":"thumbsup","user":{"id":2005797,"username":"oliverpool","public_email":"","name":"oliverpool","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/2005797/avatar.png","web_url":"https://gitlab.com/oliverpool"},"created_at":"2024-09-03T07:52:13.111Z","updated_at":"2024-09-03T07:52:13.111Z","awardable_id":324656914,"awardable_type":"MergeRequest","url":null}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F4 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F4 new file mode 100644 index 00000000000..f05e6ab4324 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F4 @@ -0,0 +1,21 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"b0504885540d26b31a16be8421368581" +Gitlab-Lb: haproxy-main-15-lb-gprd +Gitlab-Sv: api-gke-us-east1-d +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 18 +Ratelimit-Remaining: 1982 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee927a8ac27f-VIE","version":"1"} +X-Runtime: 0.146911 + +{"id":435554087,"iid":4,"project_id":15578026,"title":"Test/parsing","description":"This MR was created in error, feel free to delete when convenient. Sorry for the noise.","state":"closed","created_at":"2025-11-25T09:21:42.628Z","updated_at":"2025-11-25T13:46:43.471Z","merged_by":null,"merge_user":null,"merged_at":null,"closed_by":{"id":10529876,"username":"patdyn","public_email":"","name":"Pat Dyn","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/10529876/avatar.png","web_url":"https://gitlab.com/patdyn"},"closed_at":"2025-11-25T09:43:14.581Z","target_branch":"master","source_branch":"test/parsing","user_notes_count":0,"upvotes":0,"downvotes":0,"author":{"id":10529876,"username":"patdyn","public_email":"","name":"Pat Dyn","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/10529876/avatar.png","web_url":"https://gitlab.com/patdyn"},"assignees":[],"assignee":null,"reviewers":[],"source_project_id":61363672,"target_project_id":15578026,"labels":[],"draft":false,"imported":false,"imported_from":"none","work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"cannot_be_merged","detailed_merge_status":"not_open","merge_after":null,"sha":"c59c9b451acca9d106cc19d61d87afe3fbbb8b83","merge_commit_sha":null,"squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":true,"prepared_at":"2025-11-25T09:21:43.464Z","allow_collaboration":true,"allow_maintainer_to_push":true,"reference":"!4","references":{"short":"!4","relative":"!4","full":"gitea/test_repo!4"},"web_url":"https://gitlab.com/gitea/test_repo/-/merge_requests/4","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"squash_on_merge":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":true,"blocking_discussions_resolved":true,"approvals_before_merge":null,"subscribed":false,"changes_count":null,"latest_build_started_at":null,"latest_build_finished_at":null,"first_deployed_to_production_at":null,"pipeline":null,"head_pipeline":null,"diff_refs":{"base_sha":"c59c9b451acca9d106cc19d61d87afe3fbbb8b83","head_sha":"c59c9b451acca9d106cc19d61d87afe3fbbb8b83","start_sha":"c59c9b451acca9d106cc19d61d87afe3fbbb8b83"},"merge_error":null,"first_contribution":true,"user":{"can_merge":false}} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F4%2Faward_emoji%3Fpage%3D1%26per_page%3D2 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F4%2Faward_emoji%3Fpage%3D1%26per_page%3D2 new file mode 100644 index 00000000000..fd860f4e216 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%2F4%2Faward_emoji%3Fpage%3D1%26per_page%3D2 @@ -0,0 +1,30 @@ +Accept-Ranges: bytes +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Length: 2 +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" +Gitlab-Lb: haproxy-main-12-lb-gprd +Gitlab-Sv: gke-cny-api +Link: ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 19 +Ratelimit-Remaining: 1981 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee949d06c27f-VIE","version":"1"} +X-Next-Page: +X-Page: 1 +X-Per-Page: 2 +X-Prev-Page: +X-Runtime: 0.070976 +X-Total: 0 +X-Total-Pages: 1 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%3Fpage%3D1%26per_page%3D2%26view%3Dsimple b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%3Fpage%3D1%26per_page%3D2%26view%3Dsimple new file mode 100644 index 00000000000..993d0b66bcf --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmerge_requests%3Fpage%3D1%26per_page%3D2%26view%3Dsimple @@ -0,0 +1,28 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"523433e2f02ff01da9306c97204424c2" +Gitlab-Lb: haproxy-main-21-lb-gprd +Gitlab-Sv: api-gke-us-east1-d +Link: ; rel="next", ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 17 +Ratelimit-Remaining: 1983 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee906802c27f-VIE","version":"1"} +X-Next-Page: 2 +X-Page: 1 +X-Per-Page: 2 +X-Prev-Page: +X-Runtime: 0.079335 +X-Total: 4 +X-Total-Pages: 2 + +[{"id":435554087,"iid":4,"project_id":15578026,"title":"Test/parsing","description":"This MR was created in error, feel free to delete when convenient. Sorry for the noise.","state":"closed","created_at":"2025-11-25T09:21:42.628Z","updated_at":"2025-11-25T13:46:43.471Z","web_url":"https://gitlab.com/gitea/test_repo/-/merge_requests/4"},{"id":324656914,"iid":3,"project_id":15578026,"title":"Test branch","description":"do not merge this PR","state":"closed","created_at":"2024-09-03T07:52:08.078Z","updated_at":"2024-09-03T08:09:34.155Z","web_url":"https://gitlab.com/gitea/test_repo/-/merge_requests/3"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmilestones%3Fpage%3D1%26per_page%3D100%26state%3Dall b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmilestones%3Fpage%3D1%26per_page%3D100%26state%3Dall new file mode 100644 index 00000000000..0a5caf5e116 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Fmilestones%3Fpage%3D1%26per_page%3D100%26state%3Dall @@ -0,0 +1,28 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"c8e2d3a5f05ee29c58b665c86684f9f9" +Gitlab-Lb: haproxy-main-47-lb-gprd +Gitlab-Sv: api-gke-us-east1-c +Link: ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 5 +Ratelimit-Remaining: 1995 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee7a2e97c27f-VIE","version":"1"} +X-Next-Page: +X-Page: 1 +X-Per-Page: 100 +X-Prev-Page: +X-Runtime: 0.070685 +X-Total: 2 +X-Total-Pages: 1 + +[{"id":1082927,"iid":2,"project_id":15578026,"title":"1.1.0","description":"","state":"active","created_at":"2019-11-28T08:42:44.575Z","updated_at":"2019-11-28T08:42:44.575Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/2"},{"id":1082926,"iid":1,"project_id":15578026,"title":"1.0.0","description":"","state":"closed","created_at":"2019-11-28T08:42:30.301Z","updated_at":"2019-11-28T15:57:52.401Z","due_date":null,"start_date":null,"expired":false,"web_url":"https://gitlab.com/gitea/test_repo/-/milestones/1"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Freleases%3Fpage%3D1%26per_page%3D100 b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Freleases%3Fpage%3D1%26per_page%3D100 new file mode 100644 index 00000000000..5735af83970 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2F15578026%2Freleases%3Fpage%3D1%26per_page%3D100 @@ -0,0 +1,28 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"5c4c02bdd0b9515c20be9a3c92cd45f1" +Gitlab-Lb: haproxy-main-43-lb-gprd +Gitlab-Sv: api-gke-us-east1-b +Link: ; rel="first", ; rel="last" +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 7 +Ratelimit-Remaining: 1993 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee7daa8cc27f-VIE","version":"1"} +X-Next-Page: +X-Page: 1 +X-Per-Page: 100 +X-Prev-Page: +X-Runtime: 0.097425 +X-Total: 1 +X-Total-Pages: 1 + +[{"name":"First Release","tag_name":"v0.9.99","description":"A test release","created_at":"2019-11-28T09:09:48.840Z","released_at":"2019-11-28T09:09:48.836Z","upcoming_release":false,"author":{"id":1241334,"username":"lafriks","public_email":"","name":"Lauris BH","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1241334/avatar.png","web_url":"https://gitlab.com/lafriks"},"commit":{"id":"0720a3ec57c1f843568298117b874319e7deee75","short_id":"0720a3ec","created_at":"2019-11-28T08:49:16.000+00:00","parent_ids":["93ea21ce45d35690c35e80961d239645139e872c"],"title":"Add new file","message":"Add new file","author_name":"Lauris BH","author_email":"lauris@nix.lv","authored_date":"2019-11-28T08:49:16.000+00:00","committer_name":"Lauris BH","committer_email":"lauris@nix.lv","committed_date":"2019-11-28T08:49:16.000+00:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/gitea/test_repo/-/commit/0720a3ec57c1f843568298117b874319e7deee75"},"commit_path":"/gitea/test_repo/-/commit/0720a3ec57c1f843568298117b874319e7deee75","tag_path":"/gitea/test_repo/-/tags/v0.9.99","assets":{"count":4,"sources":[{"format":"zip","url":"https://gitlab.com/gitea/test_repo/-/archive/v0.9.99/test_repo-v0.9.99.zip"},{"format":"tar.gz","url":"https://gitlab.com/gitea/test_repo/-/archive/v0.9.99/test_repo-v0.9.99.tar.gz"},{"format":"tar.bz2","url":"https://gitlab.com/gitea/test_repo/-/archive/v0.9.99/test_repo-v0.9.99.tar.bz2"},{"format":"tar","url":"https://gitlab.com/gitea/test_repo/-/archive/v0.9.99/test_repo-v0.9.99.tar"}],"links":[]},"evidences":[{"sha":"89f1223473ee01f192a83d0cb89f4d1eac1de74f01ad","filepath":"https://gitlab.com/gitea/test_repo/-/releases/v0.9.99/evidences/52147.json","collected_at":"2019-11-28T09:09:48.888Z"}],"_links":{"closed_issues_url":"https://gitlab.com/gitea/test_repo/-/issues?release_tag=v0.9.99\u0026scope=all\u0026state=closed","closed_merge_requests_url":"https://gitlab.com/gitea/test_repo/-/merge_requests?release_tag=v0.9.99\u0026scope=all\u0026state=closed","merged_merge_requests_url":"https://gitlab.com/gitea/test_repo/-/merge_requests?release_tag=v0.9.99\u0026scope=all\u0026state=merged","opened_issues_url":"https://gitlab.com/gitea/test_repo/-/issues?release_tag=v0.9.99\u0026scope=all\u0026state=opened","opened_merge_requests_url":"https://gitlab.com/gitea/test_repo/-/merge_requests?release_tag=v0.9.99\u0026scope=all\u0026state=opened","self":"https://gitlab.com/gitea/test_repo/-/releases/v0.9.99"}}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2Fgitea%252Ftest_repo b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2Fgitea%252Ftest_repo new file mode 100644 index 00000000000..a0597181dd3 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fprojects%2Fgitea%252Ftest_repo @@ -0,0 +1,21 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"4b614e9f590cbbbc47e0f5a026615034" +Gitlab-Lb: haproxy-main-01-lb-gprd +Gitlab-Sv: api-gke-us-east1-b +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 2 +Ratelimit-Remaining: 1998 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee728f28c27f-VIE","version":"1"} +X-Runtime: 0.320807 + +{"id":15578026,"description":"Test repository for testing migration from gitlab to gitea","name":"test_repo","name_with_namespace":"gitea / test_repo","path":"test_repo","path_with_namespace":"gitea/test_repo","created_at":"2019-11-28T08:20:33.019Z","default_branch":"master","tag_list":["migration","test"],"topics":["migration","test"],"ssh_url_to_repo":"git@gitlab.com:gitea/test_repo.git","http_url_to_repo":"https://gitlab.com/gitea/test_repo.git","web_url":"https://gitlab.com/gitea/test_repo","readme_url":"https://gitlab.com/gitea/test_repo/-/blob/master/README.md","forks_count":1,"avatar_url":null,"star_count":0,"last_activity_at":"2025-11-25T09:21:43.130Z","visibility":"public","namespace":{"id":3181312,"name":"gitea","path":"gitea","kind":"group","full_path":"gitea","parent_id":null,"avatar_url":"/uploads/-/system/group/avatar/3181312/gitea.png","web_url":"https://gitlab.com/groups/gitea"},"container_registry_image_prefix":"registry.gitlab.com/gitea/test_repo","_links":{"self":"https://gitlab.com/api/v4/projects/15578026","issues":"https://gitlab.com/api/v4/projects/15578026/issues","merge_requests":"https://gitlab.com/api/v4/projects/15578026/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/15578026/repository/branches","labels":"https://gitlab.com/api/v4/projects/15578026/labels","events":"https://gitlab.com/api/v4/projects/15578026/events","members":"https://gitlab.com/api/v4/projects/15578026/members","cluster_agents":"https://gitlab.com/api/v4/projects/15578026/cluster_agents"},"marked_for_deletion_at":null,"marked_for_deletion_on":null,"packages_enabled":true,"empty_repo":false,"archived":false,"resolve_outdated_diff_discussions":false,"repository_object_format":"sha1","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"container_registry_enabled":true,"service_desk_enabled":true,"can_create_merge_request_in":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"enabled","analytics_access_level":"enabled","container_registry_access_level":"enabled","security_and_compliance_access_level":"private","releases_access_level":"enabled","environments_access_level":"enabled","feature_flags_access_level":"enabled","infrastructure_access_level":"enabled","monitor_access_level":"enabled","model_experiments_access_level":"enabled","model_registry_access_level":"enabled","package_registry_access_level":"public","emails_disabled":false,"emails_enabled":true,"show_diff_preview_in_email":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":1241334,"import_status":"none","open_issues_count":0,"description_html":"\u003cp data-sourcepos=\"1:1-1:58\" dir=\"auto\"\u003eTest repository for testing migration from gitlab to gitea\u003c/p\u003e","updated_at":"2025-11-25T09:21:43.130Z","ci_config_path":null,"public_jobs":true,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"allow_merge_on_skipped_pipeline":null,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":true,"printing_merge_request_link_enabled":true,"merge_method":"ff","squash_option":"default_off","enforce_auth_checks_on_uploads":true,"suggestion_commit_message":null,"merge_commit_template":null,"squash_commit_template":null,"issue_branch_template":null,"warn_about_potentially_unwanted_characters":true,"autoclose_referenced_issues":true,"max_artifacts_size":null,"external_authorization_classification_label":"","requirements_enabled":false,"requirements_access_level":"enabled","security_and_compliance_enabled":false,"compliance_frameworks":[],"permissions":{"project_access":null,"group_access":null}} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fversion b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fversion new file mode 100644 index 00000000000..4ac49026c57 --- /dev/null +++ b/services/migrations/_mock_data/TestGitlabDownloadRepo/GET_%2Fapi%2Fv4%2Fversion @@ -0,0 +1,21 @@ +Cache-Control: max-age=0, private, must-revalidate +Cf-Cache-Status: MISS +Content-Security-Policy: default-src 'none' +Content-Type: application/json +Etag: W/"68fb72a6f737a013c4a1e293abfb186f" +Gitlab-Lb: haproxy-main-50-lb-gprd +Gitlab-Sv: api-gke-us-east1-c +Ratelimit-Limit: 2000 +Ratelimit-Name: throttle_authenticated_api +Ratelimit-Observed: 1 +Ratelimit-Remaining: 1999 +Ratelimit-Reset: 1776937500 +Referrer-Policy: strict-origin-when-cross-origin +Strict-Transport-Security: max-age=31536000 +Vary: Origin, Accept-Encoding +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gitlab-Meta: {"correlation_id":"9f0bee70fdcfc27f-VIE","version":"1"} +X-Runtime: 0.046078 + +{"version":"19.0.0-pre","revision":"3b95b4c0792","kas":{"enabled":true,"externalUrl":"wss://kas.gitlab.com","externalK8sProxyUrl":"https://kas.gitlab.com/k8s-proxy","version":"19.0.0-rc1+5cc4df2b2383c124b4a7501896f19c7df7a147cf"},"enterprise":true} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO b/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO new file mode 100644 index 00000000000..61d77fbc9c1 --- /dev/null +++ b/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO @@ -0,0 +1,3 @@ +Content-Type: application/json; charset=UTF-8 + +{"id":1,"owner":{"id":5331,"login":"lunny","full_name":"","email":"xiaolunwen@gmail.com","avatar_url":"https://try.gogs.io/avatars/5331"},"name":"TESTREPO","full_name":"lunnytest/TESTREPO","description":"","private":false,"fork":false,"parent":null,"empty":false,"mirror":false,"size":0,"html_url":"https://try.gogs.io/lunnytest/TESTREPO","ssh_url":"git@try.gogs.io:lunnytest/TESTREPO.git","clone_url":"https://try.gogs.io/lunnytest/TESTREPO.git","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":1,"default_branch":"master","created_at":"2019-06-11T08:15:00Z","updated_at":"2019-06-11T08:15:00Z","permissions":{"admin":false,"push":false,"pull":true}} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO%2Fissues%2F1%2Fcomments b/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO%2Fissues%2F1%2Fcomments new file mode 100644 index 00000000000..814fb999870 --- /dev/null +++ b/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO%2Fissues%2F1%2Fcomments @@ -0,0 +1,3 @@ +Content-Type: application/json; charset=UTF-8 + +[{"id":1,"user":{"id":5331,"login":"lunny","full_name":"","email":"xiaolunwen@gmail.com","avatar_url":"https://try.gogs.io/avatars/5331"},"body":"1111","created_at":"2019-06-11T08:19:50Z","updated_at":"2019-06-11T08:19:50Z"},{"id":2,"user":{"id":15822,"login":"clacplouf","full_name":"","email":"test1234@dbn.re","avatar_url":"https://try.gogs.io/avatars/15822"},"body":"88888888","created_at":"2019-10-26T11:07:02Z","updated_at":"2019-10-26T11:07:02Z"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO%2Fissues%3Fpage%3D1%26state%3Dopen b/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO%2Fissues%3Fpage%3D1%26state%3Dopen new file mode 100644 index 00000000000..5923e6e761a --- /dev/null +++ b/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO%2Fissues%3Fpage%3D1%26state%3Dopen @@ -0,0 +1,3 @@ +Content-Type: application/json; charset=UTF-8 + +[{"id":1,"number":1,"user":{"id":5331,"login":"lunny","full_name":"","email":"xiaolunwen@gmail.com","avatar_url":"https://try.gogs.io/avatars/5331"},"title":"test","body":"test","labels":[{"id":1,"name":"bug","color":"ee0701"}],"milestone":null,"assignee":null,"state":"open","comments":2,"created_at":"2019-06-11T08:16:44Z","updated_at":"2019-10-26T11:07:02Z","pull_request":null}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO%2Flabels b/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO%2Flabels new file mode 100644 index 00000000000..ec19f2d05c7 --- /dev/null +++ b/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO%2Flabels @@ -0,0 +1,3 @@ +Content-Type: application/json; charset=UTF-8 + +[{"id":1,"name":"bug","color":"ee0701"},{"id":2,"name":"duplicate","color":"cccccc"},{"id":3,"name":"enhancement","color":"84b6eb"},{"id":4,"name":"help wanted","color":"128a0c"},{"id":5,"name":"invalid","color":"e6e6e6"},{"id":6,"name":"question","color":"cc317c"},{"id":7,"name":"wontfix","color":"ffffff"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO%2Fmilestones b/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO%2Fmilestones new file mode 100644 index 00000000000..f4c6d9dd347 --- /dev/null +++ b/services/migrations/_mock_data/TestGogsDownloadRepo/GET_%2Fapi%2Fv1%2Frepos%2Flunnytest%2FTESTREPO%2Fmilestones @@ -0,0 +1,3 @@ +Content-Type: application/json; charset=UTF-8 + +[{"id":1,"title":"1.0","description":"","state":"open","open_issues":1,"closed_issues":0,"closed_at":null,"due_on":null}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%2F397%2Fiterations b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%2F397%2Fiterations new file mode 100644 index 00000000000..2d6b18f5db1 --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%2F397%2Fiterations @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +[{"id":51,"name":"1.1.0"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%2F398%2Fchanges b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%2F398%2Fchanges new file mode 100644 index 00000000000..1dcbc6aecec --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%2F398%2Fchanges @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%2F398%2Fcomments b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%2F398%2Fcomments new file mode 100644 index 00000000000..42683ef6d4f --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%2F398%2Fcomments @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +[{"id":1001,"date":"2021-08-09T22:56:31.128Z","userId":336,"content":"it has a comment\n\nEDIT: that got edited"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%2F398%2Fiterations b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%2F398%2Fiterations new file mode 100644 index 00000000000..1dcbc6aecec --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%2F398%2Fiterations @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%3Fcount%3D2%26offset%3D0%26query%3D%2522Project%2522%2Bis%2B%2522go-gitea-test_repo%2522%26withFields%3Dtrue b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%3Fcount%3D2%26offset%3D0%26query%3D%2522Project%2522%2Bis%2B%2522go-gitea-test_repo%2522%26withFields%3Dtrue new file mode 100644 index 00000000000..0f2de921b5d --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fissues%3Fcount%3D2%26offset%3D0%26query%3D%2522Project%2522%2Bis%2B%2522go-gitea-test_repo%2522%26withFields%3Dtrue @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +[{"id":398,"number":4,"state":"Open","title":"Hi there","description":"an issue not assigned to a milestone","submitterId":336,"submitDate":"2021-08-09T22:56:16.734Z","fields":[{"name":"Type","value":"Improvement"}]},{"id":397,"number":3,"state":"Open","title":"Add an awesome feature","description":"just another issue to test against","submitterId":336,"submitDate":"2021-08-09T22:55:49.878Z","fields":[{"name":"Type","value":"New Feature"}]}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fprojects%2F149%2Fiterations%3Fcount%3D100%26offset%3D0 b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fprojects%2F149%2Fiterations%3Fcount%3D100%26offset%3D0 new file mode 100644 index 00000000000..85fe3fec2b3 --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fprojects%2F149%2Fiterations%3Fcount%3D100%26offset%3D0 @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +[{"id":50,"name":"1.0.0","description":"","dueDay":18751,"closed":true},{"id":51,"name":"1.1.0","description":"next things?","dueDay":0,"closed":false}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fprojects%2F149%2Fiterations%3Fcount%3D100%26offset%3D100 b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fprojects%2F149%2Fiterations%3Fcount%3D100%26offset%3D100 new file mode 100644 index 00000000000..1dcbc6aecec --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fprojects%2F149%2Fiterations%3Fcount%3D100%26offset%3D100 @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fprojects%3Fcount%3D1%26offset%3D0%26query%3D%2522Path%2522%2Bis%2B%2522go-gitea-test_repo%2522 b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fprojects%3Fcount%3D1%26offset%3D0%26query%3D%2522Path%2522%2Bis%2B%2522go-gitea-test_repo%2522 new file mode 100644 index 00000000000..dc4e332e91b --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fprojects%3Fcount%3D1%26offset%3D0%26query%3D%2522Path%2522%2Bis%2B%2522go-gitea-test_repo%2522 @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +[{"id":149,"name":"go-gitea-test_repo","path":"/go-gitea-test_repo","description":"Test repository for testing migration from OneDev to gitea"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fpulls%2F186%2Fmerge-preview b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fpulls%2F186%2Fmerge-preview new file mode 100644 index 00000000000..ef3b5261c09 --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fpulls%2F186%2Fmerge-preview @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +{"targetHeadCommitHash":"f32b0a9dfd09a60f616f29158f772cedd89942d2","headCommitHash":"343deffe3526b9bc84e873743ff7f6e6d8b827c0","mergeStrategy":"MERGE_IF_NECESSARY","mergeCommitHash":"abc123"} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fpulls%2F186%2Freviews b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fpulls%2F186%2Freviews new file mode 100644 index 00000000000..e0dc043b9e4 --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fpulls%2F186%2Freviews @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +[{"id":2001,"userId":317,"status":"PENDING"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fpulls%3Fcount%3D1%26offset%3D0%26query%3D%2522Target%2BProject%2522%2Bis%2B%2522go-gitea-test_repo%2522 b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fpulls%3Fcount%3D1%26offset%3D0%26query%3D%2522Target%2BProject%2522%2Bis%2B%2522go-gitea-test_repo%2522 new file mode 100644 index 00000000000..0d40db7275f --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fpulls%3Fcount%3D1%26offset%3D0%26query%3D%2522Target%2BProject%2522%2Bis%2B%2522go-gitea-test_repo%2522 @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +[{"id":186,"number":1,"title":"Pull to add a new file","description":"just do some git stuff","submitterId":336,"submitDate":"2021-08-09T23:01:16.025Z","targetBranch":"master","sourceBranch":"branch-for-a-pull","baseCommitHash":"f32b0a9dfd09a60f616f29158f772cedd89942d2","status":"OPEN"}] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fusers%2F317 b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fusers%2F317 new file mode 100644 index 00000000000..6d42ce89bb0 --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fusers%2F317 @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +{"name":"User 317"} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fusers%2F317%2Femail-addresses b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fusers%2F317%2Femail-addresses new file mode 100644 index 00000000000..1dcbc6aecec --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fusers%2F317%2Femail-addresses @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fusers%2F336 b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fusers%2F336 new file mode 100644 index 00000000000..bf8a0e585da --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fusers%2F336 @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +{"name":"User 336"} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fusers%2F336%2Femail-addresses b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fusers%2F336%2Femail-addresses new file mode 100644 index 00000000000..1dcbc6aecec --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fusers%2F336%2Femail-addresses @@ -0,0 +1,3 @@ +Content-Type: application/json;charset=utf-8 + +[] \ No newline at end of file diff --git a/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fversion%2Fserver b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fversion%2Fserver new file mode 100644 index 00000000000..65c20b7e86a --- /dev/null +++ b/services/migrations/_mock_data/TestOneDevDownloadRepo/GET_%2F~api%2Fversion%2Fserver @@ -0,0 +1,3 @@ +Content-Type: text/plain;charset=utf-8 + +12.0.1 \ No newline at end of file diff --git a/services/migrations/codebase_test.go b/services/migrations/codebase_test.go index dabe7e1ac9f..ae198b78c7e 100644 --- a/services/migrations/codebase_test.go +++ b/services/migrations/codebase_test.go @@ -6,39 +6,34 @@ package migrations import ( "net/url" "os" + "path/filepath" + "runtime" "testing" "time" + "code.gitea.io/gitea/models/unittest" base "code.gitea.io/gitea/modules/migration" "github.com/stretchr/testify/assert" ) func TestCodebaseDownloadRepo(t *testing.T) { - // Skip tests if Codebase token is not found - cloneUser := os.Getenv("CODEBASE_CLONE_USER") - clonePassword := os.Getenv("CODEBASE_CLONE_PASSWORD") apiUser := os.Getenv("CODEBASE_API_USER") apiPassword := os.Getenv("CODEBASE_API_TOKEN") - if apiUser == "" || apiPassword == "" { - t.Skip("skipped test because a CODEBASE_ variable was not in the environment") - } + liveMode := apiUser != "" && apiPassword != "" + + _, callerFile, _, _ := runtime.Caller(0) + fixtureDir := filepath.Join(filepath.Dir(callerFile), "_mock_data/TestCodebaseDownloadRepo") + mockServer := unittest.NewMockWebServer(t, "https://api3.codebasehq.com", fixtureDir, liveMode) cloneAddr := "https://gitea-test.codebasehq.com/gitea-test/test.git" - u, _ := url.Parse(cloneAddr) - if cloneUser != "" { - u.User = url.UserPassword(cloneUser, clonePassword) - } + projectURL, _ := url.Parse(cloneAddr) + projectURL.User = nil + ctx := t.Context() - factory := &CodebaseDownloaderFactory{} - downloader, err := factory.New(ctx, base.MigrateOptions{ - CloneAddr: u.String(), - AuthUsername: apiUser, - AuthPassword: apiPassword, - }) - if err != nil { - t.Fatalf("Error creating Codebase downloader: %v", err) - } + downloader := NewCodebaseDownloader(ctx, projectURL, "gitea-test", "test", apiUser, apiPassword) + downloader.baseURL, _ = url.Parse(mockServer.URL) + repo, err := downloader.GetRepoInfo(ctx) assert.NoError(t, err) assertRepositoryEqual(t, &base.Repository{ @@ -144,6 +139,6 @@ func TestCodebaseDownloadRepo(t *testing.T) { }, prs) rvs, err := downloader.GetReviews(ctx, prs[0]) - assert.NoError(t, err) + assert.Error(t, err) assert.Empty(t, rvs) } diff --git a/services/migrations/gitea_downloader_test.go b/services/migrations/gitea_downloader_test.go index cf727b44c74..d604ebdfbd5 100644 --- a/services/migrations/gitea_downloader_test.go +++ b/services/migrations/gitea_downloader_test.go @@ -4,12 +4,14 @@ package migrations import ( - "net/http" "os" + "path/filepath" + "runtime" "sort" "testing" "time" + "code.gitea.io/gitea/models/unittest" base "code.gitea.io/gitea/modules/migration" "github.com/stretchr/testify/assert" @@ -17,19 +19,15 @@ import ( ) func TestGiteaDownloadRepo(t *testing.T) { - // Skip tests if Gitea token is not found (TODO: this test seems stopped for long time because there is no token in CI secrets) - giteaToken := os.Getenv("GITEA_TEST_OFFICIAL_SITE_TOKEN") - if giteaToken == "" { - t.Skip("skipped test because GITEA_TEST_OFFICIAL_SITE_TOKEN was not in the environment") - } + token := os.Getenv("GITEA_TEST_OFFICIAL_SITE_TOKEN") + liveMode := token != "" + + _, callerFile, _, _ := runtime.Caller(0) + fixtureDir := filepath.Join(filepath.Dir(callerFile), "_mock_data/TestGiteaDownloadRepo") + mockServer := unittest.NewMockWebServer(t, "https://gitea.com", fixtureDir, liveMode) - resp, err := http.Get("https://gitea.com/gitea") - if err != nil || resp.StatusCode != http.StatusOK { - t.Skipf("Can't reach https://gitea.com, skipping %s", t.Name()) - } - defer resp.Body.Close() ctx := t.Context() - downloader, err := NewGiteaDownloader(ctx, "https://gitea.com", "gitea/test_repo", "", "", giteaToken) + downloader, err := NewGiteaDownloader(ctx, mockServer.URL, "gitea/test_repo", "", "", token) require.NoError(t, err, "NewGiteaDownloader error occur") require.NotNil(t, downloader, "NewGiteaDownloader is nil") @@ -40,8 +38,8 @@ func TestGiteaDownloadRepo(t *testing.T) { Owner: "gitea", IsPrivate: false, Description: "Test repository for testing migration from gitea to gitea", - CloneURL: "https://gitea.com/gitea/test_repo.git", - OriginalURL: "https://gitea.com/gitea/test_repo", + CloneURL: mockServer.URL + "/gitea/test_repo.git", + OriginalURL: mockServer.URL + "/gitea/test_repo", DefaultBranch: "master", }, repo) @@ -83,21 +81,21 @@ func TestGiteaDownloadRepo(t *testing.T) { milestones, err := downloader.GetMilestones(ctx) assert.NoError(t, err) assertMilestonesEqual(t, []*base.Milestone{ - { - Title: "V2 Finalize", - Created: time.Unix(0, 0), - Deadline: new(time.Unix(1599263999, 0)), - Updated: new(time.Unix(0, 0)), - State: "open", - }, { Title: "V1", Description: "Generate Content", Created: time.Unix(0, 0), Updated: new(time.Unix(0, 0)), - Closed: new(time.Unix(1598985406, 0)), + Closed: new(time.Date(2020, 9, 1, 18, 36, 46, 0, time.UTC)), State: "closed", }, + { + Title: "V2 Finalize", + Created: time.Unix(0, 0), + Deadline: new(time.Date(2020, 9, 4, 23, 59, 59, 0, time.UTC)), + Updated: new(time.Date(2022, 11, 13, 5, 29, 15, 0, time.UTC)), + State: "open", + }, }, milestones) releases, err := downloader.GetReleases(ctx) @@ -114,7 +112,7 @@ func TestGiteaDownloadRepo(t *testing.T) { Published: time.Date(2020, 9, 1, 18, 2, 43, 0, time.UTC), PublisherID: 689, PublisherName: "6543", - PublisherEmail: "6543@obermui.de", + PublisherEmail: "689+6543@noreply.gitea.com", }, { Name: "First Release", @@ -127,7 +125,7 @@ func TestGiteaDownloadRepo(t *testing.T) { Published: time.Date(2020, 9, 1, 17, 30, 32, 0, time.UTC), PublisherID: 689, PublisherName: "6543", - PublisherEmail: "6543@obermui.de", + PublisherEmail: "689+6543@noreply.gitea.com", }, }, releases) @@ -149,7 +147,7 @@ func TestGiteaDownloadRepo(t *testing.T) { Milestone: "V1", PosterID: -1, PosterName: "Ghost", - PosterEmail: "", + PosterEmail: "-1+ghost@noreply.gitea.com", State: "closed", IsLocked: true, Created: time.Unix(1598975321, 0), @@ -180,7 +178,7 @@ func TestGiteaDownloadRepo(t *testing.T) { Milestone: "", PosterID: 689, PosterName: "6543", - PosterEmail: "6543@obermui.de", + PosterEmail: "689+6543@noreply.gitea.com", State: "closed", IsLocked: false, Created: time.Unix(1598919780, 0), @@ -201,7 +199,7 @@ func TestGiteaDownloadRepo(t *testing.T) { IssueIndex: 4, PosterID: 689, PosterName: "6543", - PosterEmail: "6543@obermui.de", + PosterEmail: "689+6543@noreply.gitea.com", Created: time.Unix(1598975370, 0), Updated: time.Unix(1599070865, 0), Content: "a really good question!\n\nIt is the used as TESTSET for gitea2gitea repo migration function", @@ -210,7 +208,7 @@ func TestGiteaDownloadRepo(t *testing.T) { IssueIndex: 4, PosterID: -1, PosterName: "Ghost", - PosterEmail: "", + PosterEmail: "-1+ghost@noreply.gitea.com", Created: time.Unix(1598975393, 0), Updated: time.Unix(1598975393, 0), Content: "Oh!", @@ -229,7 +227,7 @@ func TestGiteaDownloadRepo(t *testing.T) { Number: 12, PosterID: 689, PosterName: "6543", - PosterEmail: "6543@obermui.de", + PosterEmail: "689+6543@noreply.gitea.com", Title: "Dont Touch", Content: "\r\nadd dont touch note", Milestone: "V2 Finalize", @@ -237,7 +235,7 @@ func TestGiteaDownloadRepo(t *testing.T) { IsLocked: false, Created: time.Unix(1598982759, 0), Updated: time.Unix(1599023425, 0), - Closed: new(time.Unix(1598982934, 0)), + Closed: new(time.Unix(1598982933, 0)), Assignees: []string{"techknowlogick"}, Base: base.PullRequestBranch{ CloneURL: "", @@ -247,7 +245,7 @@ func TestGiteaDownloadRepo(t *testing.T) { OwnerName: "gitea", }, Head: base.PullRequestBranch{ - CloneURL: "https://gitea.com/6543-forks/test_repo.git", + CloneURL: mockServer.URL + "/6543-forks/test_repo.git", Ref: "refs/pull/12/head", SHA: "b6ab5d9ae000b579a5fff03f92c486da4ddf48b6", RepoName: "test_repo", @@ -256,7 +254,7 @@ func TestGiteaDownloadRepo(t *testing.T) { Merged: true, MergedTime: new(time.Unix(1598982934, 0)), MergeCommitSHA: "827aa28a907853e5ddfa40c8f9bc52471a2685fd", - PatchURL: "https://gitea.com/gitea/test_repo/pulls/12.patch", + PatchURL: mockServer.URL + "/gitea/test_repo/pulls/12.patch", }, prs[1]) reviews, err := downloader.GetReviews(ctx, &base.Issue{Number: 7, ForeignIndex: 7}) @@ -283,7 +281,7 @@ func TestGiteaDownloadRepo(t *testing.T) { PosterID: 689, Reactions: nil, CreatedAt: time.Date(2020, 9, 1, 16, 12, 58, 0, time.UTC), - UpdatedAt: time.Date(2020, 9, 1, 16, 12, 58, 0, time.UTC), + UpdatedAt: time.Date(2024, 6, 3, 1, 18, 36, 0, time.UTC), }, }, }, diff --git a/services/migrations/github_test.go b/services/migrations/github_test.go index 198062f7cf7..974a3bd8128 100644 --- a/services/migrations/github_test.go +++ b/services/migrations/github_test.go @@ -6,9 +6,12 @@ package migrations import ( "os" + "path/filepath" + "runtime" "testing" "time" + "code.gitea.io/gitea/models/unittest" base "code.gitea.io/gitea/modules/migration" "github.com/stretchr/testify/assert" @@ -16,15 +19,20 @@ import ( ) func TestGitHubDownloadRepo(t *testing.T) { - GithubLimitRateRemaining = 3 // Wait at 3 remaining since we could have 3 CI in // token := os.Getenv("GITHUB_READ_TOKEN") - if token == "" { - t.Skip("Skipping GitHub migration test because GITHUB_READ_TOKEN is empty") - } + liveMode := token != "" + + _, callerFile, _, _ := runtime.Caller(0) + fixtureDir := filepath.Join(filepath.Dir(callerFile), "_mock_data/TestGitHubDownloadRepo") + mockServer := unittest.NewMockWebServer(t, "https://api.github.com", fixtureDir, liveMode, unittest.MockServerOptions{ + StripPrefix: "/api/v3", + }) + + GithubLimitRateRemaining = 3 // Wait at 3 remaining since we could have 3 CI in // ctx := t.Context() - downloader := NewGithubDownloaderV3(ctx, "https://github.com", "", "", token, "go-gitea", "test_repo") + downloader := NewGithubDownloaderV3(ctx, mockServer.URL, "", "", token, "go-gitea", "test_repo") err := downloader.RefreshRate(ctx) - assert.NoError(t, err) + require.NoError(t, err) repo, err := downloader.GetRepoInfo(ctx) assert.NoError(t, err) @@ -47,7 +55,7 @@ func TestGitHubDownloadRepo(t *testing.T) { { Title: "1.0.0", Description: "Milestone 1.0.0", - Deadline: new(time.Date(2019, 11, 11, 8, 0, 0, 0, time.UTC)), + Deadline: new(time.Date(2019, 11, 11, 0, 0, 0, 0, time.UTC)), Created: time.Date(2019, 11, 12, 19, 37, 8, 0, time.UTC), Updated: new(time.Date(2019, 11, 12, 21, 56, 17, 0, time.UTC)), Closed: new(time.Date(2019, 11, 12, 19, 45, 49, 0, time.UTC)), @@ -56,7 +64,7 @@ func TestGitHubDownloadRepo(t *testing.T) { { Title: "1.1.0", Description: "Milestone 1.1.0", - Deadline: new(time.Date(2019, 11, 12, 8, 0, 0, 0, time.UTC)), + Deadline: new(time.Date(2019, 11, 12, 0, 0, 0, 0, time.UTC)), Created: time.Date(2019, 11, 12, 19, 37, 25, 0, time.UTC), Updated: new(time.Date(2019, 11, 12, 21, 39, 27, 0, time.UTC)), Closed: new(time.Date(2019, 11, 12, 19, 45, 46, 0, time.UTC)), @@ -269,10 +277,10 @@ func TestGitHubDownloadRepo(t *testing.T) { Description: "Improvements or additions to documentation", }, }, - PatchURL: "https://github.com/go-gitea/test_repo/pull/3.patch", + PatchURL: "", Head: base.PullRequestBranch{ Ref: "master", - CloneURL: "https://github.com/mrsdizzie/test_repo.git", + CloneURL: "", SHA: "076160cf0b039f13e5eff19619932d181269414b", RepoName: "test_repo", @@ -299,7 +307,7 @@ func TestGitHubDownloadRepo(t *testing.T) { PosterName: "mrsdizzie", State: "open", Created: time.Date(2019, 11, 12, 21, 54, 18, 0, time.UTC), - Updated: time.Date(2020, 1, 4, 11, 30, 1, 0, time.UTC), + Updated: time.Date(2025, 3, 16, 15, 46, 20, 0, time.UTC), Labels: []*base.Label{ { Name: "bug", @@ -307,13 +315,13 @@ func TestGitHubDownloadRepo(t *testing.T) { Description: "Something isn't working", }, }, - PatchURL: "https://github.com/go-gitea/test_repo/pull/4.patch", + PatchURL: "", Head: base.PullRequestBranch{ Ref: "test-branch", SHA: "2be9101c543658591222acbee3eb799edfc3853d", RepoName: "test_repo", OwnerName: "mrsdizzie", - CloneURL: "https://github.com/mrsdizzie/test_repo.git", + CloneURL: "", }, Base: base.PullRequestBranch{ Ref: "master", diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go index 1304977cdef..da664e436b8 100644 --- a/services/migrations/gitlab_test.go +++ b/services/migrations/gitlab_test.go @@ -8,10 +8,13 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" + "runtime" "strconv" "testing" "time" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/json" base "code.gitea.io/gitea/modules/migration" @@ -20,19 +23,15 @@ import ( ) func TestGitlabDownloadRepo(t *testing.T) { - // Skip tests if Gitlab token is not found - gitlabPersonalAccessToken := os.Getenv("GITLAB_READ_TOKEN") - if gitlabPersonalAccessToken == "" { - t.Skip("skipped test because GITLAB_READ_TOKEN was not in the environment") - } + token := os.Getenv("GITLAB_READ_TOKEN") + liveMode := token != "" + + _, callerFile, _, _ := runtime.Caller(0) + fixtureDir := filepath.Join(filepath.Dir(callerFile), "_mock_data/TestGitlabDownloadRepo") + mockServer := unittest.NewMockWebServer(t, "https://gitlab.com", fixtureDir, liveMode) - resp, err := http.Get("https://gitlab.com/gitea/test_repo") - if err != nil || resp.StatusCode != http.StatusOK { - t.Skipf("Can't access test repo, skipping %s", t.Name()) - } - defer resp.Body.Close() ctx := t.Context() - downloader, err := NewGitlabDownloader(ctx, "https://gitlab.com", "gitea/test_repo", gitlabPersonalAccessToken) + downloader, err := NewGitlabDownloader(ctx, mockServer.URL, "gitea/test_repo", token) if err != nil { t.Fatalf("NewGitlabDownloader is nil: %v", err) } @@ -43,8 +42,8 @@ func TestGitlabDownloadRepo(t *testing.T) { Name: "test_repo", Owner: "", Description: "Test repository for testing migration from gitlab to gitea", - CloneURL: "https://gitlab.com/gitea/test_repo.git", - OriginalURL: "https://gitlab.com/gitea/test_repo", + CloneURL: mockServer.URL + "/gitea/test_repo.git", + OriginalURL: mockServer.URL + "/gitea/test_repo", DefaultBranch: "master", }, repo) @@ -255,51 +254,75 @@ func TestGitlabDownloadRepo(t *testing.T) { }, }, comments) - prs, _, err := downloader.GetPullRequests(ctx, 1, 1) + prs, _, err := downloader.GetPullRequests(ctx, 1, 2) assert.NoError(t, err) assertPullRequestsEqual(t, []*base.PullRequest{ { - Number: 4, - Title: "Test branch", - Content: "do not merge this PR", - Milestone: "1.0.0", - PosterID: 1241334, - PosterName: "lafriks", - State: "opened", - Created: time.Date(2019, 11, 28, 15, 56, 54, 104000000, time.UTC), - Labels: []*base.Label{ - { - Name: "bug", - }, - }, - Reactions: []*base.Reaction{{ - UserID: 4575606, - UserName: "real6543", - Content: "thumbsup", - }, { - UserID: 4575606, - UserName: "real6543", - Content: "tada", - }}, - PatchURL: "https://gitlab.com/gitea/test_repo/-/merge_requests/2.patch", + Number: 6, + Title: "Test/parsing", + Content: "This MR was created in error, feel free to delete when convenient. Sorry for the noise.", + Milestone: "", + PosterID: 10529876, + PosterName: "patdyn", + State: "closed", + Created: time.Date(2025, 11, 25, 9, 21, 42, 628000000, time.UTC), + Labels: []*base.Label{}, + Reactions: []*base.Reaction{}, + PatchURL: mockServer.URL + "/gitea/test_repo/-/merge_requests/4.patch", Head: base.PullRequestBranch{ - Ref: "feat/test", - CloneURL: "https://gitlab.com/gitea/test_repo/-/merge_requests/2", - SHA: "9f733b96b98a4175276edf6a2e1231489c3bdd23", + Ref: "test/parsing", + CloneURL: mockServer.URL + "/gitea/test_repo/-/merge_requests/4", + SHA: "c59c9b451acca9d106cc19d61d87afe3fbbb8b83", RepoName: "test_repo", - OwnerName: "lafriks", + OwnerName: "patdyn", }, Base: base.PullRequestBranch{ Ref: "master", - SHA: "", - OwnerName: "lafriks", + SHA: "c59c9b451acca9d106cc19d61d87afe3fbbb8b83", + OwnerName: "patdyn", RepoName: "test_repo", }, - Closed: nil, + Closed: new(time.Date(2025, 11, 25, 9, 43, 14, 581000000, time.UTC)), Merged: false, MergedTime: nil, MergeCommitSHA: "", - ForeignIndex: 2, + ForeignIndex: 4, + Context: gitlabIssueContext{IsMergeRequest: true}, + }, + { + Number: 5, + Title: "Test branch", + Content: "do not merge this PR", + Milestone: "", + PosterID: 2005797, + PosterName: "oliverpool", + State: "closed", + Created: time.Date(2024, 9, 3, 7, 52, 8, 78000000, time.UTC), + Labels: []*base.Label{}, + Reactions: []*base.Reaction{{ + UserID: 2005797, + UserName: "oliverpool", + Content: "thumbsup", + }}, + PatchURL: mockServer.URL + "/gitea/test_repo/-/merge_requests/3.patch", + Head: base.PullRequestBranch{ + Ref: "feat/test", + CloneURL: mockServer.URL + "/gitea/test_repo/-/merge_requests/3", + SHA: "9f733b96b98a4175276edf6a2e1231489c3bdd23", + RepoName: "test_repo", + OwnerName: "oliverpool", + }, + Base: base.PullRequestBranch{ + Ref: "master", + SHA: "c59c9b451acca9d106cc19d61d87afe3fbbb8b83", + OwnerName: "oliverpool", + RepoName: "test_repo", + }, + Closed: new(time.Date(2024, 9, 3, 7, 52, 28, 488000000, time.UTC)), + Merged: false, + MergedTime: nil, + MergeCommitSHA: "", + ForeignIndex: 3, Context: gitlabIssueContext{IsMergeRequest: true}, }, }, prs) @@ -309,16 +332,16 @@ func TestGitlabDownloadRepo(t *testing.T) { assertReviewsEqual(t, []*base.Review{ { IssueIndex: 1, - ReviewerID: 4102996, - ReviewerName: "zeripath", - CreatedAt: time.Date(2019, 11, 28, 16, 2, 8, 377000000, time.UTC), + ReviewerID: 527793, + ReviewerName: "axifive", + CreatedAt: time.Date(2019, 11, 28, 8, 54, 41, 34000000, time.UTC), State: "APPROVED", }, { IssueIndex: 1, - ReviewerID: 527793, - ReviewerName: "axifive", - CreatedAt: time.Date(2019, 11, 28, 16, 2, 8, 377000000, time.UTC), + ReviewerID: 4102996, + ReviewerName: "zeripath", + CreatedAt: time.Date(2019, 11, 28, 8, 54, 41, 34000000, time.UTC), State: "APPROVED", }, }, rvs) @@ -330,7 +353,7 @@ func TestGitlabDownloadRepo(t *testing.T) { IssueIndex: 2, ReviewerID: 4575606, ReviewerName: "real6543", - CreatedAt: time.Date(2020, 4, 19, 19, 24, 21, 108000000, time.UTC), + CreatedAt: time.Date(2019, 11, 28, 15, 56, 54, 104000000, time.UTC), State: "APPROVED", }, }, rvs) diff --git a/services/migrations/gogs_test.go b/services/migrations/gogs_test.go index de7351b5bfe..2dd7b00fb9d 100644 --- a/services/migrations/gogs_test.go +++ b/services/migrations/gogs_test.go @@ -4,32 +4,28 @@ package migrations import ( - "net/http" "os" + "path/filepath" + "runtime" "testing" "time" + "code.gitea.io/gitea/models/unittest" base "code.gitea.io/gitea/modules/migration" "github.com/stretchr/testify/assert" ) func TestGogsDownloadRepo(t *testing.T) { - // Skip tests if Gogs token is not found - gogsPersonalAccessToken := os.Getenv("GOGS_READ_TOKEN") - if len(gogsPersonalAccessToken) == 0 { - t.Skip("skipped test because GOGS_READ_TOKEN was not in the environment") - } + token := os.Getenv("GOGS_READ_TOKEN") + liveMode := token != "" + + _, callerFile, _, _ := runtime.Caller(0) + fixtureDir := filepath.Join(filepath.Dir(callerFile), "_mock_data/TestGogsDownloadRepo") + mockServer := unittest.NewMockWebServer(t, "https://try.gogs.io", fixtureDir, liveMode) - resp, err := http.Get("https://try.gogs.io/lunnytest/TESTREPO") - if err != nil || resp.StatusCode/100 != 2 { - // skip and don't run test - t.Skipf("visit test repo failed, ignored") - return - } - defer resp.Body.Close() ctx := t.Context() - downloader := NewGogsDownloader(ctx, "https://try.gogs.io", "", "", gogsPersonalAccessToken, "lunnytest", "TESTREPO") + downloader := NewGogsDownloader(ctx, mockServer.URL, "", "", token, "lunnytest", "TESTREPO") repo, err := downloader.GetRepoInfo(ctx) assert.NoError(t, err) @@ -37,8 +33,8 @@ func TestGogsDownloadRepo(t *testing.T) { Name: "TESTREPO", Owner: "lunnytest", Description: "", - CloneURL: "https://try.gogs.io/lunnytest/TESTREPO.git", - OriginalURL: "https://try.gogs.io/lunnytest/TESTREPO", + CloneURL: mockServer.URL + "/lunnytest/TESTREPO.git", + OriginalURL: mockServer.URL + "/lunnytest/TESTREPO", DefaultBranch: "master", }, repo) diff --git a/services/migrations/main_test.go b/services/migrations/main_test.go index 8c14b072d61..8d8eb6459ff 100644 --- a/services/migrations/main_test.go +++ b/services/migrations/main_test.go @@ -25,8 +25,7 @@ func assertTimeEqual(t *testing.T, expected, actual time.Time) { func assertTimePtrEqual(t *testing.T, expected, actual *time.Time) { if expected == nil { assert.Nil(t, actual) - } else { - assert.NotNil(t, actual) + } else if assert.NotNil(t, actual) { assertTimeEqual(t, *expected, *actual) } } diff --git a/services/migrations/migrate_test.go b/services/migrations/migrate_test.go index 03efa6185b2..315d1111890 100644 --- a/services/migrations/migrate_test.go +++ b/services/migrations/migrate_test.go @@ -110,6 +110,6 @@ func TestAllowBlockList(t *testing.T) { assert.NoError(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("1.2.3.4")})) assert.Error(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("127.0.0.1")})) - // reset - init("", "", false) + // reset to allow local networks (mock servers use 127.0.0.1) + init("", "", true) } diff --git a/services/migrations/onedev_test.go b/services/migrations/onedev_test.go index 9e93272d385..711242c41fc 100644 --- a/services/migrations/onedev_test.go +++ b/services/migrations/onedev_test.go @@ -4,24 +4,27 @@ package migrations import ( - "net/http" "net/url" + "os" + "path/filepath" + "runtime" "testing" "time" + "code.gitea.io/gitea/models/unittest" base "code.gitea.io/gitea/modules/migration" "github.com/stretchr/testify/assert" ) func TestOneDevDownloadRepo(t *testing.T) { - resp, err := http.Get("https://code.onedev.io/projects/go-gitea-test_repo") - if err != nil || resp.StatusCode != http.StatusOK { - t.Skipf("Can't access test repo, skipping %s", t.Name()) - } - defer resp.Body.Close() + liveMode := os.Getenv("ONEDEV_LIVE") != "" - u, _ := url.Parse("https://code.onedev.io") + _, callerFile, _, _ := runtime.Caller(0) + fixtureDir := filepath.Join(filepath.Dir(callerFile), "_mock_data/TestOneDevDownloadRepo") + mockServer := unittest.NewMockWebServer(t, "https://code.onedev.io", fixtureDir, liveMode) + + u, _ := url.Parse(mockServer.URL) ctx := t.Context() downloader := NewOneDevDownloader(ctx, u, "", "", "go-gitea-test_repo") repo, err := downloader.GetRepoInfo(ctx) @@ -30,8 +33,8 @@ func TestOneDevDownloadRepo(t *testing.T) { Name: "go-gitea-test_repo", Owner: "", Description: "Test repository for testing migration from OneDev to gitea", - CloneURL: "https://code.onedev.io/go-gitea-test_repo", - OriginalURL: "https://code.onedev.io/projects/go-gitea-test_repo", + CloneURL: mockServer.URL + "/go-gitea-test_repo", + OriginalURL: mockServer.URL + "/go-gitea-test_repo", }, repo) milestones, err := downloader.GetMilestones(ctx) @@ -42,10 +45,12 @@ func TestOneDevDownloadRepo(t *testing.T) { Title: "1.0.0", Deadline: &deadline, Closed: &deadline, + State: "closed", }, { Title: "1.1.0", Description: "next things?", + State: "open", }, }, milestones) @@ -101,6 +106,7 @@ func TestOneDevDownloadRepo(t *testing.T) { assertCommentsEqual(t, []*base.Comment{ { IssueIndex: 4, + PosterID: 336, PosterName: "User 336", Created: time.Unix(1628549791, 128000000), Updated: time.Unix(1628549791, 128000000), @@ -115,6 +121,7 @@ func TestOneDevDownloadRepo(t *testing.T) { Number: 5, Title: "Pull to add a new file", Content: "just do some git stuff", + PosterID: 336, PosterName: "User 336", State: "open", Created: time.Unix(1628550076, 25000000), @@ -139,6 +146,7 @@ func TestOneDevDownloadRepo(t *testing.T) { assertReviewsEqual(t, []*base.Review{ { IssueIndex: 5, + ReviewerID: 317, ReviewerName: "User 317", State: "PENDING", }, diff --git a/services/notify/notify.go b/services/notify/notify.go index 2416cbd2e08..152d53b01c9 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -399,12 +399,18 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit } } +// WorkflowRunStatusUpdate dispatches a workflow run status change to every registered notifier. +// Prefer the helpers in services/actions/notify.go over calling this directly; +// unless you are sure the caller has already resolved the correct sender and paired notifications. func WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { for _, notifier := range notifiers { notifier.WorkflowRunStatusUpdate(ctx, repo, sender, run) } } +// WorkflowJobStatusUpdate dispatches a workflow job status change to every registered notifier. +// Prefer the helpers in services/actions/notify.go over calling this directly; +// unless you are sure the caller has already resolved the correct sender and paired notifications. func WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) { for _, notifier := range notifiers { notifier.WorkflowJobStatusUpdate(ctx, repo, sender, job, task) diff --git a/services/org/org.go b/services/org/org.go index 32c46d7cb9c..629b5acc443 100644 --- a/services/org/org.go +++ b/services/org/org.go @@ -102,10 +102,14 @@ func DeleteOrganization(ctx context.Context, org *org_model.Organization, purge return nil } -func updateOrgRepoForVisibilityChanged(ctx context.Context, repo *repo_model.Repository, makePrivate bool) error { +func updateRepoForVisibilityChanged(ctx context.Context, repo *repo_model.Repository, makePrivate bool) error { + if err := repo.LoadOwner(ctx); err != nil { + return fmt.Errorf("LoadOwner: %w", err) + } + // Organization repository need to recalculate access table when visibility is changed. - if err := access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { - return fmt.Errorf("recalculateTeamAccesses: %w", err) + if err := access_model.RecalculateAccesses(ctx, repo); err != nil { + return fmt.Errorf("RecalculateAccesses: %w", err) } if makePrivate { @@ -135,7 +139,7 @@ func updateOrgRepoForVisibilityChanged(ctx context.Context, repo *repo_model.Rep return fmt.Errorf("getRepositoriesByForkID: %w", err) } for i := range forkRepos { - if err := updateOrgRepoForVisibilityChanged(ctx, forkRepos[i], makePrivate); err != nil { + if err := updateRepoForVisibilityChanged(ctx, forkRepos[i], makePrivate); err != nil { return fmt.Errorf("updateRepoForVisibilityChanged[%s]: %w", forkRepos[i].FullName(), err) } } @@ -161,8 +165,8 @@ func ChangeOrganizationVisibility(ctx context.Context, org *org_model.Organizati return err } for _, repo := range repos { - if err := updateOrgRepoForVisibilityChanged(ctx, repo, visibility == structs.VisibleTypePrivate); err != nil { - return fmt.Errorf("updateOrgRepoForVisibilityChanged: %w", err) + if err := updateRepoForVisibilityChanged(ctx, repo, visibility == structs.VisibleTypePrivate); err != nil { + return fmt.Errorf("updateRepoForVisibilityChanged: %w", err) } } return nil diff --git a/services/org/org_test.go b/services/org/org_test.go index 5253c739020..9c0aa163df4 100644 --- a/services/org/org_test.go +++ b/services/org/org_test.go @@ -10,6 +10,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -60,4 +61,11 @@ func TestOrg(t *testing.T) { assert.Error(t, DeleteOrganization(t.Context(), user, false)) unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) }) + + t.Run("ChangeVisibilityWithUserFork", func(t *testing.T) { + // org 19 has a repository 27 which has a forked repository 29 by user 20 + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 19}) + require.NoError(t, ChangeOrganizationVisibility(t.Context(), org, structs.VisibleTypePrivate)) + unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: org.ID, Visibility: structs.VisibleTypePrivate}) + }) } diff --git a/services/projects/issue.go b/services/projects/issue.go index 377c4b9d583..ece9910cd23 100644 --- a/services/projects/issue.go +++ b/services/projects/issue.go @@ -6,11 +6,14 @@ package project import ( "context" "errors" + "slices" + "strings" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" project_model "code.gitea.io/gitea/models/project" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/optional" ) @@ -86,6 +89,31 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum }) } +func LoadIssuesAssigneesForProject(ctx context.Context, issuesMap map[int64]issues_model.IssueList) ([]*user_model.User, error) { + var issueList issues_model.IssueList + for _, colIssues := range issuesMap { + issueList = append(issueList, colIssues...) + } + err := issueList.LoadAssignees(ctx) + if err != nil { + return nil, err + } + users := make([]*user_model.User, 0, len(issueList)) + usersAdded := container.Set[int64]{} + for _, issue := range issueList { + for _, assignee := range issue.Assignees { + if !usersAdded.Contains(assignee.ID) { + usersAdded.Add(assignee.ID) + users = append(users, assignee) + } + } + } + slices.SortFunc(users, func(a, b *user_model.User) int { + return strings.Compare(a.Name, b.Name) + }) + return users, nil +} + // LoadIssuesFromProject load issues assigned to each project column inside the given project func LoadIssuesFromProject(ctx context.Context, project *project_model.Project, opts *issues_model.IssuesOptions) (results map[int64]issues_model.IssueList, _ error) { issueList, err := issues_model.Issues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) { diff --git a/services/projects/issue_test.go b/services/projects/issue_test.go index 7255cdfe527..17d0fef2e6e 100644 --- a/services/projects/issue_test.go +++ b/services/projects/issue_test.go @@ -15,6 +15,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_Projects(t *testing.T) { @@ -207,4 +208,18 @@ func Test_Projects(t *testing.T) { assert.Len(t, columnIssues[3], 1) }) }) + + t.Run("LoadIssuesAssigneesForProject", func(t *testing.T) { + issuesMap := map[int64]issues_model.IssueList{} + issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + issue6 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6}) + issuesMap[1] = issues_model.IssueList{issue1} + issuesMap[2] = issues_model.IssueList{issue6} + assignees, err := LoadIssuesAssigneesForProject(t.Context(), issuesMap) + require.NoError(t, err) + require.Len(t, assignees, 3) + require.Equal(t, "user1", assignees[0].Name) + require.Equal(t, "user10", assignees[1].Name) + require.Equal(t, "user2", assignees[2].Name) + }) } diff --git a/services/pull/check.go b/services/pull/check.go index 6486ca79df3..bc706844b25 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -38,14 +38,15 @@ import ( var prPatchCheckerQueue *queue.WorkerPoolQueue[string] var ( - ErrIsClosed = errors.New("pull is closed") - ErrNoPermissionToMerge = errors.New("no permission to merge") - ErrNotReadyToMerge = errors.New("not ready to merge") - ErrHasMerged = errors.New("has already been merged") - ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged") - ErrIsChecking = errors.New("cannot merge while conflict checking is in progress") - ErrNotMergeableState = errors.New("not in mergeable state") - ErrDependenciesLeft = errors.New("is blocked by an open dependency") + ErrIsClosed = errors.New("pull is closed") + ErrNoPermissionToMerge = errors.New("no permission to merge") + ErrNotReadyToMerge = errors.New("not ready to merge") + ErrHasMerged = errors.New("has already been merged") + ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged") + ErrIsChecking = errors.New("cannot merge while conflict checking is in progress") + ErrNotMergeableState = errors.New("not in mergeable state") + ErrDependenciesLeft = errors.New("is blocked by an open dependency") + ErrHeadCommitsNotAllVerified = errors.New("the branch requires signed commits but not all head commits are verified") ) func markPullRequestStatusAsChecking(ctx context.Context, pr *issues_model.PullRequest) bool { @@ -132,7 +133,13 @@ const ( ) // CheckPullMergeable check if the pull mergeable based on all conditions (branch protection, merge options, ...) -func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, adminForceMerge bool) error { +// mergeStyle tailors the "require signed commits" prechecks: +// - fast-forward-only: no Gitea commit is produced, so Gitea's merge-signing check is skipped; +// only the user's head commits are verified. +// - merge: both the head commits must be verified and Gitea must sign the merge commit. +// - rebase, rebase-merge, squash: Gitea rewrites the commits and signs each, so only Gitea's +// signing ability is checked. +func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, mergeStyle repo_model.MergeStyle, adminForceMerge bool) error { return db.WithTx(stdCtx, func(ctx context.Context) error { if pr.HasMerged { return ErrHasMerged @@ -161,7 +168,7 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc return ErrIsWorkInProgress } - if !pr.CanAutoMerge() && !pr.IsEmpty() { + if !pr.IsStatusMergeable() && !pr.IsEmpty() { return ErrNotMergeableState } @@ -207,7 +214,7 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc } } - if _, err := isSignedIfRequired(ctx, pr, doer); err != nil { + if err := checkSigningRequirements(ctx, pr, doer, mergeStyle); err != nil { return err } @@ -221,26 +228,45 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc }) } -// isSignedIfRequired check if merge will be signed if required -func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) (bool, error) { +// checkSigningRequirements enforces the target branch's RequireSignedCommits rule +// against the selected merge style: +// - fast-forward-only and merge keep the user's commits on the base branch, so +// those commits must all be verified, or the pre-receive hook will reject the +// push with a generic error. +// - fast-forward-only creates no Gitea commit, so Gitea's signing key is not used. +// - merge, rebase, rebase-merge and squash produce a Gitea-signed commit, so +// Gitea must be configured to sign it. +func checkSigningRequirements(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle) error { pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) if err != nil { - return false, err + return err } - if pb == nil || !pb.RequireSignedCommits { - return true, nil + return nil } gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo) if err != nil { - return false, err + return err } defer closer.Close() - sign, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo) + if mergeStyle == repo_model.MergeStyleFastForwardOnly || mergeStyle == repo_model.MergeStyleMerge { + verified, err := asymkey_service.AllHeadCommitsVerified(ctx, pr, gitRepo) + if err != nil { + return err + } + if !verified { + return ErrHeadCommitsNotAllVerified + } + } - return sign, err + if mergeStyle != repo_model.MergeStyleFastForwardOnly { + if _, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo); err != nil { + return err + } + } + return nil } // markPullRequestAsMergeable checks if pull request is possible to leaving checking status, diff --git a/services/pull/check_test.go b/services/pull/check_test.go index 0f392379324..506cd423015 100644 --- a/services/pull/check_test.go +++ b/services/pull/check_test.go @@ -9,6 +9,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" @@ -73,6 +74,39 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) { prPatchCheckerQueue = nil } +func TestCheckSigningRequirementsHeadCommits(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) + require.NoError(t, pr.LoadBaseRepo(ctx)) + require.NoError(t, pr.LoadHeadRepo(ctx)) + + check := func() error { + return checkSigningRequirements(ctx, pr, nil, repo_model.MergeStyleFastForwardOnly) + } + + // No protected branch rule on the base branch: the check must pass. + require.NoError(t, check()) + + // Protected branch without RequireSignedCommits: the check must still pass. + require.NoError(t, git_model.UpdateProtectBranch(ctx, pr.BaseRepo, &git_model.ProtectedBranch{ + RepoID: pr.BaseRepoID, + RuleName: pr.BaseBranch, + RequireSignedCommits: false, + }, git_model.WhitelistOptions{})) + require.NoError(t, check()) + + // With RequireSignedCommits enabled: the test fixture commits have no signatures, + // so the check must report ErrHeadCommitsNotAllVerified. + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) + require.NoError(t, err) + require.NotNil(t, pb) + pb.RequireSignedCommits = true + require.NoError(t, git_model.UpdateProtectBranch(ctx, pr.BaseRepo, pb, git_model.WhitelistOptions{})) + require.ErrorIs(t, check(), ErrHeadCommitsNotAllVerified) +} + func TestMarkPullRequestAsMergeable(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/services/pull/review.go b/services/pull/review.go index 261cf234b36..cf9c7bf6f14 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -35,7 +35,7 @@ func isErrBlameNotFoundOrNotEnoughLines(err error) bool { return notFound || notEnoughLines } -// ErrDismissRequestOnClosedPR represents an error when an user tries to dismiss a review associated to a closed or merged PR. +// ErrDismissRequestOnClosedPR represents an error when a user tries to dismiss a review associated to a closed or merged PR. type ErrDismissRequestOnClosedPR struct{} // IsErrDismissRequestOnClosedPR checks if an error is an ErrDismissRequestOnClosedPR. @@ -52,7 +52,7 @@ func (err ErrDismissRequestOnClosedPR) Unwrap() error { return util.ErrPermissionDenied } -// ErrSubmitReviewOnClosedPR represents an error when an user tries to submit an approve or reject review associated to a closed or merged PR. +// ErrSubmitReviewOnClosedPR represents an error when a user tries to submit an approve or reject review associated to a closed or merged PR. var ErrSubmitReviewOnClosedPR = errors.New("can't submit review for a closed or merged PR") // LineBlame returns the latest commit at the given line diff --git a/services/pull/reviewer.go b/services/pull/reviewer.go index 52f2f3401c2..139aeeb9503 100644 --- a/services/pull/reviewer.go +++ b/services/pull/reviewer.go @@ -40,15 +40,8 @@ func GetReviewers(ctx context.Context, repo *repo_model.Repository, doerID, post uniqueUserIDs.AddMultiple(collaboratorIDs...) if repo.Owner.IsOrganization() { - 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"). - Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? AND `team_unit`.`type` = ?)", - repo.ID, perm.AccessModeRead, unit.TypePullRequests). - Distinct("`team_user`.uid"). - Select("`team_user`.uid"). - Find(&additionalUserIDs); err != nil { + additionalUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests) + if err != nil { return nil, err } uniqueUserIDs.AddMultiple(additionalUserIDs...) diff --git a/services/repository/create.go b/services/repository/create.go index a8b57b67071..b0b1f4e7c76 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -31,6 +31,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates/vars" + "code.gitea.io/gitea/modules/util" ) // CreateRepoOptions contains the create repository options @@ -85,7 +86,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir cloneLink := repo.CloneLink(ctx, nil /* no doer so do not generate user-related SSH link */) match := map[string]string{ "Name": repo.Name, - "Description": repo.Description, + "Description": util.NormalizeStringEOL(repo.Description), "CloneURL.SSH": cloneLink.SSH, "CloneURL.HTTPS": cloneLink.HTTPS, "OwnerName": repo.OwnerName, diff --git a/services/repository/files/content.go b/services/repository/files/content.go index fc0e00a1a72..9b042f9e67a 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -32,11 +32,6 @@ const ( ContentTypeSubmodule ContentType = "submodule" // submodule content type (submodule) ) -// String gets the string of ContentType -func (ct *ContentType) String() string { - return string(*ct) -} - type GetContentsOrListOptions struct { TreePath string IncludeSingleFileContent bool // include the file's content when the tree path is a file diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index 68d1df24b7e..5d5cc22513b 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -178,8 +178,9 @@ func (t *TemporaryUploadRepository) HashObjectAndWrite(ctx context.Context, cont // AddObjectToIndex adds the provided object hash to the index with the provided mode and path func (t *TemporaryUploadRepository) AddObjectToIndex(ctx context.Context, mode, objectHash, objectPath string) error { - if err := gitcmd.NewCommand("update-index", "--add", "--replace", "--cacheinfo"). - AddDynamicArguments(mode, objectHash, objectPath).WithDir(t.basePath).RunWithStderr(ctx); err != nil { + cmd := gitcmd.NewCommand("update-index", "--add", "--replace", "--cacheinfo"). + AddDynamicArguments(mode + "," + objectHash + "," + objectPath).WithDir(t.basePath) + if err := cmd.RunWithStderr(ctx); err != nil { if matched, _ := regexp.MatchString(".*Invalid path '.*", err.Stderr()); matched { return ErrFilePathInvalid{ Message: objectPath, diff --git a/services/repository/files/temp_repo_test.go b/services/repository/files/temp_repo_test.go new file mode 100644 index 00000000000..89e44e2721b --- /dev/null +++ b/services/repository/files/temp_repo_test.go @@ -0,0 +1,45 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package files + +import ( + "bytes" + "testing" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/git" + + "github.com/stretchr/testify/require" +) + +func TestTemporaryUploadRepository(t *testing.T) { + mockedRepo := &repo_model.Repository{Name: "mocked-repo-name", OwnerName: "mocked-owner-name"} + + doTest := func(t *testing.T, objectFormatName string) { + tmpGitRepo, err := NewTemporaryUploadRepository(mockedRepo) + require.NoError(t, err) + defer tmpGitRepo.Close() + + require.NoError(t, tmpGitRepo.Init(t.Context(), objectFormatName)) + + require.NoError(t, tmpGitRepo.RemoveFilesFromIndex(t.Context(), "any-file-name")) + require.NoError(t, tmpGitRepo.RemoveFilesFromIndex(t.Context(), "--any-file-name")) + + objID, err := tmpGitRepo.HashObjectAndWrite(t.Context(), bytes.NewReader(nil)) + require.NoError(t, err) + require.NoError(t, tmpGitRepo.AddObjectToIndex(t.Context(), "100644", objID, "any-file-name")) + require.NoError(t, tmpGitRepo.AddObjectToIndex(t.Context(), "100644", objID, "--any-file-name")) + } + + t.Run("sha1", func(t *testing.T) { + doTest(t, git.Sha1ObjectFormat.Name()) + }) + + t.Run("sha256", func(t *testing.T) { + if !git.DefaultFeatures().SupportHashSha256 { + t.Skip("sha256 is not supported") + } + doTest(t, git.Sha256ObjectFormat.Name()) + }) +} diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go index e678b07f565..fc361273388 100644 --- a/services/repository/files/tree.go +++ b/services/repository/files/tree.go @@ -26,12 +26,6 @@ type ErrSHANotFound struct { SHA string } -// IsErrSHANotFound checks if an error is a ErrSHANotFound. -func IsErrSHANotFound(err error) bool { - _, ok := err.(ErrSHANotFound) - return ok -} - func (err ErrSHANotFound) Error() string { return fmt.Sprintf("sha not found [%s]", err.SHA) } diff --git a/services/user/delete.go b/services/user/delete.go index 6047694b0c6..e5c2908ada7 100644 --- a/services/user/delete.go +++ b/services/user/delete.go @@ -27,7 +27,7 @@ import ( "xorm.io/builder" ) -// deleteUser deletes models associated to an user. +// deleteUser deletes models associated to a user. func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error) { e := db.GetEngine(ctx) diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index 58fba9f68df..105d834b826 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -36,10 +36,7 @@ import ( func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) { switch w.HTTPMethod { - case "": - log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID) - fallthrough - case http.MethodPost: + case "", http.MethodPost: switch w.ContentType { case webhook_model.ContentTypeJSON: req, err = http.NewRequest(http.MethodPost, w.URL, strings.NewReader(t.PayloadContent)) diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 2b301d4d583..d2575e9931f 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -1043,7 +1043,7 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_ return } - convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run) + convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run, nil) if err != nil { log.Error("ToActionWorkflowRun: %v", err) return diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index f4432cc3f11..8522cce8eca 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -6,6 +6,7 @@ package webhook import ( "testing" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -21,7 +22,17 @@ import ( "github.com/stretchr/testify/require" ) -func TestWebhook_GetSlackHook(t *testing.T) { +func TestWebhookService(t *testing.T) { + unittest.PrepareTestEnv(t) + t.Run("GetSlackHook", testWebhookGetSlackHook) + t.Run("PrepareWebhooks", testWebhookPrepare) + t.Run("PrepareBranchFilterMatch", testWebhookPrepareBranchFilterMatch) + t.Run("PrepareBranchFilterNoMatch", testWebhookPrepareBranchFilterNoMatch) + t.Run("WebhookUserMail", testWebhookUserMail) + t.Run("CheckBranchFilter", testWebhookCheckBranchFilter) +} + +func testWebhookGetSlackHook(t *testing.T) { w := &webhook_model.Webhook{ Meta: `{"channel": "foo", "username": "username", "color": "blue"}`, } @@ -33,66 +44,69 @@ func TestWebhook_GetSlackHook(t *testing.T) { }, *slackHook) } -func TestPrepareWebhooks(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testWebhookPrepare(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - hookTasks := []*webhook_model.HookTask{ - {HookID: 1, EventType: webhook_module.HookEventPush}, - } - for _, hookTask := range hookTasks { - unittest.AssertNotExistsBean(t, hookTask) - } - assert.NoError(t, PrepareWebhooks(t.Context(), EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{Commits: []*api.PayloadCommit{{}}})) - for _, hookTask := range hookTasks { - unittest.AssertExistsAndLoadBean(t, hookTask) + hook := &webhook_model.Webhook{ + RepoID: repo.ID, + URL: "http://localhost/gitea-webhook-test-prepare_webhooks", + ContentType: webhook_model.ContentTypeJSON, + Events: `{"push_only":true}`, + IsActive: true, } + require.NoError(t, db.Insert(t.Context(), hook)) + + hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush} + unittest.AssertNotExistsBean(t, hookTask) + err := PrepareWebhooks(t.Context(), EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{Commits: []*api.PayloadCommit{{}}}) + require.NoError(t, err) + unittest.AssertExistsAndLoadBean(t, hookTask) } -func TestPrepareWebhooksBranchFilterMatch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testWebhookPrepareBranchFilterMatch(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) - hookTasks := []*webhook_model.HookTask{ - {HookID: 4, EventType: webhook_module.HookEventPush}, - } - for _, hookTask := range hookTasks { - unittest.AssertNotExistsBean(t, hookTask) + hook := &webhook_model.Webhook{ + RepoID: repo.ID, + URL: "http://localhost/gitea-webhook-test-branch_filter_match", + ContentType: webhook_model.ContentTypeJSON, + Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`, + IsActive: true, } + require.NoError(t, db.Insert(t.Context(), hook)) + + hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush} + unittest.AssertNotExistsBean(t, hookTask) // this test also ensures that * doesn't handle / in any special way (like shell would) - assert.NoError(t, PrepareWebhooks(t.Context(), EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{Ref: "refs/heads/feature/7791", Commits: []*api.PayloadCommit{{}}})) - for _, hookTask := range hookTasks { - unittest.AssertExistsAndLoadBean(t, hookTask) - } + err := PrepareWebhooks(t.Context(), EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{Ref: "refs/heads/feature/7791", Commits: []*api.PayloadCommit{{}}}) + require.NoError(t, err) + unittest.AssertExistsAndLoadBean(t, hookTask) } -func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testWebhookPrepareBranchFilterNoMatch(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) - hookTasks := []*webhook_model.HookTask{ - {HookID: 4, EventType: webhook_module.HookEventPush}, + hook := &webhook_model.Webhook{ + RepoID: repo.ID, + URL: "http://localhost/gitea-webhook-test-branch_filter_no_match", + ContentType: webhook_model.ContentTypeJSON, + Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`, + IsActive: true, } - for _, hookTask := range hookTasks { - unittest.AssertNotExistsBean(t, hookTask) - } - assert.NoError(t, PrepareWebhooks(t.Context(), EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{Ref: "refs/heads/fix_weird_bug"})) + require.NoError(t, db.Insert(t.Context(), hook)) - for _, hookTask := range hookTasks { - unittest.AssertNotExistsBean(t, hookTask) - } + hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush} + unittest.AssertNotExistsBean(t, hookTask) + err := PrepareWebhooks(t.Context(), EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{Ref: "refs/heads/fix_weird_bug"}) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, hookTask) } -func TestWebhookUserMail(t *testing.T) { - require.NoError(t, unittest.PrepareTestDatabase()) +func testWebhookUserMail(t *testing.T) { defer test.MockVariableValue(&setting.Service.NoReplyAddress, "no-reply.com")() - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(t.Context(), user, nil).Email) assert.Equal(t, user.Email, convert.ToUser(t.Context(), user, user).Email) } -func TestCheckBranchFilter(t *testing.T) { +func testWebhookCheckBranchFilter(t *testing.T) { cases := []struct { filter string ref git.RefName diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 51647e27335..9cad252e670 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -11,10 +11,16 @@ confinement: strict base: core24 adopt-info: gitea -architectures: - - build-on: armhf - - build-on: amd64 - - build-on: arm64 +platforms: + armhf: + build-on: [armhf] + build-for: [armhf] + amd64: + build-on: [amd64] + build-for: [amd64] + arm64: + build-on: [arm64] + build-for: [arm64] environment: GITEA_CUSTOM: "$SNAP_COMMON" diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 475d350d0ca..58728fd117b 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -4,7 +4,7 @@ {{ctx.HeadMetaContentSecurityPolicy}} {{if .Title}}{{.Title}} - {{end}}{{.PageTitleCommon}} - {{if .ManifestData}}{{end}} + diff --git a/templates/devtest/repo-action-view.tmpl b/templates/devtest/repo-action-view.tmpl index 46f040d8a6f..2971039fc94 100644 --- a/templates/devtest/repo-action-view.tmpl +++ b/templates/devtest/repo-action-view.tmpl @@ -3,12 +3,12 @@ {{template "repo/actions/view_component" (dict - "RunID" (or .RunID 10) "JobID" (or .JobID 0) - "ActionsURL" (print AppSubUrl "/devtest/repo-action-view") + "ActionsViewURL" $.ActionsViewURL )}} {{template "base/footer" .}} diff --git a/templates/devtest/severity-colors.tmpl b/templates/devtest/severity-colors.tmpl index 9f86b864ea9..43a51614655 100644 --- a/templates/devtest/severity-colors.tmpl +++ b/templates/devtest/severity-colors.tmpl @@ -20,6 +20,25 @@

This is an info message using --color-info-* variables.

+

Markdown Attention Blocks

+
+

{{svg "octicon-info" 16 "attention-icon attention-note"}}Note

+

Useful information that users should know, even when skimming content.

+
+

{{svg "octicon-light-bulb" 16 "attention-icon attention-tip"}}Tip

+

Helpful advice for doing things better or more easily.

+
+

{{svg "octicon-report" 16 "attention-icon attention-important"}}Important

+

Key information users need to know to achieve their goal.

+
+

{{svg "octicon-alert" 16 "attention-icon attention-warning"}}Warning

+

Urgent info that needs immediate user attention to avoid problems.

+
+

{{svg "octicon-stop" 16 "attention-icon attention-caution"}}Caution

+

Advises about risks or negative outcomes of certain actions.

+
+
+

Form Fields

@@ -28,6 +47,22 @@
+

Error Input

+
+ +
+ +

Attached Section Boxes

+

Error Header

+
Error section body content.
+

Warning Header

+
Warning section body content.
+ +

Banner Preview (info-tinted)

+
+
Banner preview content
+
+

Labels

Red
diff --git a/templates/devtest/toast-and-message.tmpl b/templates/devtest/toast-and-message.tmpl index c4056b6fc6b..484110ef1bf 100644 --- a/templates/devtest/toast-and-message.tmpl +++ b/templates/devtest/toast-and-message.tmpl @@ -1,11 +1,14 @@ {{template "devtest/devtest-header"}} -
-

Toast

-
- - - - +
+
+
+

Toast

+
+ + + + +
{{template "devtest/devtest-footer"}} diff --git a/templates/package/content/terraform.tmpl b/templates/package/content/terraform.tmpl index 6006bee9aa7..edc8825d1f8 100644 --- a/templates/package/content/terraform.tmpl +++ b/templates/package/content/terraform.tmpl @@ -6,7 +6,7 @@
terraform {
 	backend "http" {
-		address = "{{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/terraform/state/{{$.PackageDescriptor.Package.Name}}""
+		address = "{{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/terraform/state/{{$.PackageDescriptor.Package.Name}}"
 		lock_address = "{{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/terraform/state/{{$.PackageDescriptor.Package.Name}}/lock"
 		unlock_address = "{{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/terraform/state/{{$.PackageDescriptor.Package.Name}}/lock"
 		lock_method = "POST"
diff --git a/templates/repo/actions/view.tmpl b/templates/repo/actions/view.tmpl
index 1eb84a9b937..3f879e0e5a9 100644
--- a/templates/repo/actions/view.tmpl
+++ b/templates/repo/actions/view.tmpl
@@ -3,9 +3,8 @@
 
{{template "repo/header" .}} {{template "repo/actions/view_component" (dict - "RunID" .RunID "JobID" .JobID - "ActionsURL" .ActionsURL + "ActionsViewURL" .ActionsViewURL )}}
diff --git a/templates/repo/actions/view_component.tmpl b/templates/repo/actions/view_component.tmpl index 2cc70e499ad..67926276c0c 100644 --- a/templates/repo/actions/view_component.tmpl +++ b/templates/repo/actions/view_component.tmpl @@ -1,17 +1,18 @@ -
{{template "repo/navbar" .}}
-
+
{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}} {{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}} {{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}} diff --git a/templates/repo/commit_load_branches_and_tags.tmpl b/templates/repo/commit_load_branches_and_tags.tmpl index 162d805a29e..18576f871fb 100644 --- a/templates/repo/commit_load_branches_and_tags.tmpl +++ b/templates/repo/commit_load_branches_and_tags.tmpl @@ -1,7 +1,7 @@ {{if not .PageIsWiki}}
{{if .MergedPRIssueNumber}} - {{$prLink := HTMLFormat `#%d` $.RepoLink $.MergedPRIssueNumber $.MergedPRIssueNumber}} + {{$prLink := HTMLFormat `#%d` $.RepoLink $.MergedPRIssueNumber $.MergedPRIssueNumber}}
{{ctx.Locale.Tr "repo.commit.merged_in_pr" $prLink}}
diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index a0722307a70..d520c0fbcf7 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -30,7 +30,7 @@ {{$commitBaseLink = printf "%s/wiki/commit" $commitRepoLink}} {{else if $.PageIsPullCommits}} {{$commitBaseLink = printf "%s/pulls/%d/commits" $commitRepoLink $.Issue.Index}} - {{else if $.Reponame}} + {{else}} {{$commitBaseLink = printf "%s/commit" $commitRepoLink}} {{end}} {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} diff --git a/templates/repo/diff/image_diff.tmpl b/templates/repo/diff/image_diff.tmpl index 7557129c648..5a9dbc90674 100644 --- a/templates/repo/diff/image_diff.tmpl +++ b/templates/repo/diff/image_diff.tmpl @@ -7,8 +7,8 @@ data-mime-before="{{.sniffedTypeBase.GetMimeType}}" data-mime-after="{{.sniffedTypeHead.GetMimeType}}" > - -
+ +
{{ctx.Locale.Tr "repo.diff.image.side_by_side"}} {{if and .blobBase .blobHead}} {{ctx.Locale.Tr "repo.diff.image.swipe"}} diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index 74d6dcb07f8..0acd7bfd717 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -19,7 +19,7 @@
-