diff --git a/.changelog.yml b/.changelog.yml index a7df8779de..748676569a 100644 --- a/.changelog.yml +++ b/.changelog.yml @@ -37,10 +37,7 @@ groups: name: BUGFIXES labels: - type/bug - - - name: API - labels: - - modifies/api + - name: TESTING labels: diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4f82a5d8c6..4863391f39 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" @@ -20,7 +20,6 @@ "customizations": { "vscode": { "settings": {}, - // same extensions as Gitpod, should match /.gitpod.yml "extensions": [ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", diff --git a/.dockerignore b/.dockerignore index c88fb144fe..f61d8aea3f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -40,9 +40,7 @@ cpu.out *.log /gitea -/gitea-vet /debug -/integrations.test /bin /dist @@ -54,12 +52,6 @@ cpu.out /indexers /log /tests/integration/gitea-integration-* -/tests/integration/indexers-* -/tests/e2e/gitea-e2e-* -/tests/e2e/indexers-* -/tests/e2e/reports -/tests/e2e/test-artifacts -/tests/e2e/test-snapshots /tests/*.ini /node_modules /yarn.lock diff --git a/.editorconfig b/.editorconfig index bf1cf757cc..703a834818 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,7 +18,7 @@ indent_style = tab [templates/custom/*.tmpl] insert_final_newline = false -[templates/swagger/v1_json.tmpl] +[templates/swagger/*_json.tmpl] indent_style = space insert_final_newline = false diff --git a/.github/actions/docker-dryrun/action.yml b/.github/actions/docker-dryrun/action.yml new file mode 100644 index 0000000000..d280ea26ce --- /dev/null +++ b/.github/actions/docker-dryrun/action.yml @@ -0,0 +1,29 @@ +name: docker-dryrun +description: Composite action that performs the container build steps for a single platform. + +inputs: + platform: + description: "The target platform: linux/amd64, linux/arm64, linux/riscv64." + required: true + +runs: + using: composite + steps: + - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - name: Build regular image + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: . + platforms: ${{ inputs.platform }} + push: false + file: Dockerfile + cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful + - name: Build rootless image + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: . + platforms: ${{ inputs.platform }} + push: false + file: Dockerfile.rootless + cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless diff --git a/.github/actions/go-cache/action.yml b/.github/actions/go-cache/action.yml new file mode 100644 index 0000000000..04d4bac367 --- /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 be33b8975f..0000000000 --- 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/labeler.yml b/.github/labeler.yml index 68a0f30fd6..937e69ef20 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,81 +1,3 @@ -modifies/docs: - - changed-files: - - any-glob-to-any-file: - - "**/*.md" - - "docs/**" - -modifies/templates: - - changed-files: - - all-globs-to-any-file: - - "templates/**" - - "!templates/swagger/v1_json.tmpl" - -modifies/api: - - changed-files: - - any-glob-to-any-file: - - "routers/api/**" - - "templates/swagger/v1_json.tmpl" - -modifies/cli: - - changed-files: - - any-glob-to-any-file: - - "cmd/**" - -modifies/translation: - - changed-files: - - any-glob-to-any-file: - - "options/locale/*.ini" - -modifies/migrations: - - changed-files: - - any-glob-to-any-file: - - "models/migrations/**" - -modifies/internal: - - changed-files: - - any-glob-to-any-file: - - ".air.toml" - - "Makefile" - - "Dockerfile" - - "Dockerfile.rootless" - - ".dockerignore" - - "docker/**" - - ".editorconfig" - - ".eslintrc.cjs" - - ".golangci.yml" - - ".gitpod.yml" - - ".markdownlint.yaml" - - ".spectral.yaml" - - "stylelint.config.*" - - ".yamllint.yaml" - - ".github/**" - - ".gitea/**" - - ".devcontainer/**" - - "build/**" - - "contrib/**" - -modifies/dependencies: - - changed-files: - - any-glob-to-any-file: - - "package.json" - - "pnpm-lock.yaml" - - "pyproject.toml" - - "uv.lock" - - "go.mod" - - "go.sum" - -modifies/go: - - changed-files: - - any-glob-to-any-file: - - "**/*.go" - -modifies/frontend: - - changed-files: - - any-glob-to-any-file: - - "*.js" - - "*.ts" - - "web_src/**" - docs-update-needed: - changed-files: - any-glob-to-any-file: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b7594a1ba7..91d001e078 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,10 +1,11 @@ Please check the following: 1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for backports. -2. Make sure you have read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md . -3. For documentations contribution, please go to https://gitea.com/gitea/docs -4. Describe what your pull request does and which issue you're targeting (if any). -5. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily. -6. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`. -7. Delete all these tips before posting. +2. Use a Conventional Commits PR title, for example `fix(repo): handle empty branch names`. +3. Make sure you have read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md . +4. For documentations contribution, please go to https://gitea.com/gitea/docs +5. Describe what your pull request does and which issue you're targeting (if any). +6. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily. +7. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`. +8. Delete all these tips before posting. diff --git a/.github/workflows/cache-seeder.yml b/.github/workflows/cache-seeder.yml new file mode 100644 index 0000000000..733077cc80 --- /dev/null +++ b/.github/workflows/cache-seeder.yml @@ -0,0 +1,71 @@ +# 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 + +permissions: + contents: read + +jobs: + gobuild: + runs-on: ubuntu-latest + 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 gogit" GOEXPERIMENT="" make backend + + lint: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - { job: lint-backend, tags: "bindata", target: "lint-backend" } + 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 c9a1f22a2a..0000000000 --- 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 ee1c3e0c75..edb6f2e157 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 0000000000..7e9a00450b --- /dev/null +++ b/.github/workflows/cron-renovate.yml @@ -0,0 +1,32 @@ +name: cron-renovate + +on: + schedule: + - cron: "23 * * * *" # hourly at :23 + workflow_dispatch: + +concurrency: + group: cron-renovate + +env: + RENOVATE_VERSION: 43.141.5 # renovate: datasource=docker depName=ghcr.io/renovatebot/renovate + +permissions: + contents: read + +jobs: + cron-renovate: + runs-on: ubuntu-latest + if: github.repository == 'go-gitea/gitea' # prevent running on forks + timeout-minutes: 30 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: renovatebot/github-action@79dc0ba74dc3de28db0a7aeb1d0b95d5bf5fde2a # v46.1.13 + 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 nolyfill)$"]' + RENOVATE_REPOSITORIES: '["go-gitea/gitea"]' diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml index 56a30fb5ba..17f29d4e0c 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 55d206bb0f..7b68a393d0 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -15,19 +15,24 @@ on: value: ${{ jobs.detect.outputs.templates }} docker: value: ${{ jobs.detect.outputs.docker }} + dockerfile: + value: ${{ jobs.detect.outputs.dockerfile }} swagger: value: ${{ jobs.detect.outputs.swagger }} yaml: value: ${{ jobs.detect.outputs.yaml }} json: value: ${{ jobs.detect.outputs.json }} + e2e: + value: ${{ jobs.detect.outputs.e2e }} + +permissions: + contents: read jobs: detect: runs-on: ubuntu-latest timeout-minutes: 3 - permissions: - contents: read outputs: backend: ${{ steps.changes.outputs.backend }} frontend: ${{ steps.changes.outputs.frontend }} @@ -35,12 +40,14 @@ jobs: actions: ${{ steps.changes.outputs.actions }} templates: ${{ steps.changes.outputs.templates }} docker: ${{ steps.changes.outputs.docker }} + dockerfile: ${{ steps.changes.outputs.dockerfile }} swagger: ${{ steps.changes.outputs.swagger }} yaml: ${{ steps.changes.outputs.yaml }} 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: | @@ -64,15 +71,15 @@ jobs: - "assets/emoji.json" - "package.json" - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" - "Makefile" - - ".eslintrc.cjs" - - ".npmrc" docs: - "**/*.md" - ".markdownlint.yaml" - "package.json" - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" actions: - ".github/workflows/*" @@ -91,12 +98,17 @@ jobs: - "docker/**" - "Makefile" + dockerfile: + - "Dockerfile" + - "Dockerfile.rootless" + swagger: - "templates/swagger/v1_json.tmpl" - "templates/swagger/v1_input.json" - "Makefile" - "package.json" - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" - ".spectral.yaml" yaml: @@ -107,3 +119,9 @@ jobs: json: - "**/*.json" + - "**/*.json5" + + e2e: + - "tests/e2e/**" + - "tools/test-e2e.sh" + - "playwright.config.ts" diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index e44a787587..8943dc6142 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -7,156 +7,79 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: + contents: read + jobs: files-changed: uses: ./.github/workflows/files-changed.yml - permissions: - contents: read lint-backend: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest - 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: TAGS="bindata" make generate-go # lint-go also lints with "bindata" tags which requires "_bindata.go" - run: make lint-backend - env: - TAGS: bindata sqlite sqlite_unlock_notify - lint-templates: - if: needs.files-changed.outputs.templates == 'true' + lint-on-demand: needs: files-changed runs-on: ubuntu-latest - permissions: - contents: read steps: - - uses: actions/checkout@v6 - - uses: astral-sh/setup-uv@v7 - - run: uv python install 3.14 - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml - - run: make deps-py - - run: make deps-frontend - - run: make lint-templates - - lint-yaml: - if: needs.files-changed.outputs.yaml == 'true' - needs: files-changed - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v6 - - uses: astral-sh/setup-uv@v7 - - run: uv python install 3.14 - - run: make deps-py - - run: make lint-yaml - - lint-json: - if: needs.files-changed.outputs.json == 'true' - needs: files-changed - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v5 - with: - node-version: 24 - - run: make deps-frontend - - run: make lint-json - - lint-swagger: - if: needs.files-changed.outputs.swagger == 'true' - needs: files-changed - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml - - run: make deps-frontend - - run: make lint-swagger - - lint-spell: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true' - needs: files-changed - runs-on: ubuntu-latest - 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: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + - run: make lint-spell - lint-go-windows: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' - needs: files-changed - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true' + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - go-version-file: go.mod - check-latest: true - - run: make deps-backend deps-tools - - run: make lint-go-windows lint-go-gitea-vet - env: - TAGS: bindata sqlite sqlite_unlock_notify - GOOS: windows - GOARCH: amd64 + python-version: 3.14 + - if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' + run: make deps-py lint-templates lint-yaml - lint-go-gogit: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' - needs: files-changed - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - check-latest: true - - run: make deps-backend deps-tools - - run: make lint-go - env: - TAGS: bindata gogit sqlite sqlite_unlock_notify + - if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.swagger == 'true' || needs.files-changed.outputs.json == 'true' + run: make deps-frontend lint-md lint-swagger lint-json + + - if: needs.files-changed.outputs.actions == 'true' + run: make lint-actions checks-backend: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest - 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 @@ -164,12 +87,10 @@ jobs: if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest - 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@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 cache: pnpm @@ -184,20 +105,21 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest - 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 - # no frontend build here as backend should be able to build - # even without any frontend files - - run: make deps-backend - - run: go build -o gitea_no_gcc # test if build succeeds without the sqlite tag + cache: false + - uses: ./.github/actions/go-cache + with: + cache-name: compliance-backend + - run: make deps-backend generate-go + # no frontend build here as backend should be able to build, even without any frontend files + # CGO is not used when cross-compile, so these steps also test if the code is compatible with CGO disabled - name: build-backend-arm64 - run: make backend # test cross compile + run: go build -o gitea_linux_arm64 env: GOOS: linux GOARCH: arm64 @@ -209,38 +131,7 @@ jobs: GOARCH: amd64 TAGS: bindata gogit - name: build-backend-386 - run: go build -o gitea_linux_386 # test if compatible with 32 bit + run: go build -o gitea_linux_386 env: GOOS: linux GOARCH: 386 - - docs: - if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true' - needs: files-changed - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml - - run: make deps-frontend - - run: make lint-md - - actions: - if: needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.actions == 'true' - needs: files-changed - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - check-latest: true - - run: make lint-actions diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index d168c2ecc5..6e91eafe3e 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -7,18 +7,17 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: + contents: read + jobs: files-changed: uses: ./.github/workflows/files-changed.yml - permissions: - contents: read test-pgsql: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest - permissions: - contents: read services: pgsql: image: postgres:14 @@ -28,25 +27,29 @@ jobs: ports: - "5432:5432" ldap: - image: gitea/test-openldap:latest + image: gitea/test-openldap:latest@sha256:4ac633b01d684e6b2a458cc0c8530c92f9b3702f6e040ce5f365607df34fbda0 ports: - "389:389" - "636:636" minio: # as github actions doesn't support "entrypoint", we need to use a non-official image # that has a custom entrypoint set to "minio server /data" - image: bitnamilegacy/minio:2023.8.31 + image: bitnamilegacy/minio:2025.7.23 env: MINIO_ROOT_USER: 123456 MINIO_ROOT_PASSWORD: 12345678 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 @@ -54,53 +57,58 @@ jobs: env: TAGS: bindata - name: run migration tests - run: make test-pgsql-migration + run: GITEA_TEST_DATABASE=pgsql make test-migration - name: run tests - run: make test-pgsql + run: GITEA_TEST_DATABASE=pgsql make test-integration timeout-minutes: 50 env: + # pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower. + GOTEST_FLAGS: -race -timeout=40m TAGS: bindata gogit - RACE_ENABLED: true - TEST_TAGS: gogit TEST_LDAP: 1 test-sqlite: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest - 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 + - run: make backend env: - TAGS: bindata gogit sqlite sqlite_unlock_notify + TAGS: bindata gogit + GOEXPERIMENT: - name: run migration tests - run: make test-sqlite-migration + run: GITEA_TEST_DATABASE=sqlite make test-migration + env: + TAGS: bindata gogit - name: run tests - run: GOEXPERIMENT='' make test-sqlite + run: GITEA_TEST_DATABASE=sqlite make test-integration timeout-minutes: 50 env: - TAGS: bindata gogit sqlite sqlite_unlock_notify - RACE_ENABLED: true - TEST_TAGS: gogit sqlite sqlite_unlock_notify + # sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow + GOTEST_FLAGS: -timeout=40m + TAGS: bindata gogit + GOEXPERIMENT: test-unit: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest - permissions: - contents: read services: elasticsearch: - image: elasticsearch:7.5.0 + image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15 env: discovery.type: single-node + xpack.security.enabled: false ports: - "9200:9200" meilisearch: @@ -110,7 +118,7 @@ jobs: ports: - "7700:7700" redis: - image: redis + image: redis:latest@sha256:94ea4f5ccdaa6b154df99a792986ecb3ffbb3fe7722a197220477f1f3e65f9fe options: >- # wait until redis has started --health-cmd "redis-cli ping" --health-interval 5s @@ -119,22 +127,27 @@ jobs: ports: - 6379:6379 minio: - image: bitnamilegacy/minio:2021.3.17 + image: bitnamilegacy/minio:2025.7.23 env: - MINIO_ACCESS_KEY: 123456 - MINIO_SECRET_KEY: 12345678 + MINIO_ROOT_USER: 123456 + MINIO_ROOT_PASSWORD: 12345678 ports: - "9000:9000" devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583 - image: mcr.microsoft.com/azure-storage/azurite:latest + image: mcr.microsoft.com/azure-storage/azurite:latest@sha256:dae2a5f96553962901304b94e72ef87e299d0825e4b679673bcc527a25076fe4 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,28 +155,27 @@ jobs: env: TAGS: bindata - name: unit-tests - run: make unit-test-coverage test-check + run: make test-backend test-check env: + GOTEST_FLAGS: -race -timeout=20m TAGS: bindata - RACE_ENABLED: true GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} - name: unit-tests-gogit - run: GOEXPERIMENT='' make unit-test-coverage test-check + run: make test-backend test-check env: + GOTEST_FLAGS: -race -timeout=20m TAGS: bindata gogit - RACE_ENABLED: true + GOEXPERIMENT: GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} test-mysql: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest - permissions: - contents: read services: mysql: # the bitnami mysql image has more options than the official one, it's easier to customize - image: bitnamilegacy/mysql:8.0 + image: bitnamilegacy/mysql:8.4 env: ALLOW_EMPTY_PASSWORD: true MYSQL_DATABASE: testgitea @@ -172,24 +184,29 @@ jobs: options: >- --mount type=tmpfs,destination=/bitnami/mysql/data elasticsearch: - image: elasticsearch:7.5.0 + image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15 env: discovery.type: single-node + xpack.security.enabled: false ports: - "9200:9200" smtpimap: - image: tabascoterrier/docker-imap-devel:latest + image: tabascoterrier/docker-imap-devel:latest@sha256:3fb7cf50b47693e7b80f6f74abea2def4d7386016931d61359864de8a0aba551 ports: - "25:25" - "143:143" - "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 @@ -197,21 +214,17 @@ jobs: env: TAGS: bindata - name: run migration tests - run: make test-mysql-migration + run: GITEA_TEST_DATABASE=mysql make test-migration - name: run tests - # run: make integration-test-coverage (at the moment, no coverage is really handled) - run: make test-mysql + run: GITEA_TEST_DATABASE=mysql make test-integration env: TAGS: bindata - RACE_ENABLED: true TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" test-mssql: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest - permissions: - contents: read services: mssql: image: mcr.microsoft.com/mssql/server:2019-latest @@ -222,24 +235,28 @@ jobs: ports: - "1433:1433" devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583 - image: mcr.microsoft.com/azure-storage/azurite:latest + image: mcr.microsoft.com/azure-storage/azurite:latest@sha256:dae2a5f96553962901304b94e72ef87e299d0825e4b679673bcc527a25076fe4 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 - run: make backend env: TAGS: bindata - - run: make test-mssql-migration + - run: GITEA_TEST_DATABASE=mssql make test-migration - name: run tests - run: make test-mssql + run: GITEA_TEST_DATABASE=mssql make test-integration timeout-minutes: 50 env: TAGS: bindata diff --git a/.github/workflows/pull-docker-dryrun.yml b/.github/workflows/pull-docker-dryrun.yml index 201825ccba..43a4f48669 100644 --- a/.github/workflows/pull-docker-dryrun.yml +++ b/.github/workflows/pull-docker-dryrun.yml @@ -7,34 +7,41 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: + contents: read + jobs: files-changed: uses: ./.github/workflows/files-changed.yml - permissions: - contents: read - container: + # QEMU-based build is slow (40-50 minutes), so run arm64 and riscv64 when dockerfile changes. + # Run amd64 when any docker-related files change, which is fast (4 minutes). + container-amd64: if: needs.files-changed.outputs.docker == 'true' - needs: files-changed + needs: [files-changed] runs-on: ubuntu-latest - permissions: - contents: read steps: - - uses: actions/checkout@v6 - - uses: docker/setup-qemu-action@v4 - - uses: docker/setup-buildx-action@v4 - - name: Build regular container image - uses: docker/build-push-action@v7 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./.github/actions/docker-dryrun 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 + platform: linux/amd64 + + container-arm64: + if: needs.files-changed.outputs.dockerfile == 'true' + needs: [files-changed] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./.github/actions/docker-dryrun with: - context: . - push: false - platforms: linux/amd64,linux/arm64,linux/riscv64 - file: Dockerfile.rootless - cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless + platform: linux/arm64 + + container-riscv64: + if: needs.files-changed.outputs.dockerfile == 'true' + needs: [files-changed] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./.github/actions/docker-dryrun + with: + platform: linux/riscv64 diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index 3472d517c1..f81026a5ae 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -7,26 +7,30 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: + contents: read + jobs: files-changed: uses: ./.github/workflows/files-changed.yml - permissions: - contents: read test-e2e: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' + if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.e2e == 'true' needs: files-changed runs-on: ubuntu-latest - 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@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 cache: pnpm @@ -34,10 +38,13 @@ jobs: - run: make deps-frontend - run: make frontend - run: make deps-backend - - run: make gitea-e2e + - run: make backend + env: + TAGS: bindata - run: make playwright - run: make test-e2e timeout-minutes: 10 env: + TAGS: bindata FORCE_COLOR: 1 GITEA_TEST_E2E_DEBUG: 1 diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml index d05483e56c..b27fc32cdb 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@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 with: sync-labels: true diff --git a/.github/workflows/pull-pr-title.yml b/.github/workflows/pull-pr-title.yml new file mode 100644 index 0000000000..59b0e78c40 --- /dev/null +++ b/.github/workflows/pull-pr-title.yml @@ -0,0 +1,28 @@ +name: pr-title + +on: + pull_request: + types: + - opened + - edited + - reopened + - synchronize + - ready_for_review + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint-pr-title: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - run: make lint-pr-title + env: + PR_TITLE: ${{ github.event.pull_request.title }} diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index eaebccd7fb..46cf147f02 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@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 cache: pnpm @@ -32,34 +32,42 @@ jobs: # xgo build - run: make release env: - TAGS: bindata sqlite sqlite_unlock_notify + TAGS: bindata - 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 }} - name: sign binaries + env: + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }} run: | for f in dist/release/*; do - echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f" + echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f" done # clean branch name to get the folder name in S3 - name: Get cleaned branch name id: clean_name + env: + REF: ${{ github.ref }} run: | - REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') + REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') 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@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - name: upload binaries to s3 + env: + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} + BRANCH: ${{ steps.clean_name.outputs.branch }} run: | - aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress + aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$BRANCH" --no-progress nightly-container: runs-on: namespace-profile-gitea-release-docker @@ -67,18 +75,20 @@ 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 + env: + REF: ${{ github.ref }} run: | - REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') + REF_NAME=$(echo "$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 +98,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 +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 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 +133,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 248fa532ee..6dfca8e6cf 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@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 cache: pnpm @@ -33,43 +33,52 @@ jobs: # xgo build - run: make release env: - TAGS: bindata sqlite sqlite_unlock_notify + TAGS: bindata - 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 }} - name: sign binaries + env: + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }} run: | for f in dist/release/*; do - echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f" + echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f" done # clean branch name to get the folder name in S3 - name: Get cleaned branch name id: clean_name + env: + REF: ${{ github.ref }} run: | - REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//') + REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//') 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@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - name: upload binaries to s3 + env: + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} + BRANCH: ${{ steps.clean_name.outputs.branch }} run: | - aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress + aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$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 - run: | - gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/* env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + gh release create "$TAG" --title "$TAG" --draft --notes-from-tag dist/release/* container: runs-on: namespace-profile-gitea-release-docker @@ -77,13 +86,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 +105,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 +121,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 +140,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 1e84ae1739..d486c2e8c5 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@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24 cache: pnpm @@ -36,43 +36,52 @@ jobs: # xgo build - run: make release env: - TAGS: bindata sqlite sqlite_unlock_notify + TAGS: bindata - 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 }} - name: sign binaries + env: + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }} run: | for f in dist/release/*; do - echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f" + echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f" done # clean branch name to get the folder name in S3 - name: Get cleaned branch name id: clean_name + env: + REF: ${{ github.ref }} run: | - REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//') + REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//') 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@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - name: upload binaries to s3 + env: + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} + BRANCH: ${{ steps.clean_name.outputs.branch }} run: | - aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress + aws s3 sync dist/release "s3://$AWS_S3_BUCKET/gitea/$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 - run: | - gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/* env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + gh release create "$TAG" --title "$TAG" --notes-from-tag dist/release/* container: runs-on: namespace-profile-gitea-release-docker @@ -80,13 +89,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 +112,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 +133,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 +152,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/.gitignore b/.gitignore index 45e8e9295f..76a7578646 100644 --- a/.gitignore +++ b/.gitignore @@ -55,10 +55,7 @@ cpu.out *.log.*.gz /gitea -/gitea-e2e -/gitea-vet /debug -/integrations.test /bin /dist @@ -79,6 +76,7 @@ cpu.out /yarn-error.log /npm-debug.log* /.pnpm-store +/public/assets/.vite /public/assets/js /public/assets/css /public/assets/fonts @@ -87,8 +85,6 @@ cpu.out /VERSION /.air -# Files and folders that were previously generated -/public/assets/img/webpack # Snapcraft /gitea_a*.txt diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 8671edc47c..0000000000 --- a/.gitpod.yml +++ /dev/null @@ -1,51 +0,0 @@ -tasks: - - name: Setup - init: | - cp -r contrib/ide/vscode .vscode - make deps - make build - command: | - gp sync-done setup - exit 0 - - name: Run backend - command: | - gp sync-await setup - - # Get the URL and extract the domain - url=$(gp url 3000) - domain=$(echo $url | awk -F[/:] '{print $4}') - - if [ -f custom/conf/app.ini ]; then - sed -i "s|^ROOT_URL =.*|ROOT_URL = ${url}/|" custom/conf/app.ini - sed -i "s|^DOMAIN =.*|DOMAIN = ${domain}|" custom/conf/app.ini - sed -i "s|^SSH_DOMAIN =.*|SSH_DOMAIN = ${domain}|" custom/conf/app.ini - sed -i "s|^NO_REPLY_ADDRESS =.*|SSH_DOMAIN = noreply.${domain}|" custom/conf/app.ini - else - mkdir -p custom/conf/ - echo -e "[server]\nROOT_URL = ${url}/" > custom/conf/app.ini - echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini - fi - export TAGS="sqlite sqlite_unlock_notify" - make watch-backend - - name: Run frontend - command: | - gp sync-await setup - make watch-frontend - openMode: split-right - -vscode: - extensions: - - editorconfig.editorconfig - - dbaeumer.vscode-eslint - - golang.go - - stylelint.vscode-stylelint - - DavidAnson.vscode-markdownlint - - Vue.volar - - ms-azuretools.vscode-docker - - vitest.explorer - - cweijan.vscode-database-client2 - - GitHub.vscode-pull-request-github - -ports: - - name: Gitea - port: 3000 diff --git a/.golangci.yml b/.golangci.yml index afd91d65e5..056a6c2a08 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -51,6 +51,14 @@ linters: desc: do not use the go-chi cache package, use gitea's cache system - pkg: github.com/pkg/errors desc: use builtin errors package instead + migrations: + files: + - '**/models/migrations/**/*.go' + deny: + - pkg: code.gitea.io/gitea/models$ + desc: migrations must not depend on the models package + - pkg: code.gitea.io/gitea/modules/structs + desc: migrations must not depend on modules/structs (API structures change over time) nolintlint: allow-unused: false require-explanation: true @@ -158,9 +166,16 @@ issues: max-same-issues: 0 formatters: enable: - - gofmt + - gci - gofumpt settings: + gci: + custom-order: true + sections: + - standard + - prefix(code.gitea.io/gitea) + - blank + - default gofumpt: extra-rules: true exclusions: @@ -170,9 +185,6 @@ formatters: - .venv - public - web_src - - third_party$ - - builtin$ - - examples$ run: timeout: 10m diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 790a49a6eb..0000000000 --- a/.npmrc +++ /dev/null @@ -1,7 +0,0 @@ -audit=false -fund=false -update-notifier=false -save-exact=true -auto-install-peers=true -dedupe-peer-dependents=false -enable-pre-post-scripts=true diff --git a/AGENTS.md b/AGENTS.md index 3db66637b7..153ce964b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,18 @@ - 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 -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 to pull request branches -- Always start issue and pull request comments with an authorship attribution +- Use Conventional Commits format for commit messages and PR titles (e.g. `type(scope): subject`) +- Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates +- Preserve existing code comments, do not remove or rewrite comments that are still relevant +- Keep comments short, prefer same-line, explain why, never narrate code +- Prefer unit tests over integration tests when logic is testable in isolation +- Aim for sub-2s local runtime for integration and e2e tests +- In TypeScript, use `!` (non-null assertion) instead of `?.`/`??` when a value is known to always exist +- For CSS layout, prefer `flex-*` helpers over per-child `tw-ml-*` / `tw-mr-*` margins; fall back to `tw-*` utilities when specificity requires `!important` +- Include authorship attribution in issue and pull request comments +- Add `Co-Authored-By` lines to all commits, indicating name and model used diff --git a/CHANGELOG.md b/CHANGELOG.md index b662cb4ad5..f807f16b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,428 @@ 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 + * Correct swagger annotations for enums, status codes, and notification state (#37030) + * Remove GET API registration-token (#36801) + * Support Actions `concurrency` syntax (#32751) + * Make PUBLIC_URL_DETECTION default to "auto" (#36955) +* SECURITY + * Bound PageSize in `ListUnadoptedRepositories` (#36884) +* FEATURES + * Support Actions `concurrency` syntax (#32751) + * Add terraform state registry (#36710) + * Instance-wide (global) info banner and maintenance mode (#36571) + * Support rendering OpenAPI spec (#36449) + * Add keyboard shortcuts for repository file and code search (#36416) + * Add support for archive-upload rpc (#36391) + * Add ability to download subpath archive (#36371) + * Add workflow dependencies visualization (#26062) (#36248) & Restyle Workflow Graph (#36912) + * Automatic generation of release notes (#35977) + * Add "Go to file", "Delete Directory" to repo file list page (#35911) + * Introduce "config edit-ini" sub command to help maintaining INI config file (#35735) + * Add button to re-run failed jobs in Actions (#36924) + * Support actions and reusable workflows from private repos (#32562) + * Add summary to action runs view (#36883) + * Add user badges (#36752) + * Add configurable permissions for Actions automatic tokens (#36173) + * Add per-runner "Disable/Pause" (#36776) + * Feature non-zipped actions artifacts (action v7 / nodejs / npm v6.2.0) (#36786) +* PERFORMANCE + * WorkflowDispatch API optionally return runid (#36706) + * Add render cache for SVG icons (#36863) + * Load `mentionValues` asynchronously (#36739) + * Lazy-load some Vue components, fix heatmap chunk loading on every page (#36719) + * Load heatmap data asynchronously (#36622) + * Use prev/next pagination for user profile activities page to speed up (#36642) + * Refactor cat-file batch operations and support `--batch-command` approach (#35775) + * Use merge tree to detect conflicts when possible (#36400) +* ENHANCEMENTS + * Implement logout redirection for reverse proxy auth setups (#36085) (#37171) + * Adds option to force update new branch in contents routes (#35592) + * Add viewer controller for mermaid (zoom, drag) (#36557) + * Add code editor setting dropdowns (#36534) + * Add `elk` layout support to mermaid (#36486) + * Add resolve/unresolve review comment API endpoints (#36441) + * Allow configuring default PR base branch (fixes #36412) (#36425) + * Add support for RPM Errata (updateinfo.xml) (#37125) + * Require additional user confirmation for making repo private (#36959) + * Add `actions.WORKFLOW_DIRS` setting (#36619) + * Avoid opening new tab when downloading actions logs (#36740) + * Implements OIDC RP-Initiated Logout (#36724) + * Show workflow link (#37070) + * Desaturate dark theme background colors (#37056) + * Refactor "org teams" page and help new users to "add member" to an org (#37051) + * Add webhook name field to improve webhook identification (#37025) (#37040) + * Make task list checkboxes clickable in the preview tab (#37010) + * Improve severity labels in Actions logs and tweak colors (#36993) + * Linkify URLs in Actions workflow logs (#36986) + * Allow text selection on checkbox labels (#36970) + * Support dark/light theme images in markdown (#36922) + * Enable native dark mode for swagger-ui (#36899) + * Rework checkbox styling, remove `input` border hover effect (#36870) + * Refactor storage content-type handling of ServeDirectURL (#36804) + * Use "Enable Gravatar" but not "Disable" (#36771) + * Use case-insensitive matching for Git error "Not a valid object name" (#36728) + * Add "Copy Source" to markup comment menu (#36726) + * Change image transparency grid to CSS (#36711) + * Add "Run" prefix for unnamed action steps (#36624) + * Persist actions log time display settings in `localStorage` (#36623) + * Use first commit title for multi-commit PRs and fix auto-focus title field (#36606) + * Improve BuildCaseInsensitiveLike with lowercase (#36598) + * Improve diff highlighting (#36583) + * Exclude cancelled runs from failure-only email notifications (#36569) + * Use full-file highlighting for diff sections (#36561) + * Color command/error logs in Actions log (#36538) + * Add paging headers (#36521) + * Improve timeline entries for WIP prefix changes in pull requests (#36518) + * Add FOLDER_ICON_THEME configuration option (#36496) + * Normalize guessed languages for code highlighting (#36450) + * Add chunked transfer encoding support for LFS uploads (#36380) + * Indicate when only optional checks failed (#36367) + * Add 'allow_maintainer_edit' API option for creating a pull request (#36283) + * Support closing keywords with URL references (#36221) + * Improve diff file headers (#36215) + * Fix and enhance comment editor monospace toggle (#36181) + * Add git.DIFF_RENAME_SIMILARITY_THRESHOLD option (#36164) + * Add matching pair insertion to markdown textarea (#36121) + * Add sorting/filtering to admin user search API endpoint (#36112) + * Allow action user have read permission in public repo like other user (#36095) + * Disable matchBrackets in monaco (#36089) + * Use GitHub-style commit message for squash merge (#35987) + * Make composer registry support tar.gz and tar.bz2 and fix bugs (#35958) + * Add GITEA_PR_INDEX env variable to githooks (#35938) + * Add proper error message if session provider can not be created (#35520) + * Add button to copy file name in PR files (#35509) + * Move `X_FRAME_OPTIONS` setting from `cors` to `security` section (#30256) + * Add placeholder content for empty content page (#37114) + * Add `DEFAULT_DELETE_BRANCH_AFTER_MERGE` setting (#36917) + * Redirect to the only OAuth2 provider when no other login methods and fix various problems (#36901) + * Add admin badge to navbar avatar (#36790) + * Add `never` option to `PUBLIC_URL_DETECTION` configuration (#36785) + * Add background and run count to actions list page (#36707) + * Add icon to buttons "Close with Comment", "Close Pull Request", "Close Issue" (#36654) + * Add support for in_progress event in workflow_run webhook (#36979) + * Report commit status for pull_request_review events (#36589) + * Render merged pull request title as such in dashboard feed (#36479) + * Feature to be able to filter project boards by milestones (#36321) + * Use user id in noreply emails (#36550) + * Enable pagination on GiteaDownloader.getIssueReactions() (#36549) + * Remove striped tables in UI (#36509) + * Improve control char rendering and escape button styling (#37094) + * Support legacy run/job index-based URLs and refactor migration 326 (#37008) + * Add date to "No Contributions" tooltip (#36190) + * Show edit page confirmation dialog on tree view file change (#36130) + * Mention proc-receive in text for dashboard.resync_all_hooks func (#35991) + * Reuse selectable style for wiki (#35990) + * Support blue yellow colorblind theme (#35910) + * Support selecting theme on the footer (#35741) + * Improve online runner check (#35722) + * Add quick approve button on PR page (#35678) + * Enable commenting on expanded lines in PR diffs (#35662) + * Print PR-Title into tooltip for actions (#35579) + * Use explicit, stronger defaults for newly generated repo signing keys for Debian (#36236) + * Improve the compare page (#36261) + * Unify repo names in system notices (#36491) + * Move package settings to package instead of being tied to version (#37026) + * Add Actions API rerun endpoints for runs and jobs (#36768) + * Add branch_count to repository API (#35351) (#36743) + * Add created_by filter to SearchIssues (#36670) + * Allow admins to rename non-local users (#35970) + * Support updating branch via API (#35951) + * Add an option to automatically verify SSH keys from LDAP (#35927) + * Make "update file" API can create a new file when SHA is not set (#35738) + * Update issue.go with labels documentation (labels content, not ids) (#35522) + * Expose content_version for optimistic locking on issue and PR edits (#37035) + * Pass ServeHeaderOptions by value instead of pointer, fine tune httplib tests (#36982) +* BUGFIXES + * Frontend iframe renderer framework: 3D models, OpenAPI (#37233) (#37273) + * Fix CODEOWNERS absolute path matching. (#37244) (#37264) + * Swift registry metadata: preserve more JSON fields and accept empty metadata (#37254) (#37261) + * Fix user ssh key exporting and tests (#37256) (#37258) + * Fix team member avatar size and add tooltip (#37253) + * Fix commit title rendering in action run and blame (#37243) (#37251) + * Fix corrupted JSON caused by goccy library (#37214) (#37220) + * Add test for "fetch redirect", add CSS value validation for external render (#37207) (#37216) + * Fix incorrect concurrency check (#37205) (#37215) + * Fix handle missing base branch in PR commits API (#37193) (#37203) + * Fix encoding for Matrix Webhooks (#37190) (#37201) + * Fix handle fork-only commits in compare API (#37185) (#37199) + * Indicate form field readonly via background, fix RunUser config (#37175, #37180) (#37178) + * Report structurally invalid workflows to users (#37116) (#37164) + * Fix API not persisting pull request unit config when has_pull_requests is not set (#36718) + * Rename CSS variables and improve colorblind themes (#36353) + * Hide `add-matcher` and `remove-matcher` from actions job logs (#36520) + * Prevent navigation keys from triggering actions during IME composition (#36540) + * Fix vertical alignment of `.commit-sign-badge` children (#36570) + * Fix duplicate startup warnings in admin panel (#36641) + * Fix CODEOWNERS review request attribution using comment metadata (#36348) + * Fix HTML tags appearing in wiki table of contents (#36284) + * Fix various bugs (#37096) + * Fix various legacy problems (#37092) + * Fix RPM Registry 404 when package name contains 'package' (#37087) + * Merge some standalone Vite entries into index.js (#37085) + * Fix various problems (#37077) + * Fix issue label deletion with Actions tokens (#37013) + * Hide delete branch or tag buttons in mirror or archived repositories. (#37006) + * Fix org contact email not clearable once set (#36975) + * Fix a bug when forking a repository in an organization (#36950) + * Preserve sort order of exclusive labels from template repo (#36931) + * Make container registry support Apple Container (basic auth) (#36920) + * Fix the wrong push commits in the pull request when force push (#36914) + * Add class "list-header-filters" to the div for projects (#36889) + * Fix dbfs error handling (#36844) + * Fix incorrect viewed files counter if reverted change was viewed (#36819) + * Refactor avatar package, support default avatar fallback (#36788) + * Fix README symlink resolution in subdirectories like .github (#36775) + * Fix CSS stacking context issue in actions log (#36749) + * Add gpg signing for merge rebase and update by rebase (#36701) + * Delete non-exist branch should return 404 (#36694) + * Fix `TestActionsCollaborativeOwner` (#36657) + * Fix multi-arch Docker build SIGILL by splitting frontend stage (#36646) + * Fix linguist-detectable attribute being ignored for configuration files (#36640) + * Fix state desync in ComboMarkdownEditor (#36625) + * Unify DEFAULT_SHOW_FULL_NAME output in templates and dropdown (#36597) + * Pull Request Pusher should be the author of the merge (#36581) + * Fix various version parsing problems (#36553) + * Fix highlight diff result (#36539) + * Fix mirror sync parser and fix mirror messages (#36504) + * Fix bug when list pull request commits (#36485) + * Fix various bugs (#36446) + * Fix issue filter menu layout (#36426) + * Restrict branch naming when new change matches with protection rules (#36405) + * Fix link/origin referrer and login redirect (#36279) + * Generate IDs for HTML headings without id attribute (#36233) + * Use a migration test instead of a wrong test which populated the meta test repositories and fix a migration bug (#36160) + * Fix issue close timeline icon (#36138) + * Fix diff blob excerpt expansion (#35922) + * Fix external render (#35727) + * Fix review request webhook bug (#35339) (#35723) + * Fix shutdown waitgroup panic (#35676) + * Cleanup ActionRun creation (#35624) + * Fix possible bug when migrating issues/pull requests (#33487) + * Various fixes (#36697) + * Apply notify/register mail flags during install load (#37120) + * Repair duration display for bad stopped timestamps (#37121) + * Fix(upgrade.sh): use HTTPS for GPG key import and restore SELinux context after upgrade (#36930) + * Fix various trivial problems (#36921) + * Fix various trivial problems (#36953) + * Fix NuGet package upload error handling (#37074) + * Fix CodeQL code scanning alerts (#36858) + * Refactor issue sidebar and fix various problems (#37045) + * Fix various problems (#37029) + * Fix relative-time RangeError (#37021) + * Fix chroma lexer mapping (#36629) + * Fix typos and grammar in English locale (#36751) + * Fix milestone/project text overflow in issue sidebar (#36741) + * Fix `no-content` message not rendering after comment edit (#36733) + * Fix theme loading in development (#36605) + * Fix workflow run jobs API returning null steps (#36603) + * Fix timeline event layout overflow with long content (#36595) + * Fix minor UI issues in runner edit page (#36590) + * Fix incorrect vendored detections (#36508) + * Fix editorconfig not respected in PR Conversation view (#36492) + * Don't create self-references in merged PRs (#36490) + * Fix potential incorrect runID in run status update (#36437) + * Fix file-tree ui error when adding files to repo without commits (#36312) + * Improve image captcha contrast for dark mode (#36265) + * Fix panic in blame view when a file has only a single commit (#36230) + * Fix spelling error in migrate-storage cmd utility (#36226) + * Fix code highlighting on blame page (#36157) + * Fix nilnil in onedev downloader (#36154) + * Fix actions lint (#36029) + * Fix oauth2 session gob register (#36017) + * Fix Arch repo pacman.conf snippet (#35825) + * Fix a number of `strictNullChecks`-related issues (#35795) + * Fix URLJoin, markup render link reoslving, sign-in/up/linkaccount page common data (#36861) + * Hide delete directory button for mirror or archive repository and disable the menu item if user have no permission (#36384) + * Update message severity colors, fix navbar double border (#37019) + * Inline and lazy-load EasyMDE CSS, fix border colors (#36714) + * Closed milestones with no issues now show as 100% completed (#36220) + * Add test for ExtendCommentTreePathLength migration and fix bugs (#35791) + * Only turn links to current instance into hash links (#36237) + * Fix typos in code comments: doesnt, dont, wont (#36890) +* REFACTOR + * Clean up and improve non-gitea js error filter (#37148) (#37155) + * Always show owner/repo name in compare page dropdowns (#37172) (#37200) + * Remove dead CSS rules (#37173) (#37177) + * Replace Monaco with CodeMirror (#36764) + * Replace CSRF cookie with `CrossOriginProtection` (#36183) + * Replace index with id in actions routes (#36842) + * Remove unnecessary function parameter (#35765) + * Move jobparser from act repository to Gitea (#36699) + * Refactor compare router param parse (#36105) + * Optimize 'refreshAccesses' to perform update without removing then adding (#35702) + * Clean up checkbox cursor styles (#37016) + * Remove undocumented support of signing key in the repository git configuration file (#36143) + * Switch `cmd/` to use constructor functions. (#36962) + * Use `relative-time` to render absolute dates (#36238) + * Some refactors about GetMergeBase (#36186) + * Some small refactors (#36163) + * Use gitRepo as parameter instead of repopath when invoking sign functions (#36162) + * Move blame to gitrepo (#36161) + * Move some functions to gitrepo package to reduce RepoPath reference directly (#36126) + * Use gitrepo's clone and push when possible (#36093) + * Remove mermaid margin workaround (#35732) + * Move some functions to gitrepo package (#35543) + * Move GetDiverging functions to gitrepo (#35524) + * Use global lock instead of status pool for cron lock (#35507) + * Use explicit mux instead of DefaultServeMux (#36276) + * Use gitrepo's push function (#36245) + * Pass request context to generateAdditionalHeadersForIssue (#36274) + * Move assign project when creating pull request to the same database transaction (#36244) + * Move catfile batch to a sub package of git module (#36232) + * Use gitrepo.Repository instead of wikipath (#35398) + * Use experimental go json v2 library (#35392) + * Refactor template render (#36438) + * Refactor GetRepoRawDiffForFile to avoid unnecessary pipe or goroutine (#36434) + * Refactor text utility classes to Tailwind CSS (#36703) + * Refactor git command stdio pipe (#36422) + * Refactor git command context & pipeline (#36406) + * Refactor git command stdio pipe (#36393) + * Remove unused functions (#36672) + * Refactor Actions Token Access (#35688) + * Move commit related functions to gitrepo package (#35600) + * Move archive function to repo_model and gitrepo (#35514) + * Move some functions to gitrepo package (#35503) + * Use git model to detect whether branch exist instead of gitrepo method (#35459) + * Some refactor for repo path (#36251) + * Extract helper functions from SearchIssues (#36158) + * Refactor merge conan and container auth preserve actions taskID (#36560) + * Refactor Nuget Auth to reuse Basic Auth Token Validation (#36558) + * Refactor ActionsTaskID (#36503) + * Refactor auth middleware (#36848) + * Refactor code render and render control chars (#37078) + * Clean up AppURL, remove legacy origin-url webcomponent (#37090) + * Remove `util.URLJoin` and replace all callers with direct path concatenation (#36867) + * Replace legacy tw-flex utility classes with flex-text-block/inline (#36778) + * Mark unused&immature activitypub as "not implemented" (#36789) +* TESTING + * Add e2e tests for server push events (#36879) + * Rework e2e tests (#36634) + * Add e2e reaction test, improve accessibility, enable parallel testing (#37081) + * Increase e2e test timeouts on CI to fix flaky tests (#37053) +* BUILD + * Upgrade go-git to v5.18.0 (#37269) + * Replace rollup-plugin-license with rolldown-license-plugin (#37130) (#37158) + * Bump min go version to 1.26.2 (#37139) (#37143) + * Convert locale files from ini to json format (#35489) + * Bump golangci-lint to 2.7.2, enable modernize stringsbuilder (#36180) + * Port away from `flake-utils` (#35675) + * Remove nolint (#36252) + * Update the Unlicense copy to latest version (#36636) + * Update to go 1.26.0 and golangci-lint 2.9.0 (#36588) + * Replace `google/go-licenses` with custom generation (#36575) + * Update go dependencies (#36548) + * Bump appleboy/git-push-action from 1.0.0 to 1.2.0 (#36306) + * Remove fomantic form module (#36222) + * Bump setup-node to v6, re-enable cache (#36207) + * Bump crowdin/github-action from 1 to 2 (#36204) + * Revert "Bump alpine to 3.23 (#36185)" (#36202) + * Update chroma to v2.21.1 (#36201) + * Bump astral-sh/setup-uv from 6 to 7 (#36198) + * Bump docker/build-push-action from 5 to 6 (#36197) + * Bump aws-actions/configure-aws-credentials from 4 to 5 (#36196) + * Bump dev-hanz-ops/install-gh-cli-action from 0.1.0 to 0.2.1 (#36195) + * Add JSON linting (#36192) + * Enable dependabot for actions (#36191) + * Bump alpine to 3.23 (#36185) + * Update chroma to v2.21.0 (#36171) + * Update JS deps and eslint enhancements (#36147) + * Update JS deps (#36091) + * update golangci-lint to v2.7.0 (#36079) + * Update JS deps, fix deprecations (#36040) + * Update JS deps (#35978) + * Add toolchain directive to go.mod (#35901) + * Move `gitea-vet` to use `go tool` (#35878) + * Update to go 1.25.4 (#35877) + * Enable TypeScript `strictNullChecks` (#35843) + * Enable `vue/require-typed-ref` eslint rule (#35764) + * Update JS dependencies (#35759) + * Move `codeformat` folder to tools (#35758) + * Update dependencies (#35733) + * Bump happy-dom from 20.0.0 to 20.0.2 (#35677) + * Bump setup-go to v6 (#35660) + * Update JS deps, misc tweaks (#35643) + * Bump happy-dom from 19.0.2 to 20.0.0 (#35625) + * Use bundled version of spectral (#35573) + * Update JS and PY deps (#35565) + * Bump github.com/wneessen/go-mail from 0.6.2 to 0.7.1 (#35557) + * Migrate from webpack to vite (#37002) + * Update JS dependencies and misc tweaks (#37064) + * Update to eslint 10 (#36925) + * Optimize Docker build with dependency layer caching (#36864) + * Update JS deps (#36850) + * Update tool dependencies and fix new lint issues (#36702) + * Remove redundant linter rules (#36658) + * Move Fomantic dropdown CSS to custom module (#36530) + * Remove and forbid `@ts-expect-error` (#36513) + * Refactor git command stderr handling (#36402) + * Enable gocheckcompilerdirectives linter (#36156) + * Replace `lint-go-gopls` with additional `govet` linters (#36028) + * Update golangci-lint to v2.6.0 (#35801) + * Misc tool tweaks (#35734) + * Add cache to container build (#35697) + * Upgrade vite (#37126) + * Update `setup-uv` to v8.0.0 (#37101) + * Upgrade `go-git` to v5.17.2 and related dependencies (#37060) + * Raise minimum Node.js version to 22.18.0 (#37058) + * Upgrade `golang.org/x/image` to v0.38.0 (#37054) + * Update minimum go version to 1.26.1, golangci-lint to 2.11.2, fix test style (#36876) + * Enable eslint concurrency (#36878) + * Vendor relative-time-element as local web component (#36853) + * Update material-icon-theme v5.32.0 (#36832) + * Update Go dependencies (#36781) + * Upgrade minimatch (#36760) + * Remove i18n backport tool at the moment because of translation format changed (#36643) + * Update emoji data for Unicode 16 (#36596) + * Update JS dependencies, adjust webpack config, misc fixes (#36431) + * Update material-icon-theme to v5.31.0 (#36427) + * Update JS and PY deps (#36383) + * Bump alpine to 3.23, add platforms to `docker-dryrun` (#36379) + * Update JS deps (#36354) + * Update goldmark to v1.7.16 (#36343) + * Update chroma to v2.22.0 (#36342) +* DOCS + * Update AI Contribution Policy (#37022) + * Update AGENTS.md with additional guidelines (#37018) + * Add missing cron tasks to example ini (#37012) + * Add AI Contribution Policy to CONTRIBUTING.md (#36651) + * Minor punctuation improvement in CONTRIBUTING.md (#36291) + * Add documentation for markdown anchor post-processing (#36443) +* MISC + * Correct spelling (#36783) + * Update Nix flake (#37110) + * Update Nix flake (#37024) + * Add valid github scopes (#36977) + * Update Nix flake (#36943) + * Update Nix flake (#36902) + * Update Nix flake (#36857) + * Update Nix flake (#36787) + ## [1.25.5](https://github.com/go-gitea/gitea/releases/tag/v1.25.5) - 2026-03-10 * SECURITY @@ -961,7 +1383,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com). * Fix mCaptcha bug (#33659) (#33661) * Git graph: don't show detached commits (#33645) (#33650) * Use MatchPhraseQuery for bleve code search (#33628) - * Adjust appearence of commit status webhook (#33778) #33789 + * Adjust appearance of commit status webhook (#33778) #33789 * Upgrade golang net from 0.35.0 -> 0.36.0 (#33795) #33796 ## [1.23.4](https://github.com/go-gitea/gitea/releases/tag/v1.23.4) - 2025-02-16 @@ -1692,7 +2114,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com). * Optimize repo-list layout to enhance visual experience (#31272) (#31276) * fixed the dropdown menu for the top New button to expand to the left (#31273) (#31275) * Fix Activity Page Contributors dropdown (#31264) (#31269) - * fix: allow actions artifacts storage migration to complete succesfully (#31251) (#31257) + * fix: allow actions artifacts storage migration to complete successfully (#31251) (#31257) * Make blockquote attention recognize more syntaxes (#31240) (#31250) * Remove .segment from .project-column (#31204) (#31239) * Ignore FindRecentlyPushedNewBranches err (#31164) (#31171) @@ -1876,7 +2298,7 @@ Key highlights of this release encompass significant changes categorized under ` * Performance optimization for git push and check permissions for push options (#30104) (#30354) * BUGFIXES * Fix close file in the Upload func (#30262) (#30269) - * Fix inline math blocks can't be preceeded/followed by alphanumerical characters (#30175) (#30250) + * Fix inline math blocks can't be preceded/followed by alphanumerical characters (#30175) (#30250) * Fix missing 0 prefix of GPG key id (#30245) (#30247) * Include encoding in signature payload (#30174) (#30181) * Move from `max( id )` to `max( index )` for latest commit statuses (#30076) (#30155) @@ -5168,7 +5590,7 @@ Key highlights of this release encompass significant changes categorized under ` * Fix navbar on project view (#17749) * More pleasantly handle broken or missing git repositories (#17747) * Use `*PushUpdateOptions` as receiver (#17724) - * Remove unused `user` paramater (#17723) + * Remove unused `user` parameter (#17723) * Better builtin avatar generator (#17707) * Cleanup and use global style on popups (#17674) * Move user/org deletion to services (#17673) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33b329182c..27103a991b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,14 @@ # Contribution Guidelines +This document explains how to contribute changes to the Gitea project. Topic-specific guides live in separate files so the essentials are easier to find. + +| Topic | Document | +| :---- | :------- | +| Backend (Go modules, API v1) | [docs/guideline-backend.md](docs/guideline-backend.md) | +| Frontend (npm, UI guidelines) | [docs/guideline-frontend.md](docs/guideline-frontend.md) | +| Maintainers, TOC, labels, merge queue, commit format for mergers | [docs/community-governance.md](docs/community-governance.md) | +| Release cycle, backports, tagging releases | [docs/release-management.md](docs/release-management.md) | +
Table of Contents - [Contribution Guidelines](#contribution-guidelines) @@ -11,10 +20,6 @@ - [Discuss your design before the implementation](#discuss-your-design-before-the-implementation) - [Issue locking](#issue-locking) - [Building Gitea](#building-gitea) - - [Dependencies](#dependencies) - - [Backend](#backend) - - [Frontend](#frontend) - - [Design guideline](#design-guideline) - [Styleguide](#styleguide) - [Copyright](#copyright) - [Testing](#testing) @@ -22,47 +27,19 @@ - [Code review](#code-review) - [Pull request format](#pull-request-format) - [PR title and summary](#pr-title-and-summary) - - [Milestone](#milestone) - - [Labels](#labels) - [Breaking PRs](#breaking-prs) - [What is a breaking PR?](#what-is-a-breaking-pr) - [How to handle breaking PRs?](#how-to-handle-breaking-prs) - [Maintaining open PRs](#maintaining-open-prs) - - [Getting PRs merged](#getting-prs-merged) - - [Final call](#final-call) - - [Commit messages](#commit-messages) - - [PR Co-authors](#pr-co-authors) - - [PRs targeting `main`](#prs-targeting-main) - - [Backport PRs](#backport-prs) + - [Reviewing PRs](#reviewing-prs) + - [For PR authors](#for-pr-authors) - [Documentation](#documentation) - - [API v1](#api-v1) - - [GitHub API compatibility](#github-api-compatibility) - - [Adding/Maintaining API routes](#addingmaintaining-api-routes) - - [When to use what HTTP method](#when-to-use-what-http-method) - - [Requirements for API routes](#requirements-for-api-routes) - - [Backports and Frontports](#backports-and-frontports) - - [What is backported?](#what-is-backported) - - [How to backport?](#how-to-backport) - - [Format of backport PRs](#format-of-backport-prs) - - [Frontports](#frontports) - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) - - [Release Cycle](#release-cycle) - - [Maintainers](#maintainers) - - [Technical Oversight Committee (TOC)](#technical-oversight-committee-toc) - - [TOC election process](#toc-election-process) - - [Current TOC members](#current-toc-members) - - [Previous TOC/owners members](#previous-tocowners-members) - - [Governance Compensation](#governance-compensation) - - [TOC \& Working groups](#toc--working-groups) - - [Roadmap](#roadmap) - - [Versions](#versions) - - [Releasing Gitea](#releasing-gitea)
## Introduction -This document explains how to contribute changes to the Gitea project. \ It assumes you have followed the [installation instructions](https://docs.gitea.com/category/installation). \ Sensitive security-related issues should be reported to [security@gitea.io](mailto:security@gitea.io). @@ -70,16 +47,18 @@ For configuring IDEs for Gitea development, see the [contributed IDE configurati ## AI Contribution Policy -Contributions made with the assistance of AI tools are welcome, but contributors must use them responsibly. +Contributions made with the assistance of AI tools are welcome, but contributors must use them responsibly and disclose that use clearly. -1. Include related issues or pull requests in the prompt so that the AI has ideal context. -2. Review AI-generated code closely before submitting a pull request. -3. Manually test the changes and add appropriate automated tests where feasible. -4. Only use AI to assist in contributions that you understand well enough to respond to feedback without relying on AI. -5. Indicate AI-generated content in issue and pull requests descriptions and comments. Specify which model was used. -6. Do not use AI to reply to questions about your issue or pull request. The questions are for you, not an AI model. +1. Review AI-generated code closely before marking a pull request ready for review. +2. Manually test the changes and add appropriate automated tests where feasible. +3. Only use AI to assist in contributions that you understand well enough to explain, defend, and revise yourself during review. +4. Disclose AI-assisted content clearly. +5. Do not use AI to reply to questions about your issue or pull request. The questions are for you, not an AI model. +6. AI may be used to help draft issues and pull requests, but contributors remain responsible for the accuracy, completeness, and intent of what they submit. -Maintainers reserve the right to close pull requests and issues that appear to be low-quality AI-generated content. We welcome new contributors, but cannot sustain the effort of supporting contributors who primarily defer to AI rather than engaging substantively with the review process. +Maintainers reserve the right to close pull requests and issues that do not disclose AI assistance, that appear to be low-quality AI-generated content, or where the contributor cannot explain or defend the proposed changes themselves. + +We welcome new contributors, but cannot sustain the effort of supporting contributors who primarily defer to AI rather than engaging substantively with the review process. ## Issues @@ -129,34 +108,6 @@ If further discussion is needed, we encourage you to open a new issue instead an See the [development setup instructions](https://docs.gitea.com/development/hacking-on-gitea). -## Dependencies - -### Backend - -Go dependencies are managed using [Go Modules](https://go.dev/cmd/go/#hdr-Module_maintenance). \ -You can find more details in the [go mod documentation](https://go.dev/ref/mod) and the [Go Modules Wiki](https://github.com/golang/go/wiki/Modules). - -Pull requests should only modify `go.mod` and `go.sum` where it is related to your change, be it a bugfix or a new feature. \ -Apart from that, these files should only be modified by Pull Requests whose only purpose is to update dependencies. - -The `go.mod`, `go.sum` update needs to be justified as part of the PR description, -and must be verified by the reviewers and/or merger to always reference -an existing upstream commit. - -### Frontend - -For the frontend, we use [npm](https://www.npmjs.com/). - -The same restrictions apply for frontend dependencies as for backend dependencies, with the exceptions that the files for it are `package.json` and `package-lock.json`, and that new versions must always reference an existing version. - -## Design guideline - -Depending on your change, please read the - -- [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend) -- [frontend development guideline](https://docs.gitea.com/contributing/guidelines-frontend) -- [refactoring guideline](https://docs.gitea.com/contributing/guidelines-refactoring) - ## Styleguide You should always run `make fmt` before committing to conform to Gitea's styleguide. @@ -188,18 +139,19 @@ Here's how to run the test suite: - run tests (we suggest running them on Linux) -| Command | Action | | -| :------------------------------------------ | :------------------------------------------------------- | ------------------------------------------- | -|``make test[\#SpecificTestName]`` | run unit test(s) | | -|``make test-sqlite[\#SpecificTestName]`` | run [integration](tests/integration) test(s) for SQLite | [More details](tests/integration/README.md) | -|``make test-e2e`` | run [end-to-end](tests/e2e) test(s) using Playwright | | +| Command | Action | | +|:----------------------------------------------|:-----------------------------------------------------| ------------------------------------------- | +| ``make test-backend[\#SpecificTestName]`` | run unit test(s) | | +| ``make test-integration[\#SpecificTestName]`` | run [integration](tests/integration) test(s) | [More details](tests/integration/README.md) | +| ``make test-e2e`` | run [end-to-end](tests/e2e) test(s) using Playwright | | - E2E test environment variables -| Variable | Description | -| :------------------------ | :---------------------------------------------------------------- | -| ``GITEA_TEST_E2E_DEBUG`` | When set, show Gitea server output | -| ``GITEA_TEST_E2E_FLAGS`` | Additional flags passed to Playwright, for example ``--ui`` | +| Variable | Description | +| :-------------------------------- | :---------------------------------------------------------- | +| ``GITEA_TEST_E2E_DEBUG`` | When set, show Gitea server output | +| ``GITEA_TEST_E2E_FLAGS`` | Additional flags passed to Playwright, for example ``--ui`` | +| ``GITEA_TEST_E2E_TIMEOUT_FACTOR`` | Timeout multiplier (default: 4 on CI, 1 locally) | ## Translation @@ -213,6 +165,8 @@ The tool `go run build/backport-locale.go` can be used to backport locales from ## Code review +How labels, milestones, and the merge queue work is documented in [docs/community-governance.md](docs/community-governance.md). + ### Pull request format Please try to make your pull request easy to review for us. \ @@ -235,6 +189,22 @@ In the PR title, describe the problem you are fixing, not how you are fixing it. Use the first comment as a summary of your PR. \ In the PR summary, you can describe exactly how you are fixing this problem. +PR titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format, because PRs are squash-merged and the PR title becomes the resulting commit message: + +```text +type(scope)!: subject +``` + +The allowed types are `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, and `test`. The generic `chore` type is intentionally not accepted; pick a more descriptive type instead. + +Examples: + +```text +fix(web): prevent avatar upload crash on empty file +feat(api): add pagination to repo hooks list +ci(workflows): lint PR titles with commitlint +``` + Keep this summary up-to-date as the PR evolves. \ If your PR changes the UI, you must add **after** screenshots in the PR summary. \ If you are not implementing a new feature, you should also post **before** screenshots for comparison. @@ -257,29 +227,6 @@ Fixes/Closes/Resolves #. to your summary. \ Each issue that will be closed must stand on a separate line. -### Milestone - -A PR should only be assigned to a milestone if it will likely be merged into the given version. \ -As a rule of thumb, assume that a PR will stay open for an additional month for every 100 added lines. \ -PRs without a milestone may not be merged. - -### Labels - -Almost all labels used inside Gitea can be classified as one of the following: - -- `modifies/…`: Determines which parts of the codebase are affected. These labels will be set through the CI. -- `topic/…`: Determines the conceptual component of Gitea that is affected, i.e. issues, projects, or authentication. At best, PRs should only target one component but there might be overlap. Must be set manually. -- `type/…`: Determines the type of an issue or PR (feature, refactoring, docs, bug, …). If GitHub supported scoped labels, these labels would be exclusive, so you should set **exactly** one, not more or less (every PR should fall into one of the provided categories, and only one). -- `issue/…` / `pr/…`: Labels that are specific to issues or PRs respectively and that are only necessary in a given context, i.e. `issue/not-a-bug` or `pr/need-2-approvals` - -Every PR should be labeled correctly with every label that applies. - -There are also some labels that will be managed automatically.\ -In particular, these are - -- the amount of pending required approvals -- has all `backport`s or needs a manual backport - ### Breaking PRs #### What is a breaking PR? @@ -308,165 +255,29 @@ Breaking PRs will not be merged as long as not both of these requirements are me ### Maintaining open PRs -The moment you create a non-draft PR or the moment you convert a draft PR to a non-draft PR is the moment code review starts for it. \ -Once that happens, do not rebase or squash your branch anymore as it makes it difficult to review the new changes. \ -Merge the base branch into your branch only when you really need to, i.e. because of conflicting changes in the mean time. \ -This reduces unnecessary CI runs. \ -Don't worry about merge commits messing up your commit history as every PR will be squash merged. \ -This means that all changes are joined into a single new commit whose message is as described below. +Code review starts when you open a non-draft PR or move a draft out of draft state. After that, do not rebase or squash your branch; it makes new changes harder to review. -### Getting PRs merged +Merge the base branch into yours only when you need to, for example because of conflicting changes elsewhere. That limits unnecessary CI runs. -Changes to Gitea must be reviewed before they are accepted — no matter who -makes the change, even if they are an owner or a maintainer. \ -The only exception are critical bugs that prevent Gitea from being compiled or started. \ -Specifically, we require two approvals from maintainers for every PR. \ -Once this criteria has been met, your PR receives the `lgtm/done` label. \ -From this point on, your only responsibility is to fix merge conflicts or respond to/implement requests by maintainers. \ -It is the responsibility of the maintainers from this point to get your PR merged. +Every PR is squash-merged, so merge commits on your branch do not matter for final history. The squash produces a single commit; mergers follow the [commit message format](docs/community-governance.md#commit-messages) in the governance guide. -If a PR has the `lgtm/done` label and there are no open discussions or merge conflicts anymore, any maintainer can add the `reviewed/wait-merge` label. \ -This label means that the PR is part of the merge queue and will be merged as soon as possible. \ -The merge queue will be cleared in the order of the list below: +### Reviewing PRs - +Maintainers are encouraged to review pull requests in areas where they have expertise or particular interest. -Gitea uses it's own tool, the to automate parts of the review process. \ -This tool does the things listed below automatically: +#### For PR authors -- create a backport PR if needed once the initial PR was merged -- remove the PR from the merge queue after the PR merged -- keep the oldest branch in the merge queue up to date with merges +- **Response**: When answering reviewer questions, use real-world cases or examples and avoid speculation. +- **Discussion**: A discussion is always welcome and should be used to clarify the changes and the intent of the PR. +- **Help**: If you need help with the PR or comments are unclear, ask for clarification. -### Final call - -If a PR has been ignored for more than 7 days with no comments or reviews, and the author or any maintainer believes it will not survive a long wait (such as a refactoring PR), they can send "final call" to the TOC by mentioning them in a comment. - -After another 7 days, if there is still zero approval, this is considered a polite refusal, and the PR will be closed to avoid wasting further time. Therefore, the "final call" has a cost, and should be used cautiously. - -However, if there are no objections from maintainers, the PR can be merged with only one approval from the TOC (not the author). - -### Commit messages - -Mergers are able and required to rewrite the PR title and summary (the first comment of a PR) so that it can produce an easily understandable commit message if necessary. \ -The final commit message should no longer contain any uncertainty such as `hopefully, won't happen anymore`. Replace uncertainty with certainty. - -#### PR Co-authors - -A person counts as a PR co-author the moment they (co-)authored a commit that is not simply a `Merge base branch into branch` commit. \ -Mergers are required to remove such "false-positive" co-authors when writing the commit message. \ -The true co-authors must remain in the commit message. - -#### PRs targeting `main` - -The commit message of PRs targeting `main` is always - -```bash -$PR_TITLE ($PR_INDEX) - -$REWRITTEN_PR_SUMMARY -``` - -#### Backport PRs - -The commit message of backport PRs is always - -```bash -$PR_TITLE ($INITIAL_PR_INDEX) ($BACKPORT_PR_INDEX) - -$REWRITTEN_PR_SUMMARY -``` +Guidance for reviewers, the merge queue, and the squash commit message format is in [docs/community-governance.md](docs/community-governance.md). ## Documentation If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated in another PR at [https://gitea.com/gitea/docs](https://gitea.com/gitea/docs). **The docs directory on main repository will be removed at some time. We will have a yaml file to store configuration file's meta data. After that completed, configuration documentation should be in the main repository.** -## API v1 - -The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest). - -### GitHub API compatibility - -Gitea's API should use the same endpoints and fields as the GitHub API as far as possible, unless there are good reasons to deviate. \ -If Gitea provides functionality that GitHub does not, a new endpoint can be created. \ -If information is provided by Gitea that is not provided by the GitHub API, a new field can be used that doesn't collide with any GitHub fields. \ -Updating an existing API should not remove existing fields unless there is a really good reason to do so. \ -The same applies to status responses. If you notice a problem, feel free to leave a comment in the code for future refactoring to API v2 (which is currently not planned). - -### Adding/Maintaining API routes - -All expected results (errors, success, fail messages) must be documented ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L319-L327)). \ -All JSON input types must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L76-L91)) \ -and referenced in [routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go). \ -They can then be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L318). \ -All JSON responses must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L36-L68)) \ -and referenced in its category in [routers/api/v1/swagger/](routers/api/v1/swagger/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16)) \ -They can be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279). - -### When to use what HTTP method - -In general, HTTP methods are chosen as follows: - -- **GET** endpoints return the requested object(s) and status **OK (200)** -- **DELETE** endpoints return the status **No Content (204)** and no content either -- **POST** endpoints are used to **create** new objects (e.g. a User) and return the status **Created (201)** and the created object -- **PUT** endpoints are used to **add/assign** existing Objects (e.g. a user to a team) and return the status **No Content (204)** and no content either -- **PATCH** endpoints are used to **edit/change** an existing object and return the changed object and the status **OK (200)** - -### Requirements for API routes - -All parameters of endpoints changing/editing an object must be optional (except the ones to identify the object, which are required). - -Endpoints returning lists must - -- support pagination (`page` & `limit` options in query) -- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444)) - -## Backports and Frontports - -### What is backported? - -We backport PRs given the following circumstances: - -1. Feature freeze is active, but `-rc0` has not been released yet. Here, we backport as much as possible. -2. `rc0` has been released. Here, we only backport bug- and security-fixes, and small enhancements. Large PRs such as refactors are not backported anymore. -3. We never backport new features. -4. We never backport breaking changes except when - 1. The breaking change has no effect on the vast majority of users - 2. The component triggering the breaking change is marked as experimental - -### How to backport? - -In the past, it was necessary to manually backport your PRs. \ -Now, that's not a requirement anymore as our [backport bot](https://github.com/GiteaBot) tries to create backports automatically once the PR is merged when the PR - -- does not have the label `backport/manual` -- has the label `backport/` - -The `backport/manual` label signifies either that you want to backport the change yourself, or that there were conflicts when backporting, thus you **must** do it yourself. - -### Format of backport PRs - -The title of backport PRs should be - -``` - (#) -``` - -The first two lines of the summary of the backporting PR should be - -``` -Backport # - -``` - -with the rest of the summary and labels matching the original PR. - -### Frontports - -Frontports behave exactly as described above for backports. - ## Developer Certificate of Origin (DCO) We consider the act of contributing to the code by submitting a Pull Request as the "Sign off" or agreement to the certifications and terms of the [DCO](DCO) and [MIT license](LICENSE). \ @@ -480,148 +291,3 @@ Signed-off-by: Joe Smith If you set the `user.name` and `user.email` Git config options, you can add the line to the end of your commits automatically with `git commit -s`. We assume in good faith that the information you provide is legally binding. - -## Release Cycle - -We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. \ -The overall goal is to make a major release every three or four months, which breaks down into two or three months of general development followed by one month of testing and polishing known as the release freeze. \ -All the feature pull requests should be -merged before feature freeze. All feature pull requests haven't been merged before this feature freeze will be moved to next milestone, please notice our feature freeze announcement on discord. And, during the frozen period, a corresponding -release branch is open for fixes backported from main branch. Release candidates -are made during this period for user testing to -obtain a final version that is maintained in this branch. - -During a development cycle, we may also publish any necessary minor releases -for the previous version. For example, if the latest, published release is -v1.2, then minor changes for the previous release—e.g., v1.1.0 -> v1.1.1—are -still possible. - -## Maintainers - -To make sure every PR is checked, we have [maintainers](MAINTAINERS). \ -Every PR **must** be reviewed by at least two maintainers (or owners) before it can get merged. \ -For refactoring PRs after a week and documentation only PRs, the approval of only one maintainer is enough. \ -A maintainer should be a contributor of Gitea and contributed at least -4 accepted PRs. A contributor should apply as a maintainer in the -[Discord](https://discord.gg/Gitea) `#develop` channel. The team maintainers may invite the contributor. A maintainer -should spend some time on code reviews. If a maintainer has no -time to do that, they should apply to leave the maintainers team -and we will give them the honor of being a member of the [advisors -team](https://github.com/orgs/go-gitea/teams/advisors). Of course, if -an advisor has time to code review, we will gladly welcome them back -to the maintainers team. If a maintainer is inactive for more than 3 -months and forgets to leave the maintainers team, the owners may move -him or her from the maintainers team to the advisors team. -For security reasons, Maintainers should use 2FA for their accounts and -if possible provide GPG signed commits. -https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/ -https://help.github.com/articles/signing-commits-with-gpg/ - -Furthermore, any account with write access (like bots and TOC members) **must** use 2FA. -https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/ - -## Technical Oversight Committee (TOC) - -At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company. -https://blog.gitea.com/quarterly-23q1/ - -### TOC election process - -Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company. -A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election. -If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate. - -The TOC is elected for one year, the TOC election happens yearly. -After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat. -If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat. -Refusals result in the person with the next highest vote getting the same choice. -As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat. - -If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration. - -### Current TOC members - -- 2024-01-01 ~ 2024-12-31 - - Company - - [Jason Song](https://gitea.com/wolfogre) - - [Lunny Xiao](https://gitea.com/lunny) - - [Matti Ranta](https://gitea.com/techknowlogick) - - Community - - [6543](https://gitea.com/6543) <6543@obermui.de> - - [delvh](https://gitea.com/delvh) - - [John Olheiser](https://gitea.com/jolheiser) - -### Previous TOC/owners members - -Here's the history of the owners and the time they served: - -- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 -- [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017 -- [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017 -- [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801) -- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 -- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 -- [6543](https://gitea.com/6543) - 2023 -- [John Olheiser](https://gitea.com/jolheiser) - 2023 -- [Jason Song](https://gitea.com/wolfogre) - 2023 - -## Governance Compensation - -Each member of the community elected TOC will be granted $500 each month as compensation for their work. - -Furthermore, any community release manager for a specific release or LTS will be compensated $500 for the delivery of said release. - -These funds will come from community sources like the OpenCollective rather than directly from the company. -Only non-company members are eligible for this compensation, and if a member of the community TOC takes the responsibility of release manager, they would only be compensated for their TOC duties. -Gitea Ltd employees are not eligible to receive any funds from the OpenCollective unless it is reimbursement for a purchase made for the Gitea project itself. - -## TOC & Working groups - -With Gitea covering many projects outside of the main repository, several groups will be created to help focus on specific areas instead of requiring maintainers to be a jack-of-all-trades. Maintainers are of course more than welcome to be part of multiple groups should they wish to contribute in multiple places. - -The currently proposed groups are: - -- **Core Group**: maintain the primary Gitea repository -- **Integration Group**: maintain the Gitea ecosystem's related tools, including go-sdk/tea/changelog/bots etc. -- **Documentation Group**: maintain related documents and repositories -- **Translation Group**: coordinate with translators and maintain translations -- **Security Group**: managed by TOC directly, members are decided by TOC, maintains security patches/responsible for security items - -## Roadmap - -Each year a roadmap will be discussed with the entire Gitea maintainers team, and feedback will be solicited from various stakeholders. -TOC members need to review the roadmap every year and work together on the direction of the project. - -When a vote is required for a proposal or other change, the vote of community elected TOC members count slightly more than the vote of company elected TOC members. With this approach, we both avoid ties and ensure that changes align with the mission statement and community opinion. - -You can visit our roadmap on the wiki. - -## Versions - -Gitea has the `main` branch as a tip branch and has version branches -such as `release/v1.19`. `release/v1.19` is a release branch and we will -tag `v1.19.0` for binary download. If `v1.19.0` has bugs, we will accept -pull requests on the `release/v1.19` branch and publish a `v1.19.1` tag, -after bringing the bug fix also to the main branch. - -Since the `main` branch is a tip version, if you wish to use Gitea -in production, please download the latest release tag version. All the -branches will be protected via GitHub, all the PRs to every branch must -be reviewed by two maintainers and must pass the automatic tests. - -## Releasing Gitea - -- Let $vmaj, $vmin and $vpat be Major, Minor and Patch version numbers, $vpat should be rc1, rc2, 0, 1, ...... $vmaj.$vmin will be kept the same as milestones on github or gitea in future. -- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours. -- If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps: - - Create `-dev` tag as `git tag -s -F release.notes v$vmaj.$vmin.0-dev` and push the tag as `git push origin v$vmaj.$vmin.0-dev`. - - When CI has finished building tag then you have to create a new branch named `release/v$vmaj.$vmin` -- If it is bugfix version create PR for changelog on branch `release/v$vmaj.$vmin` and wait till it is reviewed and merged. -- Add a tag as `git tag -s -F release.notes v$vmaj.$vmin.$`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`. -- And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.) -- If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version. -- Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release. -- Verify all release assets were correctly published through CI on dl.gitea.com and GitHub releases. Once ACKed: - - bump the version of https://dl.gitea.com/gitea/version.json - - merge the blog post PR - - announce the release in discord `#announcements` diff --git a/Dockerfile b/Dockerfile index 9922cee9c4..c69b5a65d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ # syntax=docker/dockerfile:1 -# Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack +# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build RUN apk --no-cache add build-base git nodejs pnpm WORKDIR /src -COPY package.json pnpm-lock.yaml .npmrc ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile COPY --exclude=.git/ . . RUN make frontend @@ -12,7 +12,7 @@ RUN make frontend FROM docker.io/library/golang:1.26-alpine3.23 AS build-env ARG GITEA_VERSION -ARG TAGS="sqlite sqlite_unlock_notify" +ARG TAGS="" ENV TAGS="bindata timetzdata $TAGS" ARG CGO_EXTRA_CFLAGS diff --git a/Dockerfile.rootless b/Dockerfile.rootless index a1742e3d51..6f029067de 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,9 +1,9 @@ # syntax=docker/dockerfile:1 -# Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack +# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build RUN apk --no-cache add build-base git nodejs pnpm WORKDIR /src -COPY package.json pnpm-lock.yaml .npmrc ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile COPY --exclude=.git/ . . RUN make frontend @@ -12,7 +12,7 @@ RUN make frontend FROM docker.io/library/golang:1.26-alpine3.23 AS build-env ARG GITEA_VERSION -ARG TAGS="sqlite sqlite_unlock_notify" +ARG TAGS="" ENV TAGS="bindata timetzdata $TAGS" ARG CGO_EXTRA_CFLAGS diff --git a/Makefile b/Makefile index 4d1bd96ea5..809300d880 100644 --- a/Makefile +++ b/Makefile @@ -7,33 +7,41 @@ export GOEXPERIMENT ?= jsonv2 GO ?= go SHASUM ?= shasum -a 256 -HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes) COMMA := , -XGO_VERSION := go-1.25.x +XGO_VERSION := go-1.26.x -AIR_PACKAGE ?= github.com/air-verse/air@v1 -EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 -GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.2 -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 -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 - -DOCKER_IMAGE ?= gitea/gitea -DOCKER_TAG ?= latest -DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG) +AIR_PACKAGE ?= github.com/air-verse/air@v1.65.1 # renovate: datasource=go +EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.6.1 # renovate: datasource=go +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go +GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go +MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.2 # renovate: datasource=go +XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go +GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.0 # renovate: datasource=go +ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go +HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes) ifeq ($(HAS_GO), yes) CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766 CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS) endif +MAKE_EVIDENCE_DIR := .make_evidence + +# Use sqlite as default database if running tests, only do so for local tests, not in CI. +# CI should explicitly set the database to avoid unexpected results. +ifneq ($(findstring test-,$(MAKECMDGOALS)),) + ifeq ($(CI),) + GITEA_TEST_DATABASE ?= sqlite + endif +endif + +TAGS ?= +TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags + CGO_ENABLED ?= 0 -ifneq (,$(findstring sqlite,$(TAGS))$(findstring pam,$(TAGS))) +ifneq (,$(findstring sqlite_mattn,$(TAGS))$(findstring pam,$(TAGS))) CGO_ENABLED = 1 endif @@ -50,15 +58,16 @@ else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows) IS_WINDOWS := yes endif endif + +# GOFLAGS and EXTRA_GOFLAGS are for the 'go build' command only ifeq ($(IS_WINDOWS),yes) GOFLAGS := -v -buildmode=exe EXECUTABLE ?= gitea.exe - EXECUTABLE_E2E ?= gitea-e2e.exe else GOFLAGS := -v EXECUTABLE ?= gitea - EXECUTABLE_E2E ?= gitea-e2e endif +EXTRA_GOFLAGS ?= ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu) SED_INPLACE := sed -i @@ -66,30 +75,14 @@ else SED_INPLACE := sed -i '' endif -EXTRA_GOFLAGS ?= - -MAKE_EVIDENCE_DIR := .make_evidence - -GOTESTFLAGS ?= -ifeq ($(RACE_ENABLED),true) - GOFLAGS += -race - GOTESTFLAGS += -race -endif +# GOTEST_FLAGS is for unit test and integration test +GOTEST_FLAGS ?= -timeout 40m STORED_VERSION_FILE := VERSION GITHUB_REF_TYPE ?= branch GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) -# Enable typescript support in Node.js before 22.18 -# TODO: Remove this once we can raise the minimum Node.js version to 22.18 (alpine >= 3.23) -NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v 2>/dev/null | cut -c2- | sed 's/-.*//' | tr '.' ' ')) -ifeq ($(shell test "$(NODE_VERSION)" -lt "022018000"; echo $$?),0) - NODE_VARS := NODE_OPTIONS="--experimental-strip-types" -else - NODE_VARS := -endif - ifneq ($(GITHUB_REF_TYPE),branch) VERSION ?= $(subst v,,$(GITHUB_REF_NAME)) GITEA_VERSION ?= $(VERSION) @@ -120,10 +113,11 @@ LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/r GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration,$(shell $(GO) list ./... | grep -v /vendor/)) MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) -WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f) -WEBPACK_CONFIGS := webpack.config.ts tailwind.config.ts -WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css -WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts +FRONTEND_SOURCES := $(shell find web_src/js web_src/css -type f) +FRONTEND_CONFIGS := vite.config.ts tailwind.config.ts +FRONTEND_DEST := public/assets/.vite/manifest.json +FRONTEND_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts public/assets/.vite +FRONTEND_DEV_LOG_LEVEL ?= warn BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.* @@ -135,12 +129,6 @@ AIR_TMP_DIR := .air GO_LICENSE_FILE := assets/go-licenses.json -TAGS ?= -TAGS_SPLIT := $(subst $(COMMA), ,$(TAGS)) -TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags - -TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify - TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) GO_DIRS := build cmd models modules routers services tests tools @@ -160,6 +148,7 @@ ESLINT_CONCURRENCY ?= 2 SWAGGER_SPEC := templates/swagger/v1_json.tmpl SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json SWAGGER_EXCLUDE := code.gitea.io/sdk +OPENAPI3_SPEC := templates/swagger/v1_openapi3_json.tmpl TEST_MYSQL_HOST ?= mysql:3306 TEST_MYSQL_DBNAME ?= testgitea @@ -172,13 +161,19 @@ TEST_PGSQL_PASSWORD ?= postgres TEST_PGSQL_SCHEMA ?= gtestschema TEST_MINIO_ENDPOINT ?= minio:9000 TEST_MSSQL_HOST ?= mssql:1433 -TEST_MSSQL_DBNAME ?= gitea +TEST_MSSQL_DBNAME ?= testgitea TEST_MSSQL_USERNAME ?= sa TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1 # Include local Makefile # Makefile.local is listed in .gitignore -sinclude Makefile.local +ifneq ("$(wildcard Makefile.local)","") + include Makefile.local +endif + +$(foreach v, $(filter TEST_%, $(.VARIABLES)), $(eval MAKEFILE_VARS+=$v=$($v))) +$(foreach v, $(filter GITEA_TEST_%, $(.VARIABLES)), $(eval MAKEFILE_VARS+=$v=$($v))) +export MAKEFILE_VARS .PHONY: all all: build @@ -187,34 +182,21 @@ all: build help: Makefile ## print Makefile help information. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m[TARGETS] default target: build\033[0m\n\n\033[35mTargets:\033[0m\n"} /^[0-9A-Za-z._-]+:.*?##/ { printf " \033[36m%-45s\033[0m %s\n", $$1, $$2 }' Makefile #$(MAKEFILE_LIST) @printf " \033[36m%-46s\033[0m %s\n" "test-e2e" "test end to end using playwright" - @printf " \033[36m%-46s\033[0m %s\n" "test[#TestSpecificName]" "run unit test" - @printf " \033[36m%-46s\033[0m %s\n" "test-sqlite[#TestSpecificName]" "run integration test for sqlite" - -.PHONY: git-check -git-check: - @if git lfs >/dev/null 2>&1 ; then : ; else \ - echo "Gitea requires git with lfs support to run tests." ; \ - exit 1; \ - fi + @printf " \033[36m%-46s\033[0m %s\n" "test-backend[#TestSpecificName]" "run unit test (sqlite only)" + @printf " \033[36m%-46s\033[0m %s\n" "test-integration[#TestSpecificName]" "run integration test for GITEA_TEST_DATABASE (sqlite, mysql, pgsql, mssql)" .PHONY: clean-all clean-all: clean ## delete backend, frontend and integration files - rm -rf $(WEBPACK_DEST_ENTRIES) node_modules + rm -rf $(FRONTEND_DEST_ENTRIES) node_modules .PHONY: clean clean: ## delete backend and integration files - rm -rf $(EXECUTABLE) $(EXECUTABLE_E2E) $(DIST) $(BINDATA_DEST_WILDCARD) \ - integrations*.test \ - tests/integration/gitea-integration-* \ - tests/integration/indexers-* \ - tests/sqlite.ini tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \ - tests/e2e/gitea-e2e-*/ \ - tests/e2e/indexers-*/ \ - tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/ + rm -f $(EXECUTABLE) test-*.test tests/*.ini + rm -rf $(DIST) $(BINDATA_DEST_WILDCARD) man tests/integration/gitea-integration-* .PHONY: fmt fmt: ## format the Go and template code - @GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run tools/code-batch-process.go gitea-fmt -w '{file-list}' + $(GO) run $(GOLANGCI_LINT_PACKAGE) fmt $(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl')) @# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only @# whitespace before it @@ -242,7 +224,7 @@ TAGS_PREREQ := $(TAGS_EVIDENCE) endif .PHONY: generate-swagger -generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments +generate-swagger: $(SWAGGER_SPEC) $(OPENAPI3_SPEC) ## generate the swagger spec from code comments $(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT) $(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)' @@ -264,6 +246,21 @@ swagger-validate: ## check if the swagger spec is valid $(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' @$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath +.PHONY: generate-openapi3 +generate-openapi3: $(OPENAPI3_SPEC) ## generate the OpenAPI 3.0 spec from the Swagger 2.0 spec + +$(OPENAPI3_SPEC): $(SWAGGER_SPEC) build/generate-openapi.go $(wildcard build/openapi3gen/*.go) + $(GO) run build/generate-openapi.go + +.PHONY: openapi3-check +openapi3-check: generate-openapi3 + @diff=$$(git diff --color=always '$(OPENAPI3_SPEC)'); \ + if [ -n "$$diff" ]; then \ + echo "Please run 'make generate-openapi3' and commit the result:"; \ + printf "%s" "$${diff}"; \ + exit 1; \ + fi + .PHONY: checks checks: checks-frontend checks-backend ## run various consistency checks @@ -271,10 +268,10 @@ checks: checks-frontend checks-backend ## run various consistency checks checks-frontend: lockfile-check svg-check ## check frontend files .PHONY: checks-backend -checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files +checks-backend: tidy-check swagger-check openapi3-check fmt-check swagger-validate security-check ## check backend files .PHONY: lint -lint: lint-frontend lint-backend lint-spell ## lint everything +lint: lint-frontend lint-backend lint-templates lint-swagger lint-spell lint-md lint-actions lint-json lint-yaml ## lint everything .PHONY: lint-fix lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues @@ -286,40 +283,44 @@ lint-frontend: lint-js lint-css ## lint frontend files lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues .PHONY: lint-backend -lint-backend: lint-go lint-go-gitea-vet lint-editorconfig ## lint backend files +lint-backend: lint-go lint-editorconfig ## lint backend files .PHONY: lint-backend-fix -lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues +lint-backend-fix: lint-go-fix lint-editorconfig ## lint backend files and fix issues .PHONY: lint-js lint-js: node_modules ## lint js and ts files - $(NODE_VARS) pnpm exec eslint --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) $(ESLINT_FILES) - $(NODE_VARS) pnpm exec vue-tsc + pnpm exec eslint --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) $(ESLINT_FILES) + pnpm exec vue-tsc .PHONY: lint-js-fix lint-js-fix: node_modules ## lint js and ts files and fix issues - $(NODE_VARS) pnpm exec eslint --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) $(ESLINT_FILES) --fix - $(NODE_VARS) pnpm exec vue-tsc + pnpm exec eslint --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) $(ESLINT_FILES) --fix + pnpm exec vue-tsc .PHONY: lint-css lint-css: node_modules ## lint css files - $(NODE_VARS) pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES) + pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES) .PHONY: lint-css-fix lint-css-fix: node_modules ## lint css files and fix issues - $(NODE_VARS) pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix + pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix .PHONY: lint-swagger lint-swagger: node_modules ## lint swagger files - $(NODE_VARS) pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC) + pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC) .PHONY: lint-md lint-md: node_modules ## lint markdown files - $(NODE_VARS) pnpm exec markdownlint *.md + pnpm exec markdownlint *.md .PHONY: lint-md-fix lint-md-fix: node_modules ## lint markdown files and fix issues - $(NODE_VARS) pnpm exec markdownlint --fix *.md + pnpm exec markdownlint --fix *.md + +.PHONY: lint-pr-title +lint-pr-title: ## lint PR title against Conventional Commits (set PR_TITLE=...) + @node ./tools/lint-pr-title.js .PHONY: lint-spell lint-spell: ## lint spelling @@ -331,23 +332,11 @@ lint-spell-fix: ## lint spelling and fix issues .PHONY: lint-go lint-go: ## lint go files - $(GO) run $(GOLANGCI_LINT_PACKAGE) run + GO=$(GO) GOLANGCI_LINT_PACKAGE=$(GOLANGCI_LINT_PACKAGE) $(GO) run ./tools/lint-go-all.go .PHONY: lint-go-fix lint-go-fix: ## lint go files and fix issues - $(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix - -# workaround step for the lint-go-windows CI task because 'go run' can not -# have distinct GOOS/GOARCH for its build and run steps -.PHONY: lint-go-windows -lint-go-windows: - @GOOS= GOARCH= $(GO) install $(GOLANGCI_LINT_PACKAGE) - golangci-lint run - -.PHONY: lint-go-gitea-vet -lint-go-gitea-vet: ## lint go files with gitea-vet - @echo "Running gitea-vet..." - @$(GO) vet -vettool="$(shell GOOS= GOARCH= go tool -n gitea-vet)" ./... + GO=$(GO) GOLANGCI_LINT_PACKAGE=$(GOLANGCI_LINT_PACKAGE) $(GO) run ./tools/lint-go-all.go --fix .PHONY: lint-editorconfig lint-editorconfig: @@ -355,8 +344,9 @@ lint-editorconfig: @$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES) .PHONY: lint-actions -lint-actions: ## lint action workflow files - $(GO) run $(ACTIONLINT_PACKAGE) +lint-actions: .venv ## lint action workflow files + @$(GO) run $(ACTIONLINT_PACKAGE) + @uv run --frozen zizmor --quiet --min-confidence=medium .github .PHONY: lint-templates lint-templates: .venv node_modules ## lint template files @@ -369,36 +359,32 @@ lint-yaml: .venv ## lint yaml files .PHONY: lint-json lint-json: node_modules ## lint json files - $(NODE_VARS) pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) + pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) .PHONY: lint-json-fix lint-json-fix: node_modules ## lint and fix json files - $(NODE_VARS) pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) --fix + pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) --fix .PHONY: watch watch: ## watch everything and continuously rebuild @bash tools/watch.sh .PHONY: watch-frontend -watch-frontend: node_modules ## watch frontend files and continuously rebuild - @rm -rf $(WEBPACK_DEST_ENTRIES) - NODE_ENV=development $(NODE_VARS) pnpm exec webpack --watch --progress --disable-interpret +watch-frontend: node_modules ## start vite dev server for frontend + NODE_ENV=development pnpm exec vite --logLevel $(FRONTEND_DEV_LOG_LEVEL) .PHONY: watch-backend watch-backend: ## watch backend files and continuously rebuild GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml -.PHONY: test -test: test-frontend test-backend ## test everything - .PHONY: test-backend test-backend: ## test backend files - @echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." - @$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES) + @echo "Running go test with $(GOTEST_FLAGS) -tags '$(TAGS)'..." + @$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' $(GO_TEST_PACKAGES) .PHONY: test-frontend test-frontend: node_modules ## test frontend files - $(NODE_VARS) pnpm exec vitest + pnpm exec vitest .PHONY: test-check test-check: @@ -412,10 +398,10 @@ test-check: exit 1; \ fi -.PHONY: test\#% -test\#%: - @echo "Running go test with -tags '$(TEST_TAGS)'..." - @$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES) +.PHONY: test-backend\#% +test-backend\#%: + @echo "Running go test with -tags '$(TAGS)'..." + @$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES) .PHONY: coverage coverage: @@ -425,8 +411,8 @@ coverage: .PHONY: unit-test-coverage unit-test-coverage: - @echo "Running unit-test-coverage $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." - @$(GO) test $(GOTESTFLAGS) -timeout=20m -tags='$(TEST_TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1 + @echo "Running unit-test-coverage $(GOTEST_FLAGS) -tags '$(TAGS)'..." + @$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1 .PHONY: tidy tidy: ## run go mod tidy @@ -453,199 +439,47 @@ go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses $(GO_LICENSE_FILE): go.mod go.sum GO=$(GO) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_FILE) -generate-ini-sqlite: - sed -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-sqlite|g' \ - -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \ - tests/sqlite.ini.tmpl > tests/sqlite.ini +.PHONY: test-integration +test-integration: + @# Use a compiled binary: testlogger forwards gitea logs to t.Log, so `go test -v` + @# would flood output per passing test. testcache can't help these tests anyway — + @# they mutate the work directory, so cache inputs change between runs. + $(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -c code.gitea.io/gitea/tests/integration -o ./test-integration-$(GITEA_TEST_DATABASE).test + ./test-integration-$(GITEA_TEST_DATABASE).test -.PHONY: test-sqlite -test-sqlite: integrations.sqlite.test generate-ini-sqlite - GITEA_TEST_CONF=tests/sqlite.ini ./integrations.sqlite.test +.PHONY: test-integration\#% +test-integration\#%: + $(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -run $(subst .,/,$*) code.gitea.io/gitea/tests/integration -.PHONY: test-sqlite\#% -test-sqlite\#%: integrations.sqlite.test generate-ini-sqlite - GITEA_TEST_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.run $(subst .,/,$*) +.PHONY: test-migration +test-migration: migrations.integration.test migrations.individual.test -.PHONY: test-sqlite-migration -test-sqlite-migration: migrations.sqlite.test migrations.individual.sqlite.test +.PHONY: migrations.integration.test +migrations.integration.test: + $(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' code.gitea.io/gitea/tests/integration/migration-test -generate-ini-mysql: - sed -e 's|{{TEST_MYSQL_HOST}}|${TEST_MYSQL_HOST}|g' \ - -e 's|{{TEST_MYSQL_DBNAME}}|${TEST_MYSQL_DBNAME}|g' \ - -e 's|{{TEST_MYSQL_USERNAME}}|${TEST_MYSQL_USERNAME}|g' \ - -e 's|{{TEST_MYSQL_PASSWORD}}|${TEST_MYSQL_PASSWORD}|g' \ - -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-mysql|g' \ - -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \ - tests/mysql.ini.tmpl > tests/mysql.ini +.PHONY: migrations.individual.test +migrations.individual.test: + @# tests of multiple packages use the same database, don't run in parallel + $(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) -.PHONY: test-mysql -test-mysql: integrations.mysql.test generate-ini-mysql - GITEA_TEST_CONF=tests/mysql.ini ./integrations.mysql.test - -.PHONY: test-mysql\#% -test-mysql\#%: integrations.mysql.test generate-ini-mysql - GITEA_TEST_CONF=tests/mysql.ini ./integrations.mysql.test -test.run $(subst .,/,$*) - -.PHONY: test-mysql-migration -test-mysql-migration: migrations.mysql.test migrations.individual.mysql.test - -generate-ini-pgsql: - sed -e 's|{{TEST_PGSQL_HOST}}|${TEST_PGSQL_HOST}|g' \ - -e 's|{{TEST_PGSQL_DBNAME}}|${TEST_PGSQL_DBNAME}|g' \ - -e 's|{{TEST_PGSQL_USERNAME}}|${TEST_PGSQL_USERNAME}|g' \ - -e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \ - -e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|g' \ - -e 's|{{TEST_MINIO_ENDPOINT}}|${TEST_MINIO_ENDPOINT}|g' \ - -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-pgsql|g' \ - -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \ - tests/pgsql.ini.tmpl > tests/pgsql.ini - -.PHONY: test-pgsql -test-pgsql: integrations.pgsql.test generate-ini-pgsql - GITEA_TEST_CONF=tests/pgsql.ini ./integrations.pgsql.test - -.PHONY: test-pgsql\#% -test-pgsql\#%: integrations.pgsql.test generate-ini-pgsql - GITEA_TEST_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.run $(subst .,/,$*) - -.PHONY: test-pgsql-migration -test-pgsql-migration: migrations.pgsql.test migrations.individual.pgsql.test - -generate-ini-mssql: - sed -e 's|{{TEST_MSSQL_HOST}}|${TEST_MSSQL_HOST}|g' \ - -e 's|{{TEST_MSSQL_DBNAME}}|${TEST_MSSQL_DBNAME}|g' \ - -e 's|{{TEST_MSSQL_USERNAME}}|${TEST_MSSQL_USERNAME}|g' \ - -e 's|{{TEST_MSSQL_PASSWORD}}|${TEST_MSSQL_PASSWORD}|g' \ - -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-mssql|g' \ - -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \ - tests/mssql.ini.tmpl > tests/mssql.ini - -.PHONY: test-mssql -test-mssql: integrations.mssql.test generate-ini-mssql - GITEA_TEST_CONF=tests/mssql.ini ./integrations.mssql.test - -.PHONY: test-mssql\#% -test-mssql\#%: integrations.mssql.test generate-ini-mssql - GITEA_TEST_CONF=tests/mssql.ini ./integrations.mssql.test -test.run $(subst .,/,$*) - -.PHONY: test-mssql-migration -test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test +.PHONY: migrations.individual.test\#% +migrations.individual.test\#%: + $(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' code.gitea.io/gitea/models/migrations/$* .PHONY: playwright playwright: deps-frontend - @# on GitHub Actions VMs, playwright's system deps are pre-installed - @$(NODE_VARS) pnpm exec playwright install $(if $(GITHUB_ACTIONS),,--with-deps) chromium $(if $(CI),firefox) $(PLAYWRIGHT_FLAGS) + @./tools/test-e2e.sh install .PHONY: test-e2e -test-e2e: playwright $(EXECUTABLE_E2E) - @EXECUTABLE=$(EXECUTABLE_E2E) ./tools/test-e2e.sh $(GITEA_TEST_E2E_FLAGS) - -.PHONY: bench-sqlite -bench-sqlite: integrations.sqlite.test generate-ini-sqlite - GITEA_TEST_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . - -.PHONY: bench-mysql -bench-mysql: integrations.mysql.test generate-ini-mysql - GITEA_TEST_CONF=tests/mysql.ini ./integrations.mysql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . - -.PHONY: bench-mssql -bench-mssql: integrations.mssql.test generate-ini-mssql - GITEA_TEST_CONF=tests/mssql.ini ./integrations.mssql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . - -.PHONY: bench-pgsql -bench-pgsql: integrations.pgsql.test generate-ini-pgsql - GITEA_TEST_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . - -.PHONY: integration-test-coverage -integration-test-coverage: integrations.cover.test generate-ini-mysql - GITEA_TEST_CONF=tests/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out - -.PHONY: integration-test-coverage-sqlite -integration-test-coverage-sqlite: integrations.cover.sqlite.test generate-ini-sqlite - GITEA_TEST_CONF=tests/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out - -integrations.mysql.test: git-check $(GO_SOURCES) - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mysql.test - -integrations.pgsql.test: git-check $(GO_SOURCES) - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.pgsql.test - -integrations.mssql.test: git-check $(GO_SOURCES) - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mssql.test - -integrations.sqlite.test: git-check $(GO_SOURCES) - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.sqlite.test -tags '$(TEST_TAGS)' - -integrations.cover.test: git-check $(GO_SOURCES) - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -coverpkg $(shell echo $(GO_TEST_PACKAGES) | tr ' ' ',') -o integrations.cover.test - -integrations.cover.sqlite.test: git-check $(GO_SOURCES) - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -coverpkg $(shell echo $(GO_TEST_PACKAGES) | tr ' ' ',') -o integrations.cover.sqlite.test -tags '$(TEST_TAGS)' - -.PHONY: migrations.mysql.test -migrations.mysql.test: $(GO_SOURCES) generate-ini-mysql - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mysql.test - GITEA_TEST_CONF=tests/mysql.ini ./migrations.mysql.test - -.PHONY: migrations.pgsql.test -migrations.pgsql.test: $(GO_SOURCES) generate-ini-pgsql - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.pgsql.test - GITEA_TEST_CONF=tests/pgsql.ini ./migrations.pgsql.test - -.PHONY: migrations.mssql.test -migrations.mssql.test: $(GO_SOURCES) generate-ini-mssql - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mssql.test - GITEA_TEST_CONF=tests/mssql.ini ./migrations.mssql.test - -.PHONY: migrations.sqlite.test -migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)' - GITEA_TEST_CONF=tests/sqlite.ini ./migrations.sqlite.test - -.PHONY: migrations.individual.mysql.test -migrations.individual.mysql.test: $(GO_SOURCES) generate-ini-mysql - GITEA_TEST_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) - -.PHONY: migrations.individual.sqlite.test\#% -migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite - GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* - -.PHONY: migrations.individual.pgsql.test -migrations.individual.pgsql.test: $(GO_SOURCES) generate-ini-pgsql - GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) - -.PHONY: migrations.individual.pgsql.test\#% -migrations.individual.pgsql.test\#%: $(GO_SOURCES) generate-ini-pgsql - GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* - -.PHONY: migrations.individual.mssql.test -migrations.individual.mssql.test: $(GO_SOURCES) generate-ini-mssql - GITEA_TEST_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) - -.PHONY: migrations.individual.mssql.test\#% -migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql - GITEA_TEST_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* - -.PHONY: migrations.individual.sqlite.test -migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite - GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) - -.PHONY: migrations.individual.sqlite.test\#% -migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite - GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* - -.PHONY: check -check: test - -.PHONY: install $(TAGS_PREREQ) -install: $(wildcard *.go) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' +test-e2e: playwright frontend backend + @EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh run $(GITEA_TEST_E2E_FLAGS) .PHONY: build build: frontend backend ## build everything .PHONY: frontend -frontend: $(WEBPACK_DEST) ## build frontend files +frontend: $(FRONTEND_DEST) ## build frontend files .PHONY: backend backend: generate-backend $(EXECUTABLE) ## build backend files @@ -672,9 +506,6 @@ ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),) endif CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ -$(EXECUTABLE_E2E): $(GO_SOURCES) - CGO_ENABLED=1 $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TEST_TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ - .PHONY: release release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check @@ -739,7 +570,6 @@ deps-backend: ## install backend dependencies deps-tools: ## install tool dependencies $(GO) install $(AIR_PACKAGE) & \ $(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) & \ - $(GO) install $(GOFUMPT_PACKAGE) & \ $(GO) install $(GOLANGCI_LINT_PACKAGE) & \ $(GO) install $(GXZ_PACKAGE) & \ $(GO) install $(MISSPELL_PACKAGE) & \ @@ -750,7 +580,7 @@ deps-tools: ## install tool dependencies wait node_modules: pnpm-lock.yaml - $(NODE_VARS) pnpm install --frozen-lockfile + pnpm install --frozen-lockfile @touch node_modules .venv: uv.lock @@ -758,33 +588,44 @@ node_modules: pnpm-lock.yaml @touch .venv .PHONY: update -update: update-js update-py ## update js and py dependencies +update: update-go update-js update-py ## update dependencies + +.PHONY: update-go +update-go: ## update go dependencies + $(GO) get -u ./... + $(MAKE) tidy .PHONY: update-js update-js: node_modules ## update js dependencies - $(NODE_VARS) pnpm exec updates -u -f package.json + pnpm exec updates -u -f package.json rm -rf node_modules pnpm-lock.yaml - $(NODE_VARS) pnpm install - $(NODE_VARS) pnpm exec nolyfill install - $(NODE_VARS) pnpm install + pnpm install + @touch node_modules + $(MAKE) --no-print-directory nolyfill + +.PHONY: nolyfill +nolyfill: node_modules ## apply nolyfill overrides to package.json and relock + pnpm exec nolyfill install + node tools/migrate-nolyfills.ts + pnpm install @touch node_modules .PHONY: update-py update-py: node_modules ## update py dependencies - $(NODE_VARS) pnpm exec updates -u -f pyproject.toml + pnpm exec updates -u -f pyproject.toml rm -rf .venv uv.lock uv sync @touch .venv -.PHONY: webpack -webpack: $(WEBPACK_DEST) ## build webpack files +.PHONY: vite +vite: $(FRONTEND_DEST) ## build vite files -$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) pnpm-lock.yaml +$(FRONTEND_DEST): $(FRONTEND_SOURCES) $(FRONTEND_CONFIGS) pnpm-lock.yaml @$(MAKE) -s node_modules - @rm -rf $(WEBPACK_DEST_ENTRIES) - @echo "Running webpack..." - @BROWSERSLIST_IGNORE_OLD_DATA=true $(NODE_VARS) pnpm exec webpack --disable-interpret - @touch $(WEBPACK_DEST) + @rm -rf $(FRONTEND_DEST_ENTRIES) + @echo "Running vite build..." + @pnpm exec vite build + @touch $(FRONTEND_DEST) .PHONY: svg svg: node_modules ## build svg files @@ -803,7 +644,7 @@ svg-check: svg .PHONY: lockfile-check lockfile-check: - $(NODE_VARS) pnpm install --frozen-lockfile + pnpm install --frozen-lockfile @diff=$$(git diff --color=always pnpm-lock.yaml); \ if [ -n "$$diff" ]; then \ echo "pnpm-lock.yaml is inconsistent with package.json"; \ @@ -820,6 +661,10 @@ generate-gitignore: ## update gitignore files generate-images: | node_modules ## generate images cd tools && node generate-images.ts $(TAGS) +.PHONY: generate-codemirror-languages +generate-codemirror-languages: | node_modules ## generate codemirror languages + node tools/generate-codemirror-languages.ts + .PHONY: generate-manpage generate-manpage: ## generate manpage @[ -f gitea ] || make backend @@ -828,11 +673,6 @@ generate-manpage: ## generate manpage @gzip -9 man/man1/gitea.1 && echo man/man1/gitea.1.gz created @#TODO A small script that formats config-cheat-sheet.en-us.md nicely for use as a config man page -.PHONY: docker -docker: - docker build --disable-content-trust=false -t $(DOCKER_REF) . -# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" . - # Disable parallel execution because it would break some targets that don't # specify exact dependencies like 'backend' which does currently not depend # on 'frontend' to enable Node.js-less builds from source tarballs. diff --git a/README.md b/README.md index ed000971a7..4c4fedd3b8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ [![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") -[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) [![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [繁體中文](./README.zh-tw.md) | [简体中文](./README.zh-cn.md) @@ -45,10 +44,6 @@ From the root of the source tree, run: TAGS="bindata" make build -or if SQLite support is required: - - TAGS="bindata sqlite sqlite_unlock_notify" make build - The `build` target is split into two sub-targets: - `make backend` which requires [Go Stable](https://go.dev/dl/), the required version is defined in [go.mod](/go.mod). diff --git a/README.zh-cn.md b/README.zh-cn.md index 8d9531e8e4..52c7781831 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -8,7 +8,6 @@ [![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") -[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) [![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [English](./README.md) | [繁體中文](./README.zh-tw.md) @@ -39,10 +38,6 @@ TAGS="bindata" make build -如果需要 SQLite 支持: - - TAGS="bindata sqlite sqlite_unlock_notify" make build - `build` 目标分为两个子目标: - `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定义。 diff --git a/README.zh-tw.md b/README.zh-tw.md index 875d31e28a..77ad6828db 100644 --- a/README.zh-tw.md +++ b/README.zh-tw.md @@ -8,7 +8,6 @@ [![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") -[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) [![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [English](./README.md) | [简体中文](./README.zh-cn.md) @@ -39,10 +38,6 @@ TAGS="bindata" make build -如果需要 SQLite 支援: - - TAGS="bindata sqlite sqlite_unlock_notify" make build - `build` 目標分為兩個子目標: - `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定義。 diff --git a/assets/codemirror-languages.json b/assets/codemirror-languages.json new file mode 100644 index 0000000000..573e7c493b --- /dev/null +++ b/assets/codemirror-languages.json @@ -0,0 +1,1277 @@ +[ + { + "name": "APL", + "extensions": [ + "apl", + "dyalog" + ], + "filenames": [] + }, + { + "name": "ASN.1", + "extensions": [ + "asn", + "asn1" + ], + "filenames": [] + }, + { + "name": "Brainfuck", + "extensions": [ + "b", + "bf" + ], + "filenames": [] + }, + { + "name": "C", + "extensions": [ + "c", + "cats", + "h", + "idc" + ], + "filenames": [] + }, + { + "name": "C#", + "extensions": [ + "cs", + "cake", + "csx", + "linq" + ], + "filenames": [] + }, + { + "name": "C++", + "extensions": [ + "cpp", + "c++", + "cc", + "cp", + "cppm", + "cxx", + "h++", + "hh", + "hpp", + "hxx", + "inl", + "ipp", + "ixx", + "re", + "tcc", + "tpp", + "txx" + ], + "filenames": [] + }, + { + "name": "Clojure", + "extensions": [ + "clj", + "bb", + "boot", + "cl2", + "cljc", + "cljscm", + "cljx", + "hic" + ], + "filenames": [ + "riemann.config" + ] + }, + { + "name": "CMake", + "extensions": [ + "cmake" + ], + "filenames": [ + "CMakeLists.txt" + ] + }, + { + "name": "Cobol", + "extensions": [ + "cob", + "cbl", + "ccp", + "cobol", + "cpy" + ], + "filenames": [] + }, + { + "name": "CoffeeScript", + "extensions": [ + "coffee", + "_coffee", + "cake", + "cjsx", + "iced" + ], + "filenames": [ + "Cakefile" + ] + }, + { + "name": "Common Lisp", + "extensions": [ + "lisp", + "asd", + "cl", + "l", + "lsp", + "ny", + "podsl", + "sexp" + ], + "filenames": [] + }, + { + "name": "CQL", + "extensions": [ + "cql" + ], + "filenames": [] + }, + { + "name": "Crystal", + "extensions": [ + "cr" + ], + "filenames": [] + }, + { + "name": "CSS", + "extensions": [ + "css" + ], + "filenames": [] + }, + { + "name": "Cypher", + "extensions": [ + "cyp", + "cypher" + ], + "filenames": [] + }, + { + "name": "Cython", + "extensions": [ + "pyx", + "pxd", + "pxi" + ], + "filenames": [] + }, + { + "name": "D", + "extensions": [ + "d", + "di" + ], + "filenames": [] + }, + { + "name": "Dart", + "extensions": [ + "dart" + ], + "filenames": [] + }, + { + "name": "diff", + "extensions": [ + "diff", + "patch" + ], + "filenames": [] + }, + { + "name": "Dylan", + "extensions": [ + "dylan", + "dyl", + "intr", + "lid" + ], + "filenames": [] + }, + { + "name": "EBNF", + "extensions": [ + "ebnf" + ], + "filenames": [] + }, + { + "name": "ECL", + "extensions": [ + "ecl", + "eclxml" + ], + "filenames": [] + }, + { + "name": "edn", + "extensions": [ + "edn" + ], + "filenames": [] + }, + { + "name": "Eiffel", + "extensions": [ + "e" + ], + "filenames": [] + }, + { + "name": "Elm", + "extensions": [ + "elm" + ], + "filenames": [] + }, + { + "name": "Erlang", + "extensions": [ + "erl", + "app", + "es", + "escript", + "hrl", + "xrl", + "yrl" + ], + "filenames": [ + "Emakefile", + "rebar.config", + "rebar.config.lock", + "rebar.lock" + ] + }, + { + "name": "F#", + "extensions": [ + "fs", + "fsi", + "fsx" + ], + "filenames": [] + }, + { + "name": "Factor", + "extensions": [ + "factor" + ], + "filenames": [ + ".factor-boot-rc", + ".factor-rc" + ] + }, + { + "name": "Forth", + "extensions": [ + "fth", + "4th", + "forth", + "fr", + "frt" + ], + "filenames": [] + }, + { + "name": "Fortran", + "extensions": [ + "f", + "f77", + "for", + "fpp" + ], + "filenames": [] + }, + { + "name": "Gherkin", + "extensions": [ + "feature", + "story" + ], + "filenames": [] + }, + { + "name": "Go", + "extensions": [ + "go" + ], + "filenames": [] + }, + { + "name": "Groovy", + "extensions": [ + "groovy", + "grt", + "gtpl", + "gvy" + ], + "filenames": [ + "Jenkinsfile" + ] + }, + { + "name": "Haskell", + "extensions": [ + "hs", + "hs-boot", + "hsc" + ], + "filenames": [] + }, + { + "name": "Haxe", + "extensions": [ + "hx", + "hxsl" + ], + "filenames": [] + }, + { + "name": "HTML", + "extensions": [ + "html", + "hta", + "htm", + "xht", + "xhtml" + ], + "filenames": [] + }, + { + "name": "HTTP", + "extensions": [ + "http" + ], + "filenames": [] + }, + { + "name": "HXML", + "extensions": [ + "hxml" + ], + "filenames": [] + }, + { + "name": "IDL", + "extensions": [ + "pro", + "dlm" + ], + "filenames": [] + }, + { + "name": "Java", + "extensions": [ + "java", + "jav", + "jsh" + ], + "filenames": [] + }, + { + "name": "JavaScript", + "extensions": [ + "js", + "_js", + "bones", + "cjs", + "es", + "es6", + "frag", + "gs", + "jake", + "javascript", + "jsb", + "jscad", + "jsfl", + "jslib", + "jsm", + "jspre", + "jss", + "mjs", + "njs", + "pac", + "sjs", + "ssjs", + "xsjs", + "xsjslib" + ], + "filenames": [ + "Jakefile" + ] + }, + { + "name": "Jinja", + "extensions": [ + "jinja", + "j2", + "jinja2" + ], + "filenames": [] + }, + { + "name": "JSON", + "extensions": [ + "json", + "4DForm", + "4DProject", + "avsc", + "geojson", + "gltf", + "har", + "ice", + "JSON-tmLanguage", + "jsonl", + "mcmeta", + "sarif", + "tact", + "tfstate", + "topojson", + "webapp", + "webmanifest", + "yy", + "yyp" + ], + "filenames": [ + ".all-contributorsrc", + ".arcconfig", + ".auto-changelog", + ".c8rc", + ".htmlhintrc", + ".imgbotconfig", + ".nycrc", + ".tern-config", + ".tern-project", + ".watchmanconfig", + "MODULE.bazel.lock", + "Package.resolved", + "Pipfile.lock", + "bun.lock", + "composer.lock", + "deno.lock", + "flake.lock", + "mcmod.info" + ] + }, + { + "name": "JSON-LD", + "extensions": [ + "jsonld" + ], + "filenames": [] + }, + { + "name": "Julia", + "extensions": [ + "jl" + ], + "filenames": [] + }, + { + "name": "Kotlin", + "extensions": [ + "kt", + "ktm", + "kts" + ], + "filenames": [] + }, + { + "name": "LaTeX", + "extensions": [ + "tex", + "aux", + "bbx", + "cbx", + "cls", + "dtx", + "ins", + "lbx", + "ltx", + "mkii", + "mkiv", + "mkvi", + "sty", + "toc" + ], + "filenames": [] + }, + { + "name": "LESS", + "extensions": [ + "less" + ], + "filenames": [] + }, + { + "name": "Liquid", + "extensions": [ + "liquid" + ], + "filenames": [] + }, + { + "name": "LiveScript", + "extensions": [ + "ls", + "_ls" + ], + "filenames": [ + "Slakefile" + ] + }, + { + "name": "Lua", + "extensions": [ + "lua", + "nse", + "p8", + "pd_lua", + "rbxs", + "rockspec", + "wlua" + ], + "filenames": [ + ".luacheckrc" + ] + }, + { + "name": "Modelica", + "extensions": [ + "mo" + ], + "filenames": [] + }, + { + "name": "Nginx", + "extensions": [ + "nginx", + "nginxconf", + "vhost" + ], + "filenames": [ + "nginx.conf" + ] + }, + { + "name": "NSIS", + "extensions": [ + "nsi", + "nsh" + ], + "filenames": [] + }, + { + "name": "Objective-C", + "extensions": [], + "filenames": [] + }, + { + "name": "Objective-C++", + "extensions": [ + "mm" + ], + "filenames": [] + }, + { + "name": "OCaml", + "extensions": [ + "ml", + "eliom", + "eliomi", + "ml4", + "mli", + "mll", + "mly" + ], + "filenames": [] + }, + { + "name": "Oz", + "extensions": [ + "oz" + ], + "filenames": [] + }, + { + "name": "Pascal", + "extensions": [ + "pas", + "dfm", + "dpr", + "lpr", + "pascal" + ], + "filenames": [] + }, + { + "name": "Perl", + "extensions": [ + "pl", + "al", + "perl", + "ph", + "plx", + "pm", + "psgi", + "t" + ], + "filenames": [ + ".latexmkrc", + "Makefile.PL", + "Rexfile", + "ack", + "cpanfile", + "latexmkrc" + ] + }, + { + "name": "PHP", + "extensions": [ + "php", + "aw", + "ctp", + "php3", + "php4", + "php5", + "phps", + "phpt" + ], + "filenames": [ + ".php", + ".php_cs", + ".php_cs.dist", + "Phakefile" + ] + }, + { + "name": "PLSQL", + "extensions": [ + "pls", + "bdy", + "ddl", + "fnc", + "pck", + "pkb", + "pks", + "plb", + "plsql", + "prc", + "spc", + "tpb", + "tps", + "trg", + "vw" + ], + "filenames": [] + }, + { + "name": "PowerShell", + "extensions": [ + "ps1", + "psd1", + "psm1" + ], + "filenames": [] + }, + { + "name": "Properties files", + "extensions": [ + "ini", + "cnf", + "dof", + "lektorproject", + "prefs", + "properties", + "url", + "conf" + ], + "filenames": [ + ".buckconfig", + ".coveragerc", + ".flake8", + ".pylintrc", + "HOSTS", + "buildozer.spec", + "hosts", + "pylintrc", + "vlcrc", + ".editorconfig", + ".gitconfig", + ".npmrc" + ] + }, + { + "name": "ProtoBuf", + "extensions": [ + "proto" + ], + "filenames": [] + }, + { + "name": "Pug", + "extensions": [ + "jade", + "pug" + ], + "filenames": [] + }, + { + "name": "Puppet", + "extensions": [ + "pp" + ], + "filenames": [ + "Modulefile" + ] + }, + { + "name": "Python", + "extensions": [ + "py", + "gyp", + "gypi", + "lmi", + "py3", + "pyde", + "pyi", + "pyp", + "pyt", + "pyw", + "rpy", + "tac", + "wsgi", + "xpy" + ], + "filenames": [ + ".gclient", + "DEPS", + "SConscript", + "SConstruct", + "wscript", + "Snakefile" + ] + }, + { + "name": "Q", + "extensions": [ + "q" + ], + "filenames": [] + }, + { + "name": "R", + "extensions": [ + "r", + "rd", + "rsx" + ], + "filenames": [ + ".Rprofile", + "expr-dist" + ] + }, + { + "name": "RPM Spec", + "extensions": [ + "spec" + ], + "filenames": [] + }, + { + "name": "Ruby", + "extensions": [ + "rb", + "builder", + "eye", + "gemspec", + "god", + "jbuilder", + "mspec", + "pluginspec", + "podspec", + "prawn", + "rabl", + "rake", + "rbi", + "rbuild", + "rbw", + "rbx", + "ru", + "ruby", + "thor", + "watchr" + ], + "filenames": [ + ".irbrc", + ".pryrc", + ".simplecov", + "Appraisals", + "Berksfile", + "Brewfile", + "Buildfile", + "Capfile", + "Dangerfile", + "Deliverfile", + "Fastfile", + "Gemfile", + "Guardfile", + "Jarfile", + "Mavenfile", + "Podfile", + "Puppetfile", + "Rakefile", + "Snapfile", + "Steepfile", + "Thorfile", + "Vagrantfile", + "buildfile" + ] + }, + { + "name": "Rust", + "extensions": [ + "rs" + ], + "filenames": [] + }, + { + "name": "SAS", + "extensions": [ + "sas" + ], + "filenames": [] + }, + { + "name": "Sass", + "extensions": [ + "sass" + ], + "filenames": [] + }, + { + "name": "Scala", + "extensions": [ + "scala", + "kojo", + "sbt", + "sc" + ], + "filenames": [] + }, + { + "name": "Scheme", + "extensions": [ + "scm", + "sch", + "sld", + "sls", + "sps", + "ss" + ], + "filenames": [] + }, + { + "name": "SCSS", + "extensions": [ + "scss" + ], + "filenames": [] + }, + { + "name": "Shell", + "extensions": [ + "sh", + "bash", + "bats", + "command", + "ksh", + "sbatch", + "slurm", + "tmux", + "tool", + "trigger", + "zsh", + "zsh-theme" + ], + "filenames": [ + ".bash_aliases", + ".bash_functions", + ".bash_history", + ".bash_logout", + ".bash_profile", + ".bashrc", + ".cshrc", + ".envrc", + ".flaskenv", + ".kshrc", + ".login", + ".profile", + ".tmux.conf", + ".xinitrc", + ".xsession", + ".zlogin", + ".zlogout", + ".zprofile", + ".zshenv", + ".zshrc", + "9fs", + "PKGBUILD", + "bash_aliases", + "bash_logout", + "bash_profile", + "bashrc", + "cshrc", + "gradlew", + "kshrc", + "login", + "man", + "mvnw", + "profile", + "tmux.conf", + "xinitrc", + "xsession", + "zlogin", + "zlogout", + "zprofile", + "zshenv", + "zshrc" + ] + }, + { + "name": "Sieve", + "extensions": [ + "sieve" + ], + "filenames": [] + }, + { + "name": "Smalltalk", + "extensions": [ + "st" + ], + "filenames": [] + }, + { + "name": "SPARQL", + "extensions": [ + "sparql", + "rq" + ], + "filenames": [] + }, + { + "name": "SQL", + "extensions": [ + "sql", + "ddl", + "mysql", + "prc", + "tab", + "udf", + "viw" + ], + "filenames": [] + }, + { + "name": "Squirrel", + "extensions": [ + "nut" + ], + "filenames": [] + }, + { + "name": "Stylus", + "extensions": [ + "styl" + ], + "filenames": [] + }, + { + "name": "Swift", + "extensions": [ + "swift" + ], + "filenames": [] + }, + { + "name": "SystemVerilog", + "extensions": [ + "sv", + "svh", + "vh" + ], + "filenames": [] + }, + { + "name": "Tcl", + "extensions": [ + "tcl", + "adp", + "sdc", + "tm", + "xdc" + ], + "filenames": [ + "owh", + "starfield" + ] + }, + { + "name": "Textile", + "extensions": [ + "textile" + ], + "filenames": [] + }, + { + "name": "TOML", + "extensions": [ + "toml" + ], + "filenames": [ + "Cargo.lock", + "Cargo.toml.orig", + "Gopkg.lock", + "Pipfile", + "mise.local.lock", + "mise.lock", + "pdm.lock", + "poetry.lock", + "uv.lock" + ] + }, + { + "name": "TSX", + "extensions": [ + "tsx" + ], + "filenames": [] + }, + { + "name": "Turtle", + "extensions": [ + "ttl" + ], + "filenames": [] + }, + { + "name": "TypeScript", + "extensions": [ + "ts", + "cts", + "mts" + ], + "filenames": [] + }, + { + "name": "VBScript", + "extensions": [ + "vbs" + ], + "filenames": [] + }, + { + "name": "Verilog", + "extensions": [ + "veo" + ], + "filenames": [] + }, + { + "name": "VHDL", + "extensions": [ + "vhdl", + "vhd", + "vhf", + "vhi", + "vho", + "vhs", + "vht", + "vhw" + ], + "filenames": [] + }, + { + "name": "Vue", + "extensions": [ + "vue" + ], + "filenames": [] + }, + { + "name": "WebAssembly", + "extensions": [ + "wast", + "wat" + ], + "filenames": [] + }, + { + "name": "XML", + "extensions": [ + "xml", + "adml", + "admx", + "ant", + "axaml", + "axml", + "builds", + "ccproj", + "ccxml", + "clixml", + "cproject", + "cscfg", + "csdef", + "csl", + "csproj", + "ct", + "depproj", + "dita", + "ditamap", + "ditaval", + "dotsettings", + "filters", + "fsproj", + "fxml", + "glade", + "gml", + "gmx", + "gpx", + "grxml", + "gst", + "hzp", + "icls", + "iml", + "ivy", + "jelly", + "jsproj", + "kml", + "launch", + "mdpolicy", + "mjml", + "mod", + "mojo", + "mxml", + "natvis", + "ncl", + "ndproj", + "nproj", + "nuspec", + "odd", + "osm", + "pkgproj", + "pluginspec", + "proj", + "props", + "ps1xml", + "psc1", + "pt", + "pubxml", + "qhelp", + "rdf", + "res", + "resx", + "rss", + "sch", + "scxml", + "sfproj", + "shproj", + "slnx", + "srdf", + "storyboard", + "sublime-snippet", + "sw", + "targets", + "tml", + "typ", + "ui", + "urdf", + "ux", + "vbproj", + "vcxproj", + "vsixmanifest", + "vssettings", + "vstemplate", + "vxml", + "wixproj", + "workflow", + "wsdl", + "wsf", + "wxi", + "wxl", + "wxs", + "x3d", + "xacro", + "xaml", + "xib", + "xlf", + "xliff", + "xmi", + "xmp", + "xproj", + "xsd", + "xspec", + "xul", + "zcml" + ], + "filenames": [ + ".classpath", + ".cproject", + ".project", + "App.config", + "NuGet.config", + "Settings.StyleCop", + "Web.Debug.config", + "Web.Release.config", + "Web.config", + "packages.config" + ] + }, + { + "name": "XQuery", + "extensions": [ + "xquery", + "xq", + "xql", + "xqm", + "xqy" + ], + "filenames": [] + }, + { + "name": "YAML", + "extensions": [ + "yml", + "mir", + "reek", + "rviz", + "sublime-syntax", + "syntax", + "yaml", + "yaml-tmlanguage" + ], + "filenames": [ + ".clang-format", + ".clang-tidy", + ".clangd", + ".gemrc", + "CITATION.cff", + "glide.lock", + "pixi.lock", + "yarn.lock" + ] + } +] diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 30f56e5f87..991fe19746 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -39,6 +39,16 @@ "path": "filippo.io/edwards25519/LICENSE", "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "gitea.com/gitea/runner", + "path": "gitea.com/gitea/runner/LICENSE", + "licenseText": "Copyright (c) 2022 The Gitea Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" + }, + { + "name": "gitea.com/gitea/runner/act", + "path": "gitea.com/gitea/runner/act/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2022 The Gitea Authors\nCopyright (c) 2019\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, { "name": "gitea.com/go-chi/binding", "path": "gitea.com/go-chi/binding/LICENSE", @@ -132,13 +142,18 @@ { "name": "github.com/alecthomas/chroma/v2", "path": "github.com/alecthomas/chroma/v2/COPYING", - "licenseText": "Copyright (C) 2017 Alec Thomas\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + "licenseText": "Copyright (C) 2017 Alec Thomas\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\n// formatters/svg/font_liberation_mono.go\n\nDigitized data copyright (c) 2010 Google Corporation\nwith Reserved Font Arimo, Tinos and Cousine.\nCopyright (c) 2012 Red Hat, Inc.\nwith Reserved Font Name Liberation.\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded,\nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION \u0026 CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n" }, { "name": "github.com/andybalholm/brotli", "path": "github.com/andybalholm/brotli/LICENSE", "licenseText": "Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" }, + { + "name": "github.com/andybalholm/brotli/flate", + "path": "github.com/andybalholm/brotli/flate/LICENSE", + "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "github.com/andybalholm/cascadia", "path": "github.com/andybalholm/cascadia/LICENSE", @@ -299,6 +314,11 @@ "path": "github.com/blevesearch/zapx/v16/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License." }, + { + "name": "github.com/blevesearch/zapx/v17", + "path": "github.com/blevesearch/zapx/v17/LICENSE", + "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License." + }, { "name": "github.com/bmatcuk/doublestar/v4", "path": "github.com/bmatcuk/doublestar/v4/LICENSE", @@ -369,6 +389,16 @@ "path": "github.com/chi-middleware/proxy/LICENSE", "licenseText": "Copyright (c) 2020 Lauris BH\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" }, + { + "name": "github.com/clipperhouse/displaywidth", + "path": "github.com/clipperhouse/displaywidth/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2025 Matt Sherman\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, + { + "name": "github.com/clipperhouse/uax29/v2", + "path": "github.com/clipperhouse/uax29/v2/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2020 Matt Sherman\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, { "name": "github.com/cloudflare/circl", "path": "github.com/cloudflare/circl/LICENSE", @@ -386,8 +416,8 @@ }, { "name": "github.com/couchbase/goutils", - "path": "github.com/couchbase/goutils/LICENSE.md", - "licenseText": "Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n" + "path": "github.com/couchbase/goutils/LICENSE.txt", + "licenseText": "Source code in this repository is licensed under various licenses. The\nBusiness Source License 1.1 (BSL) is one such license. Each file indicates in\na section at the beginning of the file the name of the license that applies to\nit. All licenses used in this repository can be found in the top-level\nlicenses directory.\n" }, { "name": "github.com/cpuguy83/go-md2man/v2", @@ -396,19 +426,14 @@ }, { "name": "github.com/cyphar/filepath-securejoin", - "path": "github.com/cyphar/filepath-securejoin/LICENSE", - "licenseText": "Copyright (C) 2014-2015 Docker Inc \u0026 Go Authors. All rights reserved.\nCopyright (C) 2017-2024 SUSE LLC. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + "path": "github.com/cyphar/filepath-securejoin/COPYING.md", + "licenseText": "## COPYING ##\n\n`SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0`\n\nThis project is made up of code licensed under different licenses. Which code\nyou use will have an impact on whether only one or both licenses apply to your\nusage of this library.\n\nNote that **each file** in this project individually has a code comment at the\nstart describing the license of that particular file -- this is the most\naccurate license information of this project; in case there is any conflict\nbetween this document and the comment at the start of a file, the comment shall\ntake precedence. The only purpose of this document is to work around [a known\ntechnical limitation of pkg.go.dev's license checking tool when dealing with\nnon-trivial project licenses][go75067].\n\n[go75067]: https://go.dev/issue/75067\n\n### `BSD-3-Clause` ###\n\nAt time of writing, the following files and directories are licensed under the\nBSD-3-Clause license:\n\n * `doc.go`\n * `join*.go`\n * `vfs.go`\n * `internal/consts/*.go`\n * `pathrs-lite/internal/gocompat/*.go`\n * `pathrs-lite/internal/kernelversion/*.go`\n\nThe text of the BSD-3-Clause license used by this project is the following (the\ntext is also available from the [`LICENSE.BSD`](./LICENSE.BSD) file):\n\n```\nCopyright (C) 2014-2015 Docker Inc \u0026 Go Authors. All rights reserved.\nCopyright (C) 2017-2024 SUSE LLC. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n```\n\n### `MPL-2.0` ###\n\nAll other files (unless otherwise marked) are licensed under the Mozilla Public\nLicense (version 2.0).\n\nThe text of the Mozilla Public License (version 2.0) is the following (the text\nis also available from the [`LICENSE.MPL-2.0`](./LICENSE.MPL-2.0) file):\n\n```\nMozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n means each individual or legal entity that creates, contributes to\n the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n means the combination of the Contributions of others (if any) used\n by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n means Source Code Form to which the initial Contributor has attached\n the notice in Exhibit A, the Executable Form of such Source Code\n Form, and Modifications of such Source Code Form, in each case\n including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n (a) that the initial Contributor has attached the notice described\n in Exhibit B to the Covered Software; or\n\n (b) that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the\n terms of a Secondary License.\n\n1.6. \"Executable Form\"\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n means a work that combines Covered Software with other material, in\n a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n means this document.\n\n1.9. \"Licensable\"\n means having the right to grant, to the maximum extent possible,\n whether at the time of the initial grant or subsequently, any and\n all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n means any of the following:\n\n (a) any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered\n Software; or\n\n (b) any new file in Source Code Form that contains any Covered\n Software.\n\n1.11. \"Patent Claims\" of a Contributor\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the\n License, by the making, using, selling, offering for sale, having\n made, import, or transfer of either its Contributions or its\n Contributor Version.\n\n1.12. \"Secondary License\"\n means either the GNU General Public License, Version 2.0, the GNU\n Lesser General Public License, Version 2.1, the GNU Affero General\n Public License, Version 3.0, or any later versions of those\n licenses.\n\n1.13. \"Source Code Form\"\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that\n controls, is controlled by, or is under common control with You. For\n purposes of this definition, \"control\" means (a) the power, direct\n or indirect, to cause the direction or management of such entity,\n whether by contract or otherwise, or (b) ownership of more than\n fifty percent (50%) of the outstanding shares or beneficial\n ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n for sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n or\n\n(b) for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n Form, as described in Section 3.1, and You must inform recipients of\n the Executable Form how they can obtain a copy of such Source Code\n Form by reasonable means in a timely manner, at a charge no more\n than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n License, or sublicense it under different terms, provided that the\n license for the Executable Form does not attempt to limit or alter\n the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n* *\n* 6. Disclaimer of Warranty *\n* ------------------------- *\n* *\n* Covered Software is provided under this License on an \"as is\" *\n* basis, without warranty of any kind, either expressed, implied, or *\n* statutory, including, without limitation, warranties that the *\n* Covered Software is free of defects, merchantable, fit for a *\n* particular purpose or non-infringing. The entire risk as to the *\n* quality and performance of the Covered Software is with You. *\n* Should any Covered Software prove defective in any respect, You *\n* (not any Contributor) assume the cost of any necessary servicing, *\n* repair, or correction. This disclaimer of warranty constitutes an *\n* essential part of this License. No use of any Covered Software is *\n* authorized under this License except under this disclaimer. *\n* *\n************************************************************************\n\n************************************************************************\n* *\n* 7. Limitation of Liability *\n* -------------------------- *\n* *\n* Under no circumstances and under no legal theory, whether tort *\n* (including negligence), contract, or otherwise, shall any *\n* Contributor, or anyone who distributes Covered Software as *\n* permitted above, be liable to You for any direct, indirect, *\n* special, incidental, or consequential damages of any character *\n* including, without limitation, damages for lost profits, loss of *\n* goodwill, work stoppage, computer failure or malfunction, or any *\n* and all other commercial damages or losses, even if such party *\n* shall have been informed of the possibility of such damages. This *\n* limitation of liability shall not apply to liability for death or *\n* personal injury resulting from such party's negligence to the *\n* extent applicable law prohibits such limitation. Some *\n* jurisdictions do not allow the exclusion or limitation of *\n* incidental or consequential damages, so this exclusion and *\n* limitation may not apply to You. *\n* *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n This Source Code Form is subject to the terms of the Mozilla Public\n License, v. 2.0. If a copy of the MPL was not distributed with this\n file, You can obtain one at https://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n This Source Code Form is \"Incompatible With Secondary Licenses\", as\n defined by the Mozilla Public License, v. 2.0.\n```\n" }, { "name": "github.com/davecgh/go-spew", "path": "github.com/davecgh/go-spew/LICENSE", "licenseText": "ISC License\n\nCopyright (c) 2012-2016 Dave Collins \u003cdave@davec.name\u003e\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n" }, - { - "name": "github.com/dgryski/go-rendezvous", - "path": "github.com/dgryski/go-rendezvous/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2017-2020 Damian Gryski \u003cdamian@gryski.com\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" - }, { "name": "github.com/dimiro1/reply", "path": "github.com/dimiro1/reply/LICENSE", @@ -474,6 +499,11 @@ "path": "github.com/fxamacker/cbor/v2/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2019-present Faye Amacker\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE." }, + { + "name": "github.com/getkin/kin-openapi", + "path": "github.com/getkin/kin-openapi/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2017-2018 the project authors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, { "name": "github.com/git-lfs/pktline", "path": "github.com/git-lfs/pktline/LICENSE.md", @@ -500,8 +530,8 @@ "licenseText": "Copyright (c) 2014 Olivier Poitrey \u003crs@dailymotion.com\u003e\nCopyright (c) 2016-Present https://github.com/go-chi authors\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" }, { - "name": "github.com/go-co-op/gocron", - "path": "github.com/go-co-op/gocron/LICENSE", + "name": "github.com/go-co-op/gocron/v2", + "path": "github.com/go-co-op/gocron/v2/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2014, 辣椒面\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, { @@ -539,6 +569,16 @@ "path": "github.com/go-ldap/ldap/v3/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)\nPortions copyright (c) 2015-2024 go-ldap Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, + { + "name": "github.com/go-openapi/jsonpointer", + "path": "github.com/go-openapi/jsonpointer/LICENSE", + "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, + { + "name": "github.com/go-openapi/swag", + "path": "github.com/go-openapi/swag/LICENSE", + "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, { "name": "github.com/go-redsync/redsync/v4", "path": "github.com/go-redsync/redsync/v4/LICENSE", @@ -549,10 +589,15 @@ "path": "github.com/go-sql-driver/mysql/LICENSE", "licenseText": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n means each individual or legal entity that creates, contributes to\n the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n means the combination of the Contributions of others (if any) used\n by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n means Source Code Form to which the initial Contributor has attached\n the notice in Exhibit A, the Executable Form of such Source Code\n Form, and Modifications of such Source Code Form, in each case\n including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n (a) that the initial Contributor has attached the notice described\n in Exhibit B to the Covered Software; or\n\n (b) that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the\n terms of a Secondary License.\n\n1.6. \"Executable Form\"\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n means a work that combines Covered Software with other material, in \n a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n means this document.\n\n1.9. \"Licensable\"\n means having the right to grant, to the maximum extent possible,\n whether at the time of the initial grant or subsequently, any and\n all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n means any of the following:\n\n (a) any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered\n Software; or\n\n (b) any new file in Source Code Form that contains any Covered\n Software.\n\n1.11. \"Patent Claims\" of a Contributor\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the\n License, by the making, using, selling, offering for sale, having\n made, import, or transfer of either its Contributions or its\n Contributor Version.\n\n1.12. \"Secondary License\"\n means either the GNU General Public License, Version 2.0, the GNU\n Lesser General Public License, Version 2.1, the GNU Affero General\n Public License, Version 3.0, or any later versions of those\n licenses.\n\n1.13. \"Source Code Form\"\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that\n controls, is controlled by, or is under common control with You. For\n purposes of this definition, \"control\" means (a) the power, direct\n or indirect, to cause the direction or management of such entity,\n whether by contract or otherwise, or (b) ownership of more than\n fifty percent (50%) of the outstanding shares or beneficial\n ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n for sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n or\n\n(b) for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n Form, as described in Section 3.1, and You must inform recipients of\n the Executable Form how they can obtain a copy of such Source Code\n Form by reasonable means in a timely manner, at a charge no more\n than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n License, or sublicense it under different terms, provided that the\n license for the Executable Form does not attempt to limit or alter\n the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n* *\n* 6. Disclaimer of Warranty *\n* ------------------------- *\n* *\n* Covered Software is provided under this License on an \"as is\" *\n* basis, without warranty of any kind, either expressed, implied, or *\n* statutory, including, without limitation, warranties that the *\n* Covered Software is free of defects, merchantable, fit for a *\n* particular purpose or non-infringing. The entire risk as to the *\n* quality and performance of the Covered Software is with You. *\n* Should any Covered Software prove defective in any respect, You *\n* (not any Contributor) assume the cost of any necessary servicing, *\n* repair, or correction. This disclaimer of warranty constitutes an *\n* essential part of this License. No use of any Covered Software is *\n* authorized under this License except under this disclaimer. *\n* *\n************************************************************************\n\n************************************************************************\n* *\n* 7. Limitation of Liability *\n* -------------------------- *\n* *\n* Under no circumstances and under no legal theory, whether tort *\n* (including negligence), contract, or otherwise, shall any *\n* Contributor, or anyone who distributes Covered Software as *\n* permitted above, be liable to You for any direct, indirect, *\n* special, incidental, or consequential damages of any character *\n* including, without limitation, damages for lost profits, loss of *\n* goodwill, work stoppage, computer failure or malfunction, or any *\n* and all other commercial damages or losses, even if such party *\n* shall have been informed of the possibility of such damages. This *\n* limitation of liability shall not apply to liability for death or *\n* personal injury resulting from such party's negligence to the *\n* extent applicable law prohibits such limitation. Some *\n* jurisdictions do not allow the exclusion or limitation of *\n* incidental or consequential damages, so this exclusion and *\n* limitation may not apply to You. *\n* *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n This Source Code Form is subject to the terms of the Mozilla Public\n License, v. 2.0. If a copy of the MPL was not distributed with this\n file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n This Source Code Form is \"Incompatible With Secondary Licenses\", as\n defined by the Mozilla Public License, v. 2.0.\n" }, + { + "name": "github.com/go-viper/mapstructure/v2", + "path": "github.com/go-viper/mapstructure/v2/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Mitchell Hashimoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" + }, { "name": "github.com/go-webauthn/webauthn", "path": "github.com/go-webauthn/webauthn/LICENSE", - "licenseText": "Copyright (c) 2017 Duo Security, Inc. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\nCopyright (c) 2021-2022 github.com/go-webauthn/webauthn authors.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the\nfollowing conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following\n disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following\n disclaimer in the documentation and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\nTHIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + "licenseText": "Copyright (c) 2025 github.com/go-webauthn/webauthn authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." }, { "name": "github.com/go-webauthn/x", @@ -615,8 +660,8 @@ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { - "name": "github.com/google/go-github/v74", - "path": "github.com/google/go-github/v74/LICENSE", + "name": "github.com/google/go-github/v85", + "path": "github.com/google/go-github/v85/LICENSE", "licenseText": "Copyright (c) 2013 The go-github AUTHORS. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, { @@ -709,6 +754,11 @@ "path": "github.com/huandu/xstrings/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Huan Du\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, + { + "name": "github.com/inbucket/html2text", + "path": "github.com/inbucket/html2text/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Jay Taylor\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" + }, { "name": "github.com/jaytaylor/html2text", "path": "github.com/jaytaylor/html2text/LICENSE", @@ -720,10 +770,15 @@ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Juan Batiz-Benet\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" }, { - "name": "github.com/jhillyerd/enmime", - "path": "github.com/jhillyerd/enmime/LICENSE", + "name": "github.com/jhillyerd/enmime/v2", + "path": "github.com/jhillyerd/enmime/v2/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2012-2016 James Hillyerd, All Rights Reserved\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" }, + { + "name": "github.com/jonboulle/clockwork", + "path": "github.com/jonboulle/clockwork/LICENSE", + "licenseText": "Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, { "name": "github.com/josharian/intern", "path": "github.com/josharian/intern/license.md", @@ -889,11 +944,6 @@ "path": "github.com/minio/minlz/LICENSE", "licenseText": "\r\n Apache License\r\n Version 2.0, January 2004\r\n http://www.apache.org/licenses/\r\n\r\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n 1. Definitions.\r\n\r\n \"License\" shall mean the terms and conditions for use, reproduction,\r\n and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n \"Licensor\" shall mean the copyright owner or entity authorized by\r\n the copyright owner that is granting the License.\r\n\r\n \"Legal Entity\" shall mean the union of the acting entity and all\r\n other entities that control, are controlled by, or are under common\r\n control with that entity. For the purposes of this definition,\r\n \"control\" means (i) the power, direct or indirect, to cause the\r\n direction or management of such entity, whether by contract or\r\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n exercising permissions granted by this License.\r\n\r\n \"Source\" form shall mean the preferred form for making modifications,\r\n including but not limited to software source code, documentation\r\n source, and configuration files.\r\n\r\n \"Object\" form shall mean any form resulting from mechanical\r\n transformation or translation of a Source form, including but\r\n not limited to compiled object code, generated documentation,\r\n and conversions to other media types.\r\n\r\n \"Work\" shall mean the work of authorship, whether in Source or\r\n Object form, made available under the License, as indicated by a\r\n copyright notice that is included in or attached to the work\r\n (an example is provided in the Appendix below).\r\n\r\n \"Derivative Works\" shall mean any work, whether in Source or Object\r\n form, that is based on (or derived from) the Work and for which the\r\n editorial revisions, annotations, elaborations, or other modifications\r\n represent, as a whole, an original work of authorship. For the purposes\r\n of this License, Derivative Works shall not include works that remain\r\n separable from, or merely link (or bind by name) to the interfaces of,\r\n the Work and Derivative Works thereof.\r\n\r\n \"Contribution\" shall mean any work of authorship, including\r\n the original version of the Work and any modifications or additions\r\n to that Work or Derivative Works thereof, that is intentionally\r\n submitted to Licensor for inclusion in the Work by the copyright owner\r\n or by an individual or Legal Entity authorized to submit on behalf of\r\n the copyright owner. For the purposes of this definition, \"submitted\"\r\n means any form of electronic, verbal, or written communication sent\r\n to the Licensor or its representatives, including but not limited to\r\n communication on electronic mailing lists, source code control systems,\r\n and issue tracking systems that are managed by, or on behalf of, the\r\n Licensor for the purpose of discussing and improving the Work, but\r\n excluding communication that is conspicuously marked or otherwise\r\n designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n on behalf of whom a Contribution has been received by Licensor and\r\n subsequently incorporated within the Work.\r\n\r\n 2. Grant of Copyright License. Subject to the terms and conditions of\r\n this License, each Contributor hereby grants to You a perpetual,\r\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n copyright license to reproduce, prepare Derivative Works of,\r\n publicly display, publicly perform, sublicense, and distribute the\r\n Work and such Derivative Works in Source or Object form.\r\n\r\n 3. Grant of Patent License. Subject to the terms and conditions of\r\n this License, each Contributor hereby grants to You a perpetual,\r\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n (except as stated in this section) patent license to make, have made,\r\n use, offer to sell, sell, import, and otherwise transfer the Work,\r\n where such license applies only to those patent claims licensable\r\n by such Contributor that are necessarily infringed by their\r\n Contribution(s) alone or by combination of their Contribution(s)\r\n with the Work to which such Contribution(s) was submitted. If You\r\n institute patent litigation against any entity (including a\r\n cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n or a Contribution incorporated within the Work constitutes direct\r\n or contributory patent infringement, then any patent licenses\r\n granted to You under this License for that Work shall terminate\r\n as of the date such litigation is filed.\r\n\r\n 4. Redistribution. You may reproduce and distribute copies of the\r\n Work or Derivative Works thereof in any medium, with or without\r\n modifications, and in Source or Object form, provided that You\r\n meet the following conditions:\r\n\r\n (a) You must give any other recipients of the Work or\r\n Derivative Works a copy of this License; and\r\n\r\n (b) You must cause any modified files to carry prominent notices\r\n stating that You changed the files; and\r\n\r\n (c) You must retain, in the Source form of any Derivative Works\r\n that You distribute, all copyright, patent, trademark, and\r\n attribution notices from the Source form of the Work,\r\n excluding those notices that do not pertain to any part of\r\n the Derivative Works; and\r\n\r\n (d) If the Work includes a \"NOTICE\" text file as part of its\r\n distribution, then any Derivative Works that You distribute must\r\n include a readable copy of the attribution notices contained\r\n within such NOTICE file, excluding those notices that do not\r\n pertain to any part of the Derivative Works, in at least one\r\n of the following places: within a NOTICE text file distributed\r\n as part of the Derivative Works; within the Source form or\r\n documentation, if provided along with the Derivative Works; or,\r\n within a display generated by the Derivative Works, if and\r\n wherever such third-party notices normally appear. The contents\r\n of the NOTICE file are for informational purposes only and\r\n do not modify the License. You may add Your own attribution\r\n notices within Derivative Works that You distribute, alongside\r\n or as an addendum to the NOTICE text from the Work, provided\r\n that such additional attribution notices cannot be construed\r\n as modifying the License.\r\n\r\n You may add Your own copyright statement to Your modifications and\r\n may provide additional or different license terms and conditions\r\n for use, reproduction, or distribution of Your modifications, or\r\n for any such Derivative Works as a whole, provided Your use,\r\n reproduction, and distribution of the Work otherwise complies with\r\n the conditions stated in this License.\r\n\r\n 5. Submission of Contributions. Unless You explicitly state otherwise,\r\n any Contribution intentionally submitted for inclusion in the Work\r\n by You to the Licensor shall be under the terms and conditions of\r\n this License, without any additional terms or conditions.\r\n Notwithstanding the above, nothing herein shall supersede or modify\r\n the terms of any separate license agreement you may have executed\r\n with Licensor regarding such Contributions.\r\n\r\n 6. Trademarks. This License does not grant permission to use the trade\r\n names, trademarks, service marks, or product names of the Licensor,\r\n except as required for reasonable and customary use in describing the\r\n origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n 7. Disclaimer of Warranty. Unless required by applicable law or\r\n agreed to in writing, Licensor provides the Work (and each\r\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n implied, including, without limitation, any warranties or conditions\r\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n PARTICULAR PURPOSE. You are solely responsible for determining the\r\n appropriateness of using or redistributing the Work and assume any\r\n risks associated with Your exercise of permissions under this License.\r\n\r\n 8. Limitation of Liability. In no event and under no legal theory,\r\n whether in tort (including negligence), contract, or otherwise,\r\n unless required by applicable law (such as deliberate and grossly\r\n negligent acts) or agreed to in writing, shall any Contributor be\r\n liable to You for damages, including any direct, indirect, special,\r\n incidental, or consequential damages of any character arising as a\r\n result of this License or out of the use or inability to use the\r\n Work (including but not limited to damages for loss of goodwill,\r\n work stoppage, computer failure or malfunction, or any and all\r\n other commercial damages or losses), even if such Contributor\r\n has been advised of the possibility of such damages.\r\n\r\n 9. Accepting Warranty or Additional Liability. While redistributing\r\n the Work or Derivative Works thereof, You may choose to offer,\r\n and charge a fee for, acceptance of support, warranty, indemnity,\r\n or other liability obligations and/or rights consistent with this\r\n License. However, in accepting such obligations, You may act only\r\n on Your own behalf and on Your sole responsibility, not on behalf\r\n of any other Contributor, and only if You agree to indemnify,\r\n defend, and hold each Contributor harmless for any liability\r\n incurred by, or claims asserted against, such Contributor by reason\r\n of your accepting any such warranty or additional liability.\r\n\r\nEND OF TERMS AND CONDITIONS" }, - { - "name": "github.com/mitchellh/mapstructure", - "path": "github.com/mitchellh/mapstructure/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Mitchell Hashimoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" - }, { "name": "github.com/modern-go/concurrent", "path": "github.com/modern-go/concurrent/LICENSE", @@ -904,16 +954,21 @@ "path": "github.com/modern-go/reflect2/LICENSE", "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, + { + "name": "github.com/mohae/deepcopy", + "path": "github.com/mohae/deepcopy/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Joel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, + { + "name": "github.com/mschoch/smat", + "path": "github.com/mschoch/smat/LICENSE", + "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License." + }, { "name": "github.com/munnerz/goautoneg", "path": "github.com/munnerz/goautoneg/LICENSE", "licenseText": "Copyright (c) 2011, Open Knowledge Foundation Ltd.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n\n Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in\n the documentation and/or other materials provided with the\n distribution.\n\n Neither the name of the Open Knowledge Foundation Ltd. nor the\n names of its contributors may be used to endorse or promote\n products derived from this software without specific prior written\n permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, - { - "name": "github.com/nektos/act", - "path": "github.com/nektos/act/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2022 The Gitea Authors\nCopyright (c) 2019\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" - }, { "name": "github.com/niklasfasching/go-org", "path": "github.com/niklasfasching/go-org/LICENSE", @@ -924,6 +979,16 @@ "path": "github.com/nwaples/rardecode/v2/LICENSE", "licenseText": "Copyright (c) 2015, Nicholas Waples\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "github.com/oasdiff/yaml", + "path": "github.com/oasdiff/yaml/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Sam Ghods\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\nCopyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, + { + "name": "github.com/oasdiff/yaml3", + "path": "github.com/oasdiff/yaml3/LICENSE", + "licenseText": "\nThis project is covered by two different licenses: MIT and Apache.\n\n#### MIT License ####\n\nThe following files were ported to Go from C files of libyaml, and thus\nare still covered by their original MIT license, with the additional\ncopyright staring in 2011 when the project was ported over:\n\n apic.go emitterc.go parserc.go readerc.go scannerc.go\n writerc.go yamlh.go yamlprivateh.go\n\nCopyright (c) 2006-2010 Kirill Simonov\nCopyright (c) 2006-2011 Kirill Simonov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n### Apache License ###\n\nAll the remaining project files are covered by the Apache license:\n\nCopyright (c) 2011-2019 Canonical Ltd\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n" + }, { "name": "github.com/olekukonko/cat", "path": "github.com/olekukonko/cat/LICENSE", @@ -944,16 +1009,6 @@ "path": "github.com/olekukonko/tablewriter/LICENSE.md", "licenseText": "Copyright (C) 2014 by Oleku Konko\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" }, - { - "name": "github.com/olivere/elastic/v7", - "path": "github.com/olivere/elastic/v7/LICENSE", - "licenseText": "The MIT License (MIT)\nCopyright © 2012-2015 Oliver Eilhard\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the “Software”), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\nIN THE SOFTWARE.\n" - }, - { - "name": "github.com/olivere/elastic/v7/uritemplates", - "path": "github.com/olivere/elastic/v7/uritemplates/LICENSE", - "licenseText": "Copyright (c) 2013 Joshua Tacoma\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" - }, { "name": "github.com/opencontainers/go-digest", "path": "github.com/opencontainers/go-digest/LICENSE", @@ -964,6 +1019,11 @@ "path": "github.com/opencontainers/image-spec/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n Copyright 2016 The Linux Foundation.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, + { + "name": "github.com/perimeterx/marshmallow", + "path": "github.com/perimeterx/marshmallow/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2022 PerimeterX\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, { "name": "github.com/philhofer/fwd", "path": "github.com/philhofer/fwd/LICENSE.md", @@ -1024,16 +1084,16 @@ "path": "github.com/redis/go-redis/v9/LICENSE", "licenseText": "Copyright (c) 2013 The github.com/redis/go-redis Authors.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "github.com/remyoudompheng/bigfft", + "path": "github.com/remyoudompheng/bigfft/LICENSE", + "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "github.com/rhysd/actionlint", "path": "github.com/rhysd/actionlint/LICENSE.txt", "licenseText": "the MIT License\n\nCopyright (c) 2021 rhysd\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,\nINCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR\nPURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR\nTHE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n" }, - { - "name": "github.com/rivo/uniseg", - "path": "github.com/rivo/uniseg/LICENSE.txt", - "licenseText": "MIT License\n\nCopyright (c) 2019 Oliver Kuederle\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" - }, { "name": "github.com/robfig/cron/v3", "path": "github.com/robfig/cron/v3/LICENSE", @@ -1050,8 +1110,8 @@ "licenseText": "Blackfriday is distributed under the Simplified BSD License:\n\n\u003e Copyright © 2011 Russ Ross\n\u003e All rights reserved.\n\u003e\n\u003e Redistribution and use in source and binary forms, with or without\n\u003e modification, are permitted provided that the following conditions\n\u003e are met:\n\u003e\n\u003e 1. Redistributions of source code must retain the above copyright\n\u003e notice, this list of conditions and the following disclaimer.\n\u003e\n\u003e 2. Redistributions in binary form must reproduce the above\n\u003e copyright notice, this list of conditions and the following\n\u003e disclaimer in the documentation and/or other materials provided with\n\u003e the distribution.\n\u003e\n\u003e THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\u003e \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n\u003e LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS\n\u003e FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\n\u003e COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,\n\u003e INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,\n\u003e BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n\u003e LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n\u003e CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\n\u003e LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\n\u003e ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n\u003e POSSIBILITY OF SUCH DAMAGE.\n" }, { - "name": "github.com/santhosh-tekuri/jsonschema/v5", - "path": "github.com/santhosh-tekuri/jsonschema/v5/LICENSE", + "name": "github.com/santhosh-tekuri/jsonschema/v6", + "path": "github.com/santhosh-tekuri/jsonschema/v6/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability." }, { @@ -1144,6 +1204,11 @@ "path": "github.com/wneessen/go-mail/smtp/LICENSE", "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." }, + { + "name": "github.com/woodsbury/decimal128", + "path": "github.com/woodsbury/decimal128/LICENCE", + "licenseText": "BSD Zero Clause License\n\nCopyright (c) 2022 Wade Smith\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n" + }, { "name": "github.com/x448/float16", "path": "github.com/x448/float16/LICENSE", @@ -1169,11 +1234,6 @@ "path": "github.com/yuin/goldmark-highlighting/v2/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2019 Yusuke Inuzuka\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, - { - "name": "github.com/yuin/goldmark-meta", - "path": "github.com/yuin/goldmark-meta/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2019 Yusuke Inuzuka\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" - }, { "name": "github.com/yuin/goldmark", "path": "github.com/yuin/goldmark/LICENSE", @@ -1185,8 +1245,13 @@ "licenseText": "This work is released into the public domain with CC0 1.0.\n\n-------------------------------------------------------------------------------\n\nCreative Commons Legal Code\n\nCC0 1.0 Universal\n\n CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN\n ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS\n INFORMATION ON AN \"AS-IS\" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES\n REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\n PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM\n THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED\n HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator\nand subsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for\nthe purpose of contributing to a commons of creative, cultural and\nscientific works (\"Commons\") that the public can reliably and without fear\nof later claims of infringement build upon, modify, incorporate in other\nworks, reuse and redistribute as freely as possible in any form whatsoever\nand for any purposes, including without limitation commercial purposes.\nThese owners may contribute to the Commons to promote the ideal of a free\nculture and the further production of creative, cultural and scientific\nworks, or to gain reputation or greater distribution for their Work in\npart through the use and efforts of others.\n\nFor these and/or other purposes and motivations, and without any\nexpectation of additional consideration or compensation, the person\nassociating CC0 with a Work (the \"Affirmer\"), to the extent that he or she\nis an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the\nWork and the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not\nlimited to, the following:\n\n i. the right to reproduce, adapt, distribute, perform, display,\n communicate, and translate a Work;\n ii. moral rights retained by the original author(s) and/or performer(s);\niii. publicity and privacy rights pertaining to a person's image or\n likeness depicted in a Work;\n iv. rights protecting against unfair competition in regards to a Work,\n subject to the limitations in paragraph 4(a), below;\n v. rights protecting the extraction, dissemination, use and reuse of data\n in a Work;\n vi. database rights (such as those arising under Directive 96/9/EC of the\n European Parliament and of the Council of 11 March 1996 on the legal\n protection of databases, and under any national implementation\n thereof, including any amended or successor version of such\n directive); and\nvii. other similar, equivalent or corresponding rights throughout the\n world based on applicable law or treaty, and any national\n implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention\nof, applicable law, Affirmer hereby overtly, fully, permanently,\nirrevocably and unconditionally waives, abandons, and surrenders all of\nAffirmer's Copyright and Related Rights and associated claims and causes\nof action, whether now known or unknown (including existing as well as\nfuture claims and causes of action), in the Work (i) in all territories\nworldwide, (ii) for the maximum duration provided by applicable law or\ntreaty (including future time extensions), (iii) in any current or future\nmedium and for any number of copies, and (iv) for any purpose whatsoever,\nincluding without limitation commercial, advertising or promotional\npurposes (the \"Waiver\"). Affirmer makes the Waiver for the benefit of each\nmember of the public at large and to the detriment of Affirmer's heirs and\nsuccessors, fully intending that such Waiver shall not be subject to\nrevocation, rescission, cancellation, termination, or any other legal or\nequitable action to disrupt the quiet enjoyment of the Work by the public\nas contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason\nbe judged legally invalid or ineffective under applicable law, then the\nWaiver shall be preserved to the maximum extent permitted taking into\naccount Affirmer's express Statement of Purpose. In addition, to the\nextent the Waiver is so judged Affirmer hereby grants to each affected\nperson a royalty-free, non transferable, non sublicensable, non exclusive,\nirrevocable and unconditional license to exercise Affirmer's Copyright and\nRelated Rights in the Work (i) in all territories worldwide, (ii) for the\nmaximum duration provided by applicable law or treaty (including future\ntime extensions), (iii) in any current or future medium and for any number\nof copies, and (iv) for any purpose whatsoever, including without\nlimitation commercial, advertising or promotional purposes (the\n\"License\"). The License shall be deemed effective as of the date CC0 was\napplied by Affirmer to the Work. Should any part of the License for any\nreason be judged legally invalid or ineffective under applicable law, such\npartial invalidity or ineffectiveness shall not invalidate the remainder\nof the License, and in such case Affirmer hereby affirms that he or she\nwill not (i) exercise any of his or her remaining Copyright and Related\nRights in the Work or (ii) assert any associated claims and causes of\naction with respect to the Work, in either case contrary to Affirmer's\nexpress Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n a. No trademark or patent rights held by Affirmer are waived, abandoned,\n surrendered, licensed or otherwise affected by this document.\n b. Affirmer offers the Work as-is and makes no representations or\n warranties of any kind concerning the Work, express, implied,\n statutory or otherwise, including without limitation warranties of\n title, merchantability, fitness for a particular purpose, non\n infringement, or the absence of latent or other defects, accuracy, or\n the present or absence of errors, whether or not discoverable, all to\n the greatest extent permissible under applicable law.\n c. Affirmer disclaims responsibility for clearing rights of other persons\n that may apply to the Work or any use thereof, including without\n limitation any person's Copyright and Related Rights in the Work.\n Further, Affirmer disclaims responsibility for obtaining any necessary\n consents, permissions or other rights required for any use of the\n Work.\n d. Affirmer understands and acknowledges that Creative Commons is not a\n party to this document and has no duty or obligation with respect to\n this CC0 or use of the Work.\n" }, { - "name": "gitlab.com/gitlab-org/api/client-go", - "path": "gitlab.com/gitlab-org/api/client-go/LICENSE", + "name": "github.com/zeebo/xxh3", + "path": "github.com/zeebo/xxh3/LICENSE", + "licenseText": "BSD 2-Clause License\n\nCopyright (c) 2012-2014, Yann Collet\nCopyright (c) 2019, Jeff Wendling\nAll rights reserved.\n\nxxHash Library\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice, this\n list of conditions and the following disclaimer in the documentation and/or\n other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, + { + "name": "gitlab.com/gitlab-org/api/client-go/v2", + "path": "gitlab.com/gitlab-org/api/client-go/v2/LICENSE", "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { @@ -1227,7 +1292,7 @@ { "name": "go.yaml.in/yaml/v4", "path": "go.yaml.in/yaml/v4/LICENSE", - "licenseText": "\nThis project is covered by two different licenses: MIT and Apache.\n\n#### MIT License ####\n\nThe following files were ported to Go from C files of libyaml, and thus\nare still covered by their original MIT license, with the additional\ncopyright staring in 2011 when the project was ported over:\n\n apic.go emitterc.go parserc.go readerc.go scannerc.go\n writerc.go yamlh.go yamlprivateh.go\n\nCopyright (c) 2006-2010 Kirill Simonov\nCopyright (c) 2006-2011 Kirill Simonov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n### Apache License ###\n\nAll the remaining project files are covered by the Apache license:\n\nCopyright (c) 2011-2019 Canonical Ltd\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n" + "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2025 - The go-yaml Project Contributors\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { "name": "go4.org", @@ -1304,16 +1369,31 @@ "path": "gopkg.in/warnings.v0/LICENSE", "licenseText": "Copyright (c) 2016 Péter Surányi.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, - { - "name": "gopkg.in/yaml.v2", - "path": "gopkg.in/yaml.v2/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, { "name": "gopkg.in/yaml.v3", "path": "gopkg.in/yaml.v3/LICENSE", "licenseText": "\nThis project is covered by two different licenses: MIT and Apache.\n\n#### MIT License ####\n\nThe following files were ported to Go from C files of libyaml, and thus\nare still covered by their original MIT license, with the additional\ncopyright staring in 2011 when the project was ported over:\n\n apic.go emitterc.go parserc.go readerc.go scannerc.go\n writerc.go yamlh.go yamlprivateh.go\n\nCopyright (c) 2006-2010 Kirill Simonov\nCopyright (c) 2006-2011 Kirill Simonov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n### Apache License ###\n\nAll the remaining project files are covered by the Apache license:\n\nCopyright (c) 2011-2019 Canonical Ltd\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n" }, + { + "name": "modernc.org/libc", + "path": "modernc.org/libc/LICENSE", + "licenseText": "Copyright (c) 2017 The Libc Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the names of the authors nor the names of the\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, + { + "name": "modernc.org/mathutil", + "path": "modernc.org/mathutil/LICENSE", + "licenseText": "Copyright (c) 2014 The mathutil Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the names of the authors nor the names of the\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, + { + "name": "modernc.org/memory", + "path": "modernc.org/memory/LICENSE", + "licenseText": "Copyright (c) 2017 The Memory Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the names of the authors nor the names of the\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, + { + "name": "modernc.org/sqlite", + "path": "modernc.org/sqlite/LICENSE", + "licenseText": "Copyright (c) 2017 The Sqlite Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation\nand/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors\nmay be used to endorse or promote products derived from this software without\nspecific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "mvdan.cc/xurls/v2", "path": "mvdan.cc/xurls/v2/LICENSE", diff --git a/build/generate-gitignores.go b/build/generate-gitignores.go index 1e09c83a6a..7a498f548e 100644 --- a/build/generate-gitignores.go +++ b/build/generate-gitignores.go @@ -1,3 +1,6 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + //go:build ignore package main diff --git a/build/generate-go-licenses.go b/build/generate-go-licenses.go index b710fdb841..057e6a6e49 100644 --- a/build/generate-go-licenses.go +++ b/build/generate-go-licenses.go @@ -19,6 +19,7 @@ import ( // regexp is based on go-license, excluding README and NOTICE // https://github.com/google/go-licenses/blob/master/licenses/find.go +// also defined in vite.config.ts var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`) // primaryLicenseRe matches exact primary license filenames without suffixes. diff --git a/build/generate-openapi.go b/build/generate-openapi.go new file mode 100644 index 0000000000..7b37a5bbba --- /dev/null +++ b/build/generate-openapi.go @@ -0,0 +1,97 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// generate-openapi converts Gitea's Swagger 2.0 spec into an OpenAPI 3.0 spec. +// +// Gitea generates a Swagger 2.0 spec from code annotations (make generate-swagger). +// This tool converts it to OAS3 so that SDK generators and tools that require +// OAS3 (e.g. progenitor for Rust) can consume it directly. The conversion also +// deduplicates inline enum definitions into named schema components, producing +// cleaner SDK output with proper enum types instead of anonymous strings. +// +// Run: go run build/generate-openapi.go +// Output: templates/swagger/v1_openapi3_json.tmpl + +//go:build ignore + +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + "regexp" + "sort" + "strings" + + "code.gitea.io/gitea/build/openapi3gen" + + "github.com/getkin/kin-openapi/openapi3" +) + +const ( + swaggerSpecPath = "templates/swagger/v1_json.tmpl" + openapi3OutPath = "templates/swagger/v1_openapi3_json.tmpl" + + appSubUrlVar = "{{.SwaggerAppSubUrl}}" + appVerVar = "{{.SwaggerAppVer}}" + + appSubUrlPlaceholder = "GITEA_APP_SUB_URL_PLACEHOLDER" + appVerPlaceholder = "0.0.0-gitea-placeholder" +) + +var ( + appSubUrlRe = regexp.MustCompile(regexp.QuoteMeta(appSubUrlVar)) + appVerRe = regexp.MustCompile(regexp.QuoteMeta(appVerVar)) + + enumScanDirs = []string{ + "modules/structs", + "modules/commitstatus", + } +) + +func main() { + astEnumMap, err := openapi3gen.ScanSwaggerEnumTypes(enumScanDirs) + if err != nil { + log.Fatalf("scanning swagger:enum annotations: %v", err) + } + names := make([]string, 0, len(astEnumMap)) + for _, n := range astEnumMap { + names = append(names, n) + } + sort.Strings(names) + fmt.Fprintf(os.Stderr, "discovered %d swagger:enum types: %s\n", len(names), strings.Join(names, ", ")) + + data, err := os.ReadFile(swaggerSpecPath) + if err != nil { + log.Fatalf("reading swagger spec: %v", err) + } + + cleaned := appSubUrlRe.ReplaceAll(data, []byte(appSubUrlPlaceholder)) + cleaned = appVerRe.ReplaceAll(cleaned, []byte(appVerPlaceholder)) + + oas3, err := openapi3gen.Convert(cleaned, astEnumMap) + if err != nil { + log.Fatalf("converting to openapi 3.0: %v", err) + } + + oas3.Servers = openapi3.Servers{ + {URL: appSubUrlPlaceholder + "/api/v1"}, + } + + out, err := json.MarshalIndent(oas3, "", " ") + if err != nil { + log.Fatalf("marshaling openapi 3.0: %v", err) + } + + result := strings.ReplaceAll(string(out), appSubUrlPlaceholder, appSubUrlVar) + result = strings.ReplaceAll(result, appVerPlaceholder, appVerVar) + result = strings.TrimSpace(result) + + if err := os.WriteFile(openapi3OutPath, []byte(result), 0o644); err != nil { + log.Fatalf("writing openapi 3.0 spec: %v", err) + } + + fmt.Printf("Generated %s\n", openapi3OutPath) +} diff --git a/build/openapi3gen/convert.go b/build/openapi3gen/convert.go new file mode 100644 index 0000000000..04587b3303 --- /dev/null +++ b/build/openapi3gen/convert.go @@ -0,0 +1,281 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package openapi3gen + +import ( + "fmt" + "regexp" + "strings" + + "code.gitea.io/gitea/modules/json" + + "github.com/getkin/kin-openapi/openapi2" + "github.com/getkin/kin-openapi/openapi2conv" + "github.com/getkin/kin-openapi/openapi3" +) + +// rxDeprecated matches "deprecated" as a word at the start of a description +// or preceded by whitespace/punctuation that indicates a leading marker (e.g. +// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases +// like "not deprecated" or "previously deprecated, now supported". +var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`) + +// Convert parses a Swagger 2.0 spec and returns an OAS3 spec, applying +// Gitea-specific post-processing: file-schema fixups, URI formats, +// deprecated flags, and shared-enum extraction. +// +// astEnumMap is a value-set-key → Go-type-name map (built by +// ScanSwaggerEnumTypes). If a shared enum in the spec has no entry in the +// map, Convert returns an error — no fallback naming. +func Convert(swaggerJSON []byte, astEnumMap map[string]string) (*openapi3.T, error) { + var swagger2 openapi2.T + if err := json.Unmarshal(swaggerJSON, &swagger2); err != nil { + return nil, fmt.Errorf("parsing swagger 2.0: %w", err) + } + + oas3, err := openapi2conv.ToV3(&swagger2) + if err != nil { + return nil, fmt.Errorf("converting to openapi 3.0: %w", err) + } + + fixFileSchemas(oas3) + addURIFormats(oas3) + addDeprecatedFlags(oas3) + if err := extractSharedEnums(oas3, astEnumMap); err != nil { + return nil, err + } + return oas3, nil +} + +func fixFileSchemas(doc *openapi3.T) { + for _, pathItem := range doc.Paths.Map() { + for _, op := range []*openapi3.Operation{ + pathItem.Get, pathItem.Post, pathItem.Put, pathItem.Patch, + pathItem.Delete, pathItem.Head, pathItem.Options, pathItem.Trace, + } { + if op == nil { + continue + } + for _, resp := range op.Responses.Map() { + if resp.Value == nil { + continue + } + for _, mediaType := range resp.Value.Content { + fixSchema(mediaType.Schema) + } + } + if op.RequestBody != nil && op.RequestBody.Value != nil { + for _, mediaType := range op.RequestBody.Value.Content { + fixSchema(mediaType.Schema) + } + } + } + } +} + +// fixSchema rewrites any "type: file" schemas to the OAS3 equivalent +// (type: string, format: binary), recursing into Properties, Items, and +// AllOf/OneOf/AnyOf/Not branches. $ref nodes are skipped so shared schemas +// are rewritten exactly once when visited through their declaration. +func fixSchema(ref *openapi3.SchemaRef) { + if ref == nil || ref.Value == nil || ref.Ref != "" { + return + } + s := ref.Value + if s.Type.Is("file") { + s.Type = &openapi3.Types{"string"} + s.Format = "binary" + } + for _, p := range s.Properties { + fixSchema(p) + } + fixSchema(s.Items) + for _, sub := range s.AllOf { + fixSchema(sub) + } + for _, sub := range s.OneOf { + fixSchema(sub) + } + for _, sub := range s.AnyOf { + fixSchema(sub) + } + fixSchema(s.Not) +} + +// addURIFormats sets format: uri on string properties whose names indicate +// they hold URLs. This information is lost in Swagger 2.0 but is valuable +// for code generators. +func addURIFormats(doc *openapi3.T) { + if doc.Components == nil { + return + } + for _, schemaRef := range doc.Components.Schemas { + if schemaRef.Value == nil { + continue + } + for propName, propRef := range schemaRef.Value.Properties { + if propRef == nil || propRef.Value == nil || propRef.Ref != "" { + continue + } + prop := propRef.Value + if !prop.Type.Is("string") || prop.Format != "" { + continue + } + if isURLProperty(propName) { + prop.Format = "uri" + } + } + } +} + +func isURLProperty(name string) bool { + if strings.HasSuffix(name, "_url") { + return true + } + switch name { + case "url", "html_url", "clone_url": + return true + } + return false +} + +// addDeprecatedFlags sets deprecated: true on schema properties whose +// description starts with a "deprecated" marker (e.g. "Deprecated: true" +// or "deprecated (use X instead)"). Does not match negated phrases. +func addDeprecatedFlags(doc *openapi3.T) { + if doc.Components == nil { + return + } + for _, schemaRef := range doc.Components.Schemas { + if schemaRef.Value == nil { + continue + } + for _, propRef := range schemaRef.Value.Properties { + if propRef == nil || propRef.Value == nil || propRef.Ref != "" { + continue + } + if rxDeprecated.MatchString(propRef.Value.Description) { + propRef.Value.Deprecated = true + } + } + } +} + +type enumUsage struct { + schemaName string + propName string + propRef *openapi3.SchemaRef + inItems bool +} + +// extractSharedEnums finds identical enum arrays used by multiple schema +// properties, creates a standalone named schema for each, and replaces +// the inline enums with $ref pointers. +// +// If the derived enum name collides with an existing component schema, or +// no // swagger:enum annotation matches the value set, generation aborts +// with an actionable error — there are no silent fallbacks. +func extractSharedEnums(doc *openapi3.T, astEnumMap map[string]string) error { + if doc.Components == nil { + return nil + } + + enumGroups := map[string][]enumUsage{} + + for schemaName, schemaRef := range doc.Components.Schemas { + if schemaRef.Value == nil { + continue + } + for propName, propRef := range schemaRef.Value.Properties { + if propRef == nil || propRef.Value == nil || propRef.Ref != "" { + continue + } + if len(propRef.Value.Enum) > 1 && propRef.Value.Type.Is("string") { + key := EnumKey(propRef.Value.Enum) + enumGroups[key] = append(enumGroups[key], enumUsage{schemaName, propName, propRef, false}) + } + if propRef.Value.Type.Is("array") && propRef.Value.Items != nil && + propRef.Value.Items.Value != nil && propRef.Value.Items.Ref == "" && + len(propRef.Value.Items.Value.Enum) > 1 && propRef.Value.Items.Value.Type.Is("string") { + key := EnumKey(propRef.Value.Items.Value.Enum) + enumGroups[key] = append(enumGroups[key], enumUsage{schemaName, propName, propRef, true}) + } + } + } + + for key, usages := range enumGroups { + if len(usages) < 2 { + continue + } + + enumName, err := deriveEnumName(key, usages, astEnumMap) + if err != nil { + return err + } + if _, exists := doc.Components.Schemas[enumName]; exists { + return fmt.Errorf("enum name collision: %s already exists as a component schema", enumName) + } + + var enumValues []any + if usages[0].inItems { + enumValues = usages[0].propRef.Value.Items.Value.Enum + } else { + enumValues = usages[0].propRef.Value.Enum + } + + doc.Components.Schemas[enumName] = &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Enum: enumValues, + }, + } + + ref := "#/components/schemas/" + enumName + + for _, usage := range usages { + if usage.inItems { + usage.propRef.Value.Items = &openapi3.SchemaRef{Ref: ref} + } else { + old := usage.propRef.Value + if old.Description == "" && !old.Deprecated && old.Format == "" { + usage.propRef.Ref = ref + usage.propRef.Value = nil + } else { + usage.propRef.Value = &openapi3.Schema{ + AllOf: openapi3.SchemaRefs{ + {Ref: ref}, + }, + Description: old.Description, + Deprecated: old.Deprecated, + Format: old.Format, + } + } + } + } + } + return nil +} + +// deriveEnumName looks up a shared enum's Go type name from astEnumMap by +// value-set key. If no annotation matches, returns an error identifying the +// offending properties and the fix. +func deriveEnumName(key string, usages []enumUsage, astEnumMap map[string]string) (string, error) { + if name, ok := astEnumMap[key]; ok { + return name, nil + } + + props := map[string]bool{} + for _, u := range usages { + props[fmt.Sprintf("%s.%s", u.schemaName, u.propName)] = true + } + propList := make([]string, 0, len(props)) + for p := range props { + propList = append(propList, p) + } + return "", fmt.Errorf( + "no swagger:enum annotation matches value-set %q used by %d properties: %v; "+ + "fix by adding a named string type with // swagger:enum to modules/structs or modules/commitstatus", + key, len(usages), propList, + ) +} diff --git a/build/openapi3gen/convert_test.go b/build/openapi3gen/convert_test.go new file mode 100644 index 0000000000..a9a715e6c2 --- /dev/null +++ b/build/openapi3gen/convert_test.go @@ -0,0 +1,170 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package openapi3gen + +import ( + "strings" + "testing" + + "github.com/getkin/kin-openapi/openapi3" +) + +func TestDeriveEnumName_hit(t *testing.T) { + key := EnumKey([]any{"red", "green", "blue"}) + astMap := map[string]string{key: "Color"} + usages := []enumUsage{{schemaName: "Paint", propName: "color"}} + got, err := deriveEnumName(key, usages, astMap) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "Color" { + t.Fatalf("got %q, want %q", got, "Color") + } +} + +func TestDeriveEnumName_miss(t *testing.T) { + key := EnumKey([]any{"x", "y"}) + usages := []enumUsage{{schemaName: "Thing", propName: "kind"}} + _, err := deriveEnumName(key, usages, map[string]string{}) + if err == nil { + t.Fatal("expected miss error, got nil") + } + msg := err.Error() + if !strings.Contains(msg, "Thing.kind") { + t.Fatalf("error %q should list the missing usage", msg) + } + if !strings.Contains(msg, "swagger:enum") { + t.Fatalf("error %q should hint at the fix", msg) + } +} + +func TestExtractSharedEnums_usesASTMap(t *testing.T) { + doc := &openapi3.T{ + Components: &openapi3.Components{ + Schemas: openapi3.Schemas{ + "A": {Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: openapi3.Schemas{ + "color": {Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Enum: []any{"red", "green", "blue"}, + }}, + }, + }}, + "B": {Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: openapi3.Schemas{ + "color": {Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Enum: []any{"red", "green", "blue"}, + }}, + }, + }}, + }, + }, + } + astMap := map[string]string{EnumKey([]any{"red", "green", "blue"}): "Color"} + if err := extractSharedEnums(doc, astMap); err != nil { + t.Fatalf("extractSharedEnums: %v", err) + } + if _, ok := doc.Components.Schemas["Color"]; !ok { + t.Fatalf("expected Color schema to be extracted") + } +} + +func TestFixFileSchemas_recursesIntoNested(t *testing.T) { + fileType := func() *openapi3.SchemaRef { + return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"file"}}} + } + doc := &openapi3.T{ + Paths: openapi3.NewPaths(), + } + doc.Paths.Set("/upload", &openapi3.PathItem{ + Post: &openapi3.Operation{ + RequestBody: &openapi3.RequestBodyRef{ + Value: &openapi3.RequestBody{ + Content: openapi3.Content{ + "multipart/form-data": { + Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: openapi3.Schemas{ + "attachment": fileType(), + "items": {Value: &openapi3.Schema{ + Type: &openapi3.Types{"array"}, + Items: fileType(), + }}, + "alt": {Value: &openapi3.Schema{ + AllOf: openapi3.SchemaRefs{fileType()}, + }}, + "one": {Value: &openapi3.Schema{ + OneOf: openapi3.SchemaRefs{fileType()}, + }}, + "any": {Value: &openapi3.Schema{ + AnyOf: openapi3.SchemaRefs{fileType()}, + }}, + "not": {Value: &openapi3.Schema{ + Not: fileType(), + }}, + }, + }}, + }, + }, + }, + }, + Responses: openapi3.NewResponses(), + }, + }) + + fixFileSchemas(doc) + + props := doc.Paths.Value("/upload").Post.RequestBody.Value.Content["multipart/form-data"].Schema.Value.Properties + if !props["attachment"].Value.Type.Is("string") || props["attachment"].Value.Format != "binary" { + t.Errorf("nested property not fixed: %+v", props["attachment"].Value) + } + if !props["items"].Value.Items.Value.Type.Is("string") || props["items"].Value.Items.Value.Format != "binary" { + t.Errorf("array items not fixed: %+v", props["items"].Value.Items.Value) + } + if !props["alt"].Value.AllOf[0].Value.Type.Is("string") || props["alt"].Value.AllOf[0].Value.Format != "binary" { + t.Errorf("allOf branch not fixed: %+v", props["alt"].Value.AllOf[0].Value) + } + if !props["one"].Value.OneOf[0].Value.Type.Is("string") || props["one"].Value.OneOf[0].Value.Format != "binary" { + t.Errorf("oneOf branch not fixed: %+v", props["one"].Value.OneOf[0].Value) + } + if !props["any"].Value.AnyOf[0].Value.Type.Is("string") || props["any"].Value.AnyOf[0].Value.Format != "binary" { + t.Errorf("anyOf branch not fixed: %+v", props["any"].Value.AnyOf[0].Value) + } + if !props["not"].Value.Not.Value.Type.Is("string") || props["not"].Value.Not.Value.Format != "binary" { + t.Errorf("not branch not fixed: %+v", props["not"].Value.Not.Value) + } +} + +func TestExtractSharedEnums_missReturnsError(t *testing.T) { + doc := &openapi3.T{ + Components: &openapi3.Components{ + Schemas: openapi3.Schemas{ + "A": {Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: openapi3.Schemas{ + "color": {Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Enum: []any{"red", "green"}, + }}, + }, + }}, + "B": {Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: openapi3.Schemas{ + "color": {Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Enum: []any{"red", "green"}, + }}, + }, + }}, + }, + }, + } + if err := extractSharedEnums(doc, map[string]string{}); err == nil { + t.Fatal("expected miss error") + } +} diff --git a/build/openapi3gen/enumscan.go b/build/openapi3gen/enumscan.go new file mode 100644 index 0000000000..dd11620549 --- /dev/null +++ b/build/openapi3gen/enumscan.go @@ -0,0 +1,188 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// Package openapi3gen converts Gitea's Swagger 2.0 spec to an OpenAPI 3.0 +// spec. It discovers Go enum type names by scanning swagger:enum annotations +// in the source tree, then names extracted shared-enum schemas accordingly. +package openapi3gen + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" +) + +// EnumKey returns a canonical key for a set of enum values: values are +// stringified, sorted, and joined with "|". Used to match enum value sets +// across spec properties and scanned Go type declarations. +func EnumKey(values []any) string { + strs := make([]string, len(values)) + for i, v := range values { + strs[i] = fmt.Sprintf("%v", v) + } + sort.Strings(strs) + return strings.Join(strs, "|") +} + +var rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`) + +// ScanSwaggerEnumTypes walks .go files under each dir and returns a map from +// a canonical value-set key (see EnumKey) to the Go type name declared with +// // swagger:enum TypeName. +// +// Returns an error on parse failure, on an annotation for a type whose +// constants can't be extracted, or on value-set collisions between two +// different enum types. +func ScanSwaggerEnumTypes(dirs []string) (map[string]string, error) { + fset := token.NewFileSet() + parsed := []*ast.File{} + + for _, dir := range dirs { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", dir, err) + } + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { + continue + } + if strings.HasSuffix(entry.Name(), "_test.go") { + continue + } + path := filepath.Join(dir, entry.Name()) + file, err := parser.ParseFile(fset, path, nil, parser.ParseComments) + if err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + parsed = append(parsed, file) + } + } + + enumTypes := map[string]string{} // typeName → "" (presence marker) + enumValues := map[string][]any{} // typeName → values + + // Pass 1: collect every // swagger:enum TypeName declaration. + for _, file := range parsed { + for _, decl := range file.Decls { + gd, ok := decl.(*ast.GenDecl) + if !ok || gd.Tok != token.TYPE { + continue + } + if err := collectEnumType(gd, enumTypes); err != nil { + return nil, fmt.Errorf("%s: %w", fset.Position(gd.Pos()).Filename, err) + } + } + } + + // Pass 2: collect const values; now every annotated type is visible. + for _, file := range parsed { + for _, decl := range file.Decls { + gd, ok := decl.(*ast.GenDecl) + if !ok || gd.Tok != token.CONST { + continue + } + collectEnumValues(gd, enumTypes, enumValues) + } + } + + result := map[string]string{} + for typeName := range enumTypes { + values, ok := enumValues[typeName] + if !ok || len(values) == 0 { + return nil, fmt.Errorf("swagger:enum %s has no const block with typed string values", typeName) + } + key := EnumKey(values) + if existing, ok := result[key]; ok && existing != typeName { + return nil, fmt.Errorf("swagger:enum value-set collision: %s and %s both use %q", existing, typeName, key) + } + result[key] = typeName + } + return result, nil +} + +// collectEnumType scans a `type` GenDecl for // swagger:enum annotations, +// handling both the lone form (`// swagger:enum Foo\n type Foo string`) +// where the comment group is attached to the GenDecl, and the grouped form: +// +// type ( +// // swagger:enum Foo +// Foo string +// ) +// +// where the comment group is attached to each TypeSpec. Caveat: Go's parser +// only attaches a CommentGroup when it is immediately adjacent to the decl. +// A blank line (not a `//` continuation line) between the comment and the +// declaration drops the Doc, so annotations MUST sit directly above their +// type. All current annotated files obey this — the rule is noted here so +// a future edit that inserts a blank line fails fast rather than silently. +func collectEnumType(gd *ast.GenDecl, enumTypes map[string]string) error { + if err := registerEnumAnnotation(gd.Doc, gd.Specs, enumTypes); err != nil { + return err + } + for _, spec := range gd.Specs { + ts, ok := spec.(*ast.TypeSpec) + if !ok || ts.Doc == nil { + continue + } + if err := registerEnumAnnotation(ts.Doc, []ast.Spec{ts}, enumTypes); err != nil { + return err + } + } + return nil +} + +func registerEnumAnnotation(doc *ast.CommentGroup, specs []ast.Spec, enumTypes map[string]string) error { + if doc == nil { + return nil + } + matches := rxSwaggerEnum.FindStringSubmatch(doc.Text()) + if len(matches) < 2 { + return nil + } + annotated := matches[1] + for _, spec := range specs { + ts, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + if ts.Name.Name == annotated { + enumTypes[annotated] = "" + return nil + } + } + return fmt.Errorf("swagger:enum %s: no type declaration with that name in the same decl group; check for a typo", annotated) +} + +func collectEnumValues(gd *ast.GenDecl, enumTypes map[string]string, enumValues map[string][]any) { + for _, spec := range gd.Specs { + vs, ok := spec.(*ast.ValueSpec) + if !ok || vs.Type == nil { + continue + } + ident, ok := vs.Type.(*ast.Ident) + if !ok { + continue + } + if _, isEnum := enumTypes[ident.Name]; !isEnum { + continue + } + for _, val := range vs.Values { + lit, ok := val.(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + continue + } + unquoted, err := strconv.Unquote(lit.Value) + if err != nil { + continue + } + enumValues[ident.Name] = append(enumValues[ident.Name], unquoted) + } + } +} diff --git a/build/openapi3gen/enumscan_test.go b/build/openapi3gen/enumscan_test.go new file mode 100644 index 0000000000..2e5fe99db0 --- /dev/null +++ b/build/openapi3gen/enumscan_test.go @@ -0,0 +1,239 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package openapi3gen + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestEnumKey_sortsAndJoins(t *testing.T) { + key := EnumKey([]any{"b", "a", "c"}) + if key != "a|b|c" { + t.Fatalf("EnumKey = %q, want %q", key, "a|b|c") + } +} + +func TestEnumKey_handlesNonStringValues(t *testing.T) { + key := EnumKey([]any{2, 1, 3}) + if key != "1|2|3" { + t.Fatalf("EnumKey = %q, want %q", key, "1|2|3") + } +} + +func TestScanSwaggerEnumTypes_basic(t *testing.T) { + dir := t.TempDir() + src := `package fixture + +// Color is a primary color. +// swagger:enum Color +type Color string + +const ( + ColorRed Color = "red" + ColorGreen Color = "green" + ColorBlue Color = "blue" +) +` + if err := os.WriteFile(filepath.Join(dir, "color.go"), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + + got, err := ScanSwaggerEnumTypes([]string{dir}) + if err != nil { + t.Fatalf("ScanSwaggerEnumTypes: %v", err) + } + wantKey := EnumKey([]any{"red", "green", "blue"}) + if got[wantKey] != "Color" { + t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Color") + } +} + +func TestScanSwaggerEnumTypes_orphanAnnotation(t *testing.T) { + dir := t.TempDir() + src := `package fixture + +// swagger:enum Sttype +type StateType string + +const ( + StateOpen StateType = "open" +) +` + if err := os.WriteFile(filepath.Join(dir, "typo.go"), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + + _, err := ScanSwaggerEnumTypes([]string{dir}) + if err == nil { + t.Fatal("expected error for annotation referencing a non-matching type name") + } + if !strings.Contains(err.Error(), "Sttype") { + t.Fatalf("error %q should mention the typo'd name Sttype", err.Error()) + } +} + +func TestScanSwaggerEnumTypes_collision(t *testing.T) { + dir := t.TempDir() + src := `package fixture + +// swagger:enum Alpha +type Alpha string +const ( + AlphaX Alpha = "x" + AlphaY Alpha = "y" +) + +// swagger:enum Beta +type Beta string +const ( + BetaX Beta = "x" + BetaY Beta = "y" +) +` + if err := os.WriteFile(filepath.Join(dir, "dup.go"), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + + _, err := ScanSwaggerEnumTypes([]string{dir}) + if err == nil { + t.Fatal("expected collision error, got nil") + } + msg := err.Error() + if !strings.Contains(msg, "Alpha") || !strings.Contains(msg, "Beta") { + t.Fatalf("error %q should mention both Alpha and Beta", msg) + } +} + +func TestScanSwaggerEnumTypes_parseFailure(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "bad.go"), []byte("package fixture\nfunc Foo() {"), 0o644); err != nil { + t.Fatal(err) + } + + _, err := ScanSwaggerEnumTypes([]string{dir}) + if err == nil { + t.Fatal("expected parse error, got nil") + } +} + +func TestScanSwaggerEnumTypes_annotationWithoutConsts(t *testing.T) { + dir := t.TempDir() + src := `package fixture + +// swagger:enum Lonely +type Lonely string +` + if err := os.WriteFile(filepath.Join(dir, "lonely.go"), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + + _, err := ScanSwaggerEnumTypes([]string{dir}) + if err == nil { + t.Fatal("expected error for annotation without consts") + } + if !strings.Contains(err.Error(), "Lonely") { + t.Fatalf("error %q should mention Lonely", err.Error()) + } +} + +func TestScanSwaggerEnumTypes_constsAndTypeInDifferentFiles(t *testing.T) { + dir := t.TempDir() + // Name ordering: `a_consts.go` < `b_type.go`, so readdir returns consts first. + // Old single-pass scanner would miss the values; two-pass must not. + constsSrc := `package fixture + +const ( + HueA Hue = "a" + HueB Hue = "b" +) +` + typeSrc := `package fixture + +// swagger:enum Hue +type Hue string +` + if err := os.WriteFile(filepath.Join(dir, "a_consts.go"), []byte(constsSrc), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "b_type.go"), []byte(typeSrc), 0o644); err != nil { + t.Fatal(err) + } + + got, err := ScanSwaggerEnumTypes([]string{dir}) + if err != nil { + t.Fatalf("ScanSwaggerEnumTypes: %v", err) + } + wantKey := EnumKey([]any{"a", "b"}) + if got[wantKey] != "Hue" { + t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Hue") + } +} + +func TestScanSwaggerEnumTypes_constsBeforeType(t *testing.T) { + dir := t.TempDir() + src := `package fixture + +const ( + ShadeDark Shade = "dark" + ShadeLight Shade = "light" +) + +// swagger:enum Shade +type Shade string +` + if err := os.WriteFile(filepath.Join(dir, "shade.go"), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + + got, err := ScanSwaggerEnumTypes([]string{dir}) + if err != nil { + t.Fatalf("ScanSwaggerEnumTypes: %v", err) + } + wantKey := EnumKey([]any{"dark", "light"}) + if got[wantKey] != "Shade" { + t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Shade") + } +} + +func TestScanSwaggerEnumTypes_groupedTypeDecl(t *testing.T) { + dir := t.TempDir() + src := `package fixture + +type ( + // swagger:enum Color + Color string + // swagger:enum Shade + Shade string +) + +const ( + ColorRed Color = "red" + ColorBlue Color = "blue" +) + +const ( + ShadeDark Shade = "dark" + ShadeLight Shade = "light" +) +` + if err := os.WriteFile(filepath.Join(dir, "grouped.go"), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + + got, err := ScanSwaggerEnumTypes([]string{dir}) + if err != nil { + t.Fatalf("ScanSwaggerEnumTypes: %v", err) + } + colorKey := EnumKey([]any{"red", "blue"}) + shadeKey := EnumKey([]any{"dark", "light"}) + if got[colorKey] != "Color" { + t.Fatalf("Color: map[%q] = %q, want %q", colorKey, got[colorKey], "Color") + } + if got[shadeKey] != "Shade" { + t.Fatalf("Shade: map[%q] = %q, want %q", shadeKey, got[shadeKey], "Shade") + } +} diff --git a/build/test-env-check.sh b/build/test-env-check.sh deleted file mode 100755 index 38e5a28823..0000000000 --- a/build/test-env-check.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh - -set -e - -if [ ! -f ./build/test-env-check.sh ]; then - echo "${0} can only be executed in gitea source root directory" - exit 1 -fi - - -echo "check uid ..." - -# the uid of gitea defined in "https://gitea.com/gitea/test-env" is 1000 -gitea_uid=$(id -u gitea) -if [ "$gitea_uid" != "1000" ]; then - echo "The uid of linux user 'gitea' is expected to be 1000, but it is $gitea_uid" - exit 1 -fi - -cur_uid=$(id -u) -if [ "$cur_uid" != "0" -a "$cur_uid" != "$gitea_uid" ]; then - echo "The uid of current linux user is expected to be 0 or $gitea_uid, but it is $cur_uid" - exit 1 -fi diff --git a/build/test-env-prepare.sh b/build/test-env-prepare.sh deleted file mode 100755 index 0c5bc25f11..0000000000 --- a/build/test-env-prepare.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -set -e - -if [ ! -f ./build/test-env-prepare.sh ]; then - echo "${0} can only be executed in gitea source root directory" - exit 1 -fi - -echo "change the owner of files to gitea ..." -chown -R gitea:gitea . diff --git a/cmd/admin_user_change_password_test.go b/cmd/admin_user_change_password_test.go index 902632f3e4..cf497517f7 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 4242d8915b..c5775e5204 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 a36d05c76e..0000000000 --- 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 0000000000..4ff854f8ab --- /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/dump.go b/cmd/dump.go index 49f4d9e894..40b73f69aa 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -203,8 +203,8 @@ func runDump(ctx context.Context, cmd *cli.Command) error { } }() - targetDBType := cmd.String("database") - if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() { + targetDBType := setting.DatabaseType(cmd.String("database")) + if targetDBType != "" && targetDBType != setting.Database.Type { log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType) } else { log.Info("Dumping database...") diff --git a/cmd/generate.go b/cmd/generate.go index b94ff79aae..21f8b42bff 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -78,11 +78,7 @@ func runGenerateInternalToken(_ context.Context, c *cli.Command) error { } func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error { - _, jwtSecretBase64, err := generate.NewJwtSecretWithBase64() - if err != nil { - return err - } - + _, jwtSecretBase64 := generate.NewJwtSecretWithBase64() fmt.Printf("%s", jwtSecretBase64) if isatty.IsTerminal(os.Stdout.Fd()) { diff --git a/cmd/cmd.go b/cmd/helper.go similarity index 96% rename from cmd/cmd.go rename to cmd/helper.go index 25e90a1695..ca4cddb49d 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 @@ -134,7 +134,7 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cl if globalBool(c, "debug") || globalBool(c, "verbose") { level = log.TRACE } - log.SetConsoleLogger(log.DEFAULT, "console-default", level) + log.SetupStderrLogger(log.DEFAULT, "console-stderr", level) return ctx, nil } } diff --git a/cmd/main.go b/cmd/main.go index a6b89a6fad..27d8cba2e9 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 b1f6bb3ba9..f367bf12e4 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/cmd/web.go b/cmd/web.go index 994c481fc0..19ced5a336 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -150,7 +150,7 @@ func serveInstall(cmd *cli.Command) error { c := install.Routes() err := listen(c, false) if err != nil { - log.Critical("Unable to open listener for installer. Is Gitea already running?") + log.Error("Unable to open listener for installer. Is Gitea already running?") graceful.GetManager().DoGracefulShutdown() } select { @@ -375,7 +375,7 @@ func listen(m http.Handler, handleRedirector bool) error { log.Fatal("Invalid protocol: %s", setting.Protocol) } if err != nil { - log.Critical("Failed to start server: %v", err) + log.Error("Failed to start server: %v", err) } log.Info("HTTP Listener: %s Closed", listenAddr) return err diff --git a/contrib/backport/README b/contrib/backport/README deleted file mode 100644 index 466b79c6d4..0000000000 --- a/contrib/backport/README +++ /dev/null @@ -1,41 +0,0 @@ -`backport` -========== - -`backport` is a command to help create backports of PRs. It backports a -provided PR from main on to a released version. - -It will create a backport branch, cherry-pick the PR's merge commit, adjust -the commit message and then push this back up to your fork's remote. - -The default version will read from `docs/config.yml`. You can override this -using the option `--version`. - -The upstream branches will be fetched, using the remote `origin`. This can -be overridden using `--upstream`, and fetching can be avoided using -`--no-fetch`. - -By default the branch created will be called `backport-$PR-$VERSION`. You -can override this using the option `--backport-branch`. This branch will -be created from `--release-branch` which is `release/$(VERSION)` -by default and will be pulled from `$(UPSTREAM)`. - -The merge-commit as determined by the github API will be used as the SHA to -cherry-pick. You can override this using `--cherry-pick`. - -The commit message will be amended to add the `Backport` header. -`--no-amend-message` can be set to stop this from happening. - -If cherry-pick is successful the backported branch will be pushed up to your -fork using your remote. These will be determined using `git remote -v`. You -can set your fork name using `--fork-user` and your remote name using -`--remote`. You can avoid pushing using `--no-push`. - -If the push is successful, `xdg-open` will be called to open a backport url. -You can stop this using `--no-xdg-open`. - -Installation -============ - -```bash -go install contrib/backport/backport.go -``` diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go deleted file mode 100644 index 5811291b42..0000000000 --- a/contrib/backport/backport.go +++ /dev/null @@ -1,456 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//nolint:forbidigo // use of print functions is allowed in cli -package main - -import ( - "context" - "errors" - "fmt" - "log" - "net/http" - "os" - "os/exec" - "path" - "strconv" - "strings" - - "github.com/google/go-github/v74/github" - "github.com/urfave/cli/v3" - "gopkg.in/yaml.v3" -) - -const defaultVersion = "v1.18" // to backport to - -func main() { - app := &cli.Command{} - app.Name = "backport" - app.Usage = "Backport provided PR-number on to the current or previous released version" - app.Description = `Backport will look-up the PR in Gitea's git log and attempt to cherry-pick it on the current version` - app.ArgsUsage = "" - - app.Flags = []cli.Flag{ - &cli.StringFlag{ - Name: "version", - Usage: "Version branch to backport on to", - }, - &cli.StringFlag{ - Name: "upstream", - Value: "origin", - Usage: "Upstream remote for the Gitea upstream", - }, - &cli.StringFlag{ - Name: "release-branch", - Value: "", - Usage: "Release branch to backport on. Will default to release/", - }, - &cli.StringFlag{ - Name: "cherry-pick", - Usage: "SHA to cherry-pick as backport", - }, - &cli.StringFlag{ - Name: "backport-branch", - Usage: "Backport branch to backport on to (default: backport--", - }, - &cli.StringFlag{ - Name: "remote", - Value: "", - Usage: "Remote for your fork of the Gitea upstream", - }, - &cli.StringFlag{ - Name: "fork-user", - Value: "", - Usage: "Forked user name on Github", - }, - &cli.StringFlag{ - Name: "gh-access-token", - Value: "", - Usage: "Access token for GitHub api request", - }, - &cli.BoolFlag{ - Name: "no-fetch", - Usage: "Set this flag to prevent fetch of remote branches", - }, - &cli.BoolFlag{ - Name: "no-amend-message", - Usage: "Set this flag to prevent automatic amendment of the commit message", - }, - &cli.BoolFlag{ - Name: "no-push", - Usage: "Set this flag to prevent pushing the backport up to your fork", - }, - &cli.BoolFlag{ - Name: "no-xdg-open", - Usage: "Set this flag to not use xdg-open to open the PR URL", - }, - &cli.BoolFlag{ - Name: "continue", - Usage: "Set this flag to continue from a git cherry-pick that has broken", - }, - } - cli.RootCommandHelpTemplate = `NAME: - {{.Name}} - {{.Usage}} -USAGE: - {{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} - {{if len .Authors}} -AUTHOR: - {{range .Authors}}{{ . }}{{end}} - {{end}}{{if .Commands}} -OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}} -` - - app.Action = runBackport - if err := app.Run(context.Background(), os.Args); err != nil { - fmt.Fprintf(os.Stderr, "Unable to backport: %v\n", err) - } -} - -func runBackport(ctx context.Context, c *cli.Command) error { - continuing := c.Bool("continue") - - var pr string - - version := c.String("version") - if version == "" && continuing { - // determine version from current branch name - var err error - pr, version, err = readCurrentBranch(ctx) - if err != nil { - return err - } - } - if version == "" { - version = readVersion() - } - if version == "" { - version = defaultVersion - } - - upstream := c.String("upstream") - if upstream == "" { - upstream = "origin" - } - - forkUser := c.String("fork-user") - remote := c.String("remote") - if remote == "" && !c.Bool("--no-push") { - var err error - remote, forkUser, err = determineRemote(ctx, forkUser) - if err != nil { - return err - } - } - - upstreamReleaseBranch := c.String("release-branch") - if upstreamReleaseBranch == "" { - upstreamReleaseBranch = path.Join("release", version) - } - - localReleaseBranch := path.Join(upstream, upstreamReleaseBranch) - - args := c.Args().Slice() - if len(args) == 0 && pr == "" { - return errors.New("no PR number provided\nProvide a PR number to backport") - } else if len(args) != 1 && pr == "" { - return fmt.Errorf("multiple PRs provided %v\nOnly a single PR can be backported at a time", args) - } - if pr == "" { - pr = args[0] - } - - backportBranch := c.String("backport-branch") - if backportBranch == "" { - backportBranch = "backport-" + pr + "-" + version - } - - fmt.Printf("* Backporting %s to %s as %s\n", pr, localReleaseBranch, backportBranch) - - sha := c.String("cherry-pick") - accessToken := c.String("gh-access-token") - if sha == "" { - var err error - sha, err = determineSHAforPR(ctx, pr, accessToken) - if err != nil { - return err - } - } - if sha == "" { - return fmt.Errorf("unable to determine sha for cherry-pick of %s", pr) - } - - if !c.Bool("no-fetch") { - if err := fetchRemoteAndMain(ctx, upstream, upstreamReleaseBranch); err != nil { - return err - } - } - - if !continuing { - if err := checkoutBackportBranch(ctx, backportBranch, localReleaseBranch); err != nil { - return err - } - } - - if err := cherrypick(ctx, sha); err != nil { - return err - } - - if !c.Bool("no-amend-message") { - if err := amendCommit(ctx, pr); err != nil { - return err - } - } - - if !c.Bool("no-push") { - url := "https://github.com/go-gitea/gitea/compare/" + upstreamReleaseBranch + "..." + forkUser + ":" + backportBranch - - if err := gitPushUp(ctx, remote, backportBranch); err != nil { - return err - } - - if !c.Bool("no-xdg-open") { - if err := xdgOpen(ctx, url); err != nil { - return err - } - } else { - fmt.Printf("* Navigate to %s to open PR\n", url) - } - } - return nil -} - -func xdgOpen(ctx context.Context, url string) error { - fmt.Printf("* `xdg-open %s`\n", url) - out, err := exec.CommandContext(ctx, "xdg-open", url).Output() - if err != nil { - fmt.Fprintf(os.Stderr, "%s", string(out)) - return fmt.Errorf("unable to xdg-open to %s: %w", url, err) - } - return nil -} - -func gitPushUp(ctx context.Context, remote, backportBranch string) error { - fmt.Printf("* `git push -u %s %s`\n", remote, backportBranch) - out, err := exec.CommandContext(ctx, "git", "push", "-u", remote, backportBranch).Output() - if err != nil { - fmt.Fprintf(os.Stderr, "%s", string(out)) - return fmt.Errorf("unable to push up to %s: %w", remote, err) - } - return nil -} - -func amendCommit(ctx context.Context, pr string) error { - fmt.Printf("* Amending commit to prepend `Backport #%s` to body\n", pr) - out, err := exec.CommandContext(ctx, "git", "log", "-1", "--pretty=format:%B").Output() - if err != nil { - fmt.Fprintf(os.Stderr, "%s", string(out)) - return fmt.Errorf("unable to get last log message: %w", err) - } - - parts := strings.SplitN(string(out), "\n", 2) - - if len(parts) != 2 { - return fmt.Errorf("unable to interpret log message:\n%s", string(out)) - } - subject, body := parts[0], parts[1] - if !strings.HasSuffix(subject, " (#"+pr+")") { - subject = subject + " (#" + pr + ")" - } - - out, err = exec.CommandContext(ctx, "git", "commit", "--amend", "-m", subject+"\n\nBackport #"+pr+"\n"+body).Output() - if err != nil { - fmt.Fprintf(os.Stderr, "%s", string(out)) - return fmt.Errorf("unable to amend last log message: %w", err) - } - return nil -} - -func cherrypick(ctx context.Context, sha string) error { - // Check if a CHERRY_PICK_HEAD exists - if _, err := os.Stat(".git/CHERRY_PICK_HEAD"); err == nil { - // Assume that we are in the middle of cherry-pick - continue it - fmt.Println("* Attempting git cherry-pick --continue") - out, err := exec.CommandContext(ctx, "git", "cherry-pick", "--continue").Output() - if err != nil { - fmt.Fprintf(os.Stderr, "git cherry-pick --continue failed:\n%s\n", string(out)) - return fmt.Errorf("unable to continue cherry-pick: %w", err) - } - return nil - } - - fmt.Printf("* Attempting git cherry-pick %s\n", sha) - out, err := exec.CommandContext(ctx, "git", "cherry-pick", sha).Output() - if err != nil { - fmt.Fprintf(os.Stderr, "git cherry-pick %s failed:\n%s\n", sha, string(out)) - return fmt.Errorf("git cherry-pick %s failed: %w", sha, err) - } - return nil -} - -func checkoutBackportBranch(ctx context.Context, backportBranch, releaseBranch string) error { - out, err := exec.CommandContext(ctx, "git", "branch", "--show-current").Output() - if err != nil { - return fmt.Errorf("unable to check current branch %w", err) - } - - currentBranch := strings.TrimSpace(string(out)) - fmt.Printf("* Current branch is %s\n", currentBranch) - if currentBranch == backportBranch { - fmt.Printf("* Current branch is %s - not checking out\n", currentBranch) - return nil - } - - if _, err := exec.CommandContext(ctx, "git", "rev-list", "-1", backportBranch).Output(); err == nil { - fmt.Printf("* Branch %s already exists. Checking it out...\n", backportBranch) - return exec.CommandContext(ctx, "git", "checkout", "-f", backportBranch).Run() - } - - fmt.Printf("* `git checkout -b %s %s`\n", backportBranch, releaseBranch) - return exec.CommandContext(ctx, "git", "checkout", "-b", backportBranch, releaseBranch).Run() -} - -func fetchRemoteAndMain(ctx context.Context, remote, releaseBranch string) error { - fmt.Printf("* `git fetch %s main`\n", remote) - out, err := exec.CommandContext(ctx, "git", "fetch", remote, "main").Output() - if err != nil { - fmt.Println(string(out)) - return fmt.Errorf("unable to fetch %s from %s: %w", "main", remote, err) - } - fmt.Println(string(out)) - - fmt.Printf("* `git fetch %s %s`\n", remote, releaseBranch) - out, err = exec.CommandContext(ctx, "git", "fetch", remote, releaseBranch).Output() - if err != nil { - fmt.Println(string(out)) - return fmt.Errorf("unable to fetch %s from %s: %w", releaseBranch, remote, err) - } - fmt.Println(string(out)) - - return nil -} - -func determineRemote(ctx context.Context, forkUser string) (string, string, error) { - out, err := exec.CommandContext(ctx, "git", "remote", "-v").Output() - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to list git remotes:\n%s\n", string(out)) - return "", "", fmt.Errorf("unable to determine forked remote: %w", err) - } - lines := strings.SplitSeq(string(out), "\n") - for line := range lines { - fields := strings.Split(line, "\t") - name, remote := fields[0], fields[1] - // only look at pushers - if !strings.HasSuffix(remote, " (push)") { - continue - } - // only look at github.com pushes - if !strings.Contains(remote, "github.com") { - continue - } - // ignore go-gitea/gitea - if strings.Contains(remote, "go-gitea/gitea") { - continue - } - if !strings.Contains(remote, forkUser) { - continue - } - if after, ok := strings.CutPrefix(remote, "git@github.com:"); ok { - forkUser = after - } else if after, ok := strings.CutPrefix(remote, "https://github.com/"); ok { - forkUser = after - } else if after, ok := strings.CutPrefix(remote, "https://www.github.com/"); ok { - forkUser = after - } else if forkUser == "" { - return "", "", fmt.Errorf("unable to extract forkUser from remote %s: %s", name, remote) - } - idx := strings.Index(forkUser, "/") - if idx >= 0 { - forkUser = forkUser[:idx] - } - return name, forkUser, nil - } - return "", "", fmt.Errorf("unable to find appropriate remote in:\n%s", string(out)) -} - -func readCurrentBranch(ctx context.Context) (pr, version string, err error) { - out, err := exec.CommandContext(ctx, "git", "branch", "--show-current").Output() - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to read current git branch:\n%s\n", string(out)) - return "", "", fmt.Errorf("unable to read current git branch: %w", err) - } - parts := strings.Split(strings.TrimSpace(string(out)), "-") - - if len(parts) != 3 || parts[0] != "backport" { - fmt.Fprintf(os.Stderr, "Unable to continue from git branch:\n%s\n", string(out)) - return "", "", fmt.Errorf("unable to continue from git branch:\n%s", string(out)) - } - - return parts[1], parts[2], nil -} - -func readVersion() string { - bs, err := os.ReadFile("docs/config.yaml") - if err != nil { - if err == os.ErrNotExist { - log.Println("`docs/config.yaml` not present") - return "" - } - fmt.Fprintf(os.Stderr, "Unable to read `docs/config.yaml`: %v\n", err) - return "" - } - - type params struct { - Version string - } - type docConfig struct { - Params params - } - dc := &docConfig{} - if err := yaml.Unmarshal(bs, dc); err != nil { - fmt.Fprintf(os.Stderr, "Unable to read `docs/config.yaml`: %v\n", err) - return "" - } - - if dc.Params.Version == "" { - fmt.Fprintf(os.Stderr, "No version in `docs/config.yaml`") - return "" - } - - version := dc.Params.Version - if version[0] != 'v' { - version = "v" + version - } - - split := strings.SplitN(version, ".", 3) - - return strings.Join(split[:2], ".") -} - -func determineSHAforPR(ctx context.Context, prStr, accessToken string) (string, error) { - prNum, err := strconv.Atoi(prStr) - if err != nil { - return "", err - } - - client := github.NewClient(http.DefaultClient) - if accessToken != "" { - client = client.WithAuthToken(accessToken) - } - - pr, _, err := client.PullRequests.Get(ctx, "go-gitea", "gitea", prNum) - if err != nil { - return "", err - } - - if pr.Merged == nil || !*pr.Merged { - return "", fmt.Errorf("PR #%d is not yet merged - cannot determine sha to backport", prNum) - } - - if pr.MergeCommitSHA != nil { - return *pr.MergeCommitSHA, nil - } - - return "", nil -} diff --git a/contrib/ide/README.md b/contrib/development/README.md similarity index 100% rename from contrib/ide/README.md rename to contrib/development/README.md diff --git a/contrib/ide/vscode/launch.json b/contrib/development/vscode/launch.json similarity index 50% rename from contrib/ide/vscode/launch.json rename to contrib/development/vscode/launch.json index b80b826fc0..048428fc27 100644 --- a/contrib/ide/vscode/launch.json +++ b/contrib/development/vscode/launch.json @@ -13,19 +13,6 @@ }, "args": ["web"], "showLog": true - }, - { - "name": "Launch (with SQLite3)", - "type": "go", - "request": "launch", - "mode": "debug", - "buildFlags": "-tags='sqlite sqlite_unlock_notify'", - "program": "${workspaceRoot}/main.go", - "env": { - "GITEA_WORK_DIR": "${workspaceRoot}", - }, - "args": ["web"], - "showLog": true } ] } diff --git a/contrib/development/vscode/settings.json b/contrib/development/vscode/settings.json new file mode 100644 index 0000000000..d968e4daca --- /dev/null +++ b/contrib/development/vscode/settings.json @@ -0,0 +1,4 @@ +{ + "go.buildTags": "", + "go.testFlags": ["-v"] +} diff --git a/contrib/development/vscode/tasks.json b/contrib/development/vscode/tasks.json new file mode 100644 index 0000000000..490e30338b --- /dev/null +++ b/contrib/development/vscode/tasks.json @@ -0,0 +1,27 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build", + "type": "shell", + "command": "go", + "group": "build", + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared" + }, + "linux": { + "args": ["build", "-o", "gitea", "${workspaceRoot}/main.go" ] + }, + "osx": { + "args": ["build", "-o", "gitea", "${workspaceRoot}/main.go" ] + }, + "windows": { + "args": ["build", "-o", "gitea.exe", "\"${workspaceRoot}\\main.go\""] + }, + "problemMatcher": ["$go"] + } + ] +} diff --git a/contrib/gitea-monitoring-mixin/.gitignore b/contrib/grafana-monitoring-mixin/.gitignore similarity index 100% rename from contrib/gitea-monitoring-mixin/.gitignore rename to contrib/grafana-monitoring-mixin/.gitignore diff --git a/contrib/gitea-monitoring-mixin/Makefile b/contrib/grafana-monitoring-mixin/Makefile similarity index 100% rename from contrib/gitea-monitoring-mixin/Makefile rename to contrib/grafana-monitoring-mixin/Makefile diff --git a/contrib/gitea-monitoring-mixin/README.md b/contrib/grafana-monitoring-mixin/README.md similarity index 100% rename from contrib/gitea-monitoring-mixin/README.md rename to contrib/grafana-monitoring-mixin/README.md diff --git a/contrib/gitea-monitoring-mixin/config.libsonnet b/contrib/grafana-monitoring-mixin/config.libsonnet similarity index 100% rename from contrib/gitea-monitoring-mixin/config.libsonnet rename to contrib/grafana-monitoring-mixin/config.libsonnet diff --git a/contrib/gitea-monitoring-mixin/dashboards/dashboards.libsonnet b/contrib/grafana-monitoring-mixin/dashboards/dashboards.libsonnet similarity index 100% rename from contrib/gitea-monitoring-mixin/dashboards/dashboards.libsonnet rename to contrib/grafana-monitoring-mixin/dashboards/dashboards.libsonnet diff --git a/contrib/gitea-monitoring-mixin/dashboards/overview.libsonnet b/contrib/grafana-monitoring-mixin/dashboards/overview.libsonnet similarity index 100% rename from contrib/gitea-monitoring-mixin/dashboards/overview.libsonnet rename to contrib/grafana-monitoring-mixin/dashboards/overview.libsonnet diff --git a/contrib/gitea-monitoring-mixin/jsonnetfile.json b/contrib/grafana-monitoring-mixin/jsonnetfile.json similarity index 100% rename from contrib/gitea-monitoring-mixin/jsonnetfile.json rename to contrib/grafana-monitoring-mixin/jsonnetfile.json diff --git a/contrib/gitea-monitoring-mixin/jsonnetfile.lock.json b/contrib/grafana-monitoring-mixin/jsonnetfile.lock.json similarity index 100% rename from contrib/gitea-monitoring-mixin/jsonnetfile.lock.json rename to contrib/grafana-monitoring-mixin/jsonnetfile.lock.json diff --git a/contrib/gitea-monitoring-mixin/lib/alerts.jsonnet b/contrib/grafana-monitoring-mixin/lib/alerts.jsonnet similarity index 100% rename from contrib/gitea-monitoring-mixin/lib/alerts.jsonnet rename to contrib/grafana-monitoring-mixin/lib/alerts.jsonnet diff --git a/contrib/gitea-monitoring-mixin/lib/dashboards.jsonnet b/contrib/grafana-monitoring-mixin/lib/dashboards.jsonnet similarity index 100% rename from contrib/gitea-monitoring-mixin/lib/dashboards.jsonnet rename to contrib/grafana-monitoring-mixin/lib/dashboards.jsonnet diff --git a/contrib/gitea-monitoring-mixin/lib/rules.jsonnet b/contrib/grafana-monitoring-mixin/lib/rules.jsonnet similarity index 100% rename from contrib/gitea-monitoring-mixin/lib/rules.jsonnet rename to contrib/grafana-monitoring-mixin/lib/rules.jsonnet diff --git a/contrib/gitea-monitoring-mixin/mixin.libsonnet b/contrib/grafana-monitoring-mixin/mixin.libsonnet similarity index 100% rename from contrib/gitea-monitoring-mixin/mixin.libsonnet rename to contrib/grafana-monitoring-mixin/mixin.libsonnet diff --git a/contrib/ide/vscode/settings.json b/contrib/ide/vscode/settings.json deleted file mode 100644 index e33bccf902..0000000000 --- a/contrib/ide/vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "go.buildTags": "'sqlite sqlite_unlock_notify'", - "go.testFlags": ["-v"] -} \ No newline at end of file diff --git a/contrib/ide/vscode/tasks.json b/contrib/ide/vscode/tasks.json deleted file mode 100644 index e35ae303b2..0000000000 --- a/contrib/ide/vscode/tasks.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Build", - "type": "shell", - "command": "go", - "group": "build", - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared" - }, - "linux": { - "args": ["build", "-o", "gitea", "${workspaceRoot}/main.go" ] - }, - "osx": { - "args": ["build", "-o", "gitea", "${workspaceRoot}/main.go" ] - }, - "windows": { - "args": ["build", "-o", "gitea.exe", "\"${workspaceRoot}\\main.go\""] - }, - "problemMatcher": ["$go"] - }, - { - "label": "Build (with SQLite3)", - "type": "shell", - "command": "go", - "group": "build", - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared" - }, - "linux": { - "args": ["build", "-tags=\"sqlite sqlite_unlock_notify\"", "-o", "gitea", "${workspaceRoot}/main.go"] - }, - "osx": { - "args": ["build", "-tags=\"sqlite sqlite_unlock_notify\"", "-o", "gitea", "${workspaceRoot}/main.go"] - }, - "windows": { - "args": ["build", "-tags=\"sqlite sqlite_unlock_notify\"", "-o", "gitea.exe", "\"${workspaceRoot}\\main.go\""] - }, - "problemMatcher": ["$go"] - } - ] -} diff --git a/contrib/init/centos/gitea b/contrib/init/centos/gitea deleted file mode 100644 index 4f0d0d99b2..0000000000 --- a/contrib/init/centos/gitea +++ /dev/null @@ -1,93 +0,0 @@ -#!/bin/sh -# -# /etc/rc.d/init.d/gitea -# -# Runs the Gitea Git with a cup of tea. -# -# -# chkconfig: - 85 15 -# - -### BEGIN INIT INFO -# Provides: gitea -# Required-Start: $remote_fs $syslog -# Required-Stop: $remote_fs $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Start gitea at boot time. -# Description: Control gitea. -### END INIT INFO - -# Source function library. -. /etc/init.d/functions - -# Default values - -NAME=gitea -GITEA_HOME=/var/lib/${NAME} -GITEA_PATH=/usr/local/bin/${NAME} -GITEA_USER=git -SERVICENAME="Gitea - Git with a cup of tea" -LOCKFILE=/var/lock/subsys/gitea -LOGPATH=${GITEA_HOME}/log -LOGFILE=${LOGPATH}/gitea.log -RETVAL=0 - -# Read configuration from /etc/sysconfig/gitea to override defaults -[ -r /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME - -# Don't do anything if nothing is installed -[ -x ${GITEA_PATH} ] || exit 0 -# exit if logpath dir is not created. -[ -x ${LOGPATH} ] || exit 0 - -DAEMON_OPTS="--check $NAME" - -# Set additional options, if any -[ ! -z "$GITEA_USER" ] && DAEMON_OPTS="$DAEMON_OPTS --user=${GITEA_USER}" - -start() { - cd ${GITEA_HOME} - echo -n "Starting ${SERVICENAME}: " - daemon $DAEMON_OPTS "${GITEA_PATH} web -c /etc/${NAME}/app.ini > ${LOGFILE} 2>&1 &" - RETVAL=$? - echo - [ $RETVAL = 0 ] && touch ${LOCKFILE} - - return $RETVAL -} - -stop() { - cd ${GITEA_HOME} - echo -n "Shutting down ${SERVICENAME}: " - killproc ${NAME} - RETVAL=$? - echo - [ $RETVAL = 0 ] && rm -f ${LOCKFILE} -} - -case "$1" in - start) - status ${NAME} > /dev/null 2>&1 && exit 0 - start - ;; - stop) - stop - ;; - status) - status ${NAME} - ;; - restart) - stop - start - ;; - reload) - stop - start - ;; - *) - echo "Usage: ${NAME} {start|stop|status|restart}" - exit 1 - ;; -esac -exit $RETVAL diff --git a/contrib/init/debian/gitea b/contrib/init/debian/gitea deleted file mode 100644 index 2246309440..0000000000 --- a/contrib/init/debian/gitea +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/sh -### BEGIN INIT INFO -# Provides: gitea -# Required-Start: $syslog $network -# Required-Stop: $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: A self-hosted Git service written in Go. -# Description: A self-hosted Git service written in Go. -### END INIT INFO - -# Author: Danny Boisvert - -# Do NOT "set -e" - -# PATH should only include /usr/* if it runs after the mountnfs.sh script -PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin -DESC="Gitea - Git with a cup of tea" -NAME=gitea -SERVICEVERBOSE=yes -PIDFILE=/run/$NAME.pid -SCRIPTNAME=/etc/init.d/$NAME -WORKINGDIR=/var/lib/$NAME -DAEMON=/usr/local/bin/$NAME -DAEMON_ARGS="web -c /etc/$NAME/app.ini" -USER=git -USERBIND="" -# If you want to bind Gitea to a port below 1024 uncomment -# the line below -#USERBIND="setcap cap_net_bind_service=+ep" -STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/1/KILL/5}" - -# Read configuration variable file if it is present -[ -r /etc/default/$NAME ] && . /etc/default/$NAME - -# Exit if the package is not installed -[ -x "$DAEMON" ] || exit 0 - -do_start() -{ - $USERBIND $DAEMON - sh -c "USER=$USER HOME=/home/$USER GITEA_WORK_DIR=$WORKINGDIR start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile \\ - --background --chdir $WORKINGDIR --chuid $USER \\ - --exec $DAEMON -- $DAEMON_ARGS" -} - -do_stop() -{ - start-stop-daemon --stop --quiet --retry=$STOP_SCHEDULE --pidfile $PIDFILE --name $NAME --oknodo - rm -f $PIDFILE -} - -do_status() -{ - if [ -f $PIDFILE ]; then - if kill -0 $(cat "$PIDFILE"); then - echo "$NAME is running, PID is $(cat $PIDFILE)" - else - echo "$NAME process is dead, but pidfile exists" - fi - else - echo "$NAME is not running" - fi -} - -case "$1" in - start) - echo "Starting $DESC" "$NAME" - do_start - ;; - stop) - echo "Stopping $DESC" "$NAME" - do_stop - ;; - status) - do_status - ;; - restart) - echo "Restarting $DESC" "$NAME" - do_stop - do_start - ;; - *) - echo "Usage: $SCRIPTNAME {start|stop|status|restart}" >&2 - exit 2 - ;; -esac - -exit 0 diff --git a/contrib/init/suse/gitea b/contrib/init/suse/gitea deleted file mode 100644 index 6391bedaf8..0000000000 --- a/contrib/init/suse/gitea +++ /dev/null @@ -1,115 +0,0 @@ -#!/bin/sh -# -# /etc/init.d/gitea -# -# Runs the Gitea Git with a cup of tea. -# - -### BEGIN INIT INFO -# Provides: gitea -# Required-Start: $remote_fs -# Required-Stop: $remote_fs -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Start gitea at boot time. -# Description: Control gitea. -### END INIT INFO - -# Default values - -NAME=gitea -GITEA_HOME=/var/lib/$NAME -GITEA_PATH=/usr/local/bin/$NAME -GITEA_USER=git -SERVICENAME="Gitea - Git with a cup of tea" -LOCKFILE=/var/lock/subsys/gitea -LOGPATH=${GITEA_HOME}/log -LOGFILE=${LOGPATH}/error.log -# gitea creates its own gitea.log from stdout -RETVAL=0 - -# Read configuration from /etc/sysconfig/gitea to override defaults -[ -r /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME - -# Don't do anything if nothing is installed -test -x ${GITEA_PATH} || { echo "$NAME not installed"; - if [ "$1" = "stop" ]; then exit 0; - else exit 5; fi; } - -# exit if logpath dir is not created. -test -r ${LOGPATH} || { echo "$LOGPATH not existing"; - if [ "$1" = "stop" ]; then exit 0; - else exit 6; fi; } - -# Source function library. -. /etc/rc.status - -# Reset status of this service -rc_reset - - -case "$1" in - start) - echo -n "Starting ${SERVICENAME} " - - # As we can't use startproc, we have to check ourselves if the service is already running - /sbin/checkproc ${GITEA_PATH} - if [ $? -eq 0 ]; then - # return skipped as service is already running - (exit 5) - else - su - ${GITEA_USER} -c "USER=${GITEA_USER} GITEA_WORK_DIR=${GITEA_HOME} ${GITEA_PATH} web -c /etc/${NAME}/app.ini 2>&1 >>${LOGFILE} &" - fi - - # Remember status and be verbose - rc_status -v - ;; - - stop) - echo -n "Shutting down ${SERVICENAME} " - - ## Stop daemon with killproc(8) and if this fails - ## killproc sets the return value according to LSB. - /sbin/killproc ${GITEA_PATH} - - # Remember status and be verbose - rc_status -v - ;; - - restart) - ## Stop the service and regardless of whether it was - ## running or not, start it again. - $0 stop - $0 start - - # Remember status and be quiet - rc_status - ;; - - status) - echo -n "Checking for ${SERVICENAME} " - ## Check status with checkproc(8), if process is running - ## checkproc will return with exit status 0. - - # Return value is slightly different for the status command: - # 0 - service up and running - # 1 - service dead, but /run/ pid file exists - # 2 - service dead, but /var/lock/ lock file exists - # 3 - service not running (unused) - # 4 - service status unknown :-( - # 5--199 reserved (5--99 LSB, 100--149 distro, 150--199 appl.) - - # NOTE: checkproc returns LSB compliant status values. - /sbin/checkproc ${GITEA_PATH} - # NOTE: rc_status knows that we called this init script with - # "status" option and adapts its messages accordingly. - rc_status -v - ;; - - *) - echo "Usage: $0 {start|stop|status|restart}" - exit 1 - ;; - -esac -rc_exit diff --git a/contrib/legal/privacy.html.sample b/contrib/sample-page/privacy.html.sample similarity index 100% rename from contrib/legal/privacy.html.sample rename to contrib/sample-page/privacy.html.sample diff --git a/contrib/legal/tos.html.sample b/contrib/sample-page/tos.html.sample similarity index 100% rename from contrib/legal/tos.html.sample rename to contrib/sample-page/tos.html.sample diff --git a/contrib/init/freebsd/gitea b/contrib/service/freebsd/gitea similarity index 100% rename from contrib/init/freebsd/gitea rename to contrib/service/freebsd/gitea diff --git a/contrib/init/gentoo/gitea b/contrib/service/gentoo/gitea similarity index 100% rename from contrib/init/gentoo/gitea rename to contrib/service/gentoo/gitea diff --git a/contrib/launchd/io.gitea.web.plist b/contrib/service/launchd/io.gitea.web.plist similarity index 100% rename from contrib/launchd/io.gitea.web.plist rename to contrib/service/launchd/io.gitea.web.plist diff --git a/contrib/init/openbsd/gitea b/contrib/service/openbsd/gitea similarity index 100% rename from contrib/init/openbsd/gitea rename to contrib/service/openbsd/gitea diff --git a/contrib/init/openwrt/gitea b/contrib/service/openwrt/gitea similarity index 100% rename from contrib/init/openwrt/gitea rename to contrib/service/openwrt/gitea diff --git a/contrib/init/sunos/gitea.xml b/contrib/service/sunos/gitea.xml similarity index 100% rename from contrib/init/sunos/gitea.xml rename to contrib/service/sunos/gitea.xml diff --git a/contrib/supervisor/gitea b/contrib/service/supervisor/gitea similarity index 100% rename from contrib/supervisor/gitea rename to contrib/service/supervisor/gitea diff --git a/contrib/systemd/gitea.service b/contrib/service/systemd/gitea.service similarity index 100% rename from contrib/systemd/gitea.service rename to contrib/service/systemd/gitea.service diff --git a/contrib/init/ubuntu/gitea b/contrib/service/sysvinit/gitea similarity index 90% rename from contrib/init/ubuntu/gitea rename to contrib/service/sysvinit/gitea index da56b6e4a9..1508da7f84 100644 --- a/contrib/init/ubuntu/gitea +++ b/contrib/service/sysvinit/gitea @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash ### BEGIN INIT INFO # Provides: gitea # Required-Start: $syslog $network @@ -9,6 +9,8 @@ # Description: A self-hosted Git service written in Go. ### END INIT INFO +# This is a System V Init (init.d) startup script for legacy Linux distributions + # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script @@ -24,6 +26,9 @@ DAEMON_ARGS="web -c /etc/$NAME/app.ini" USER=git STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/1/KILL/5}" +# If you want to bind Gitea to a port below 1024, apply "setcap" to the gitea binary +#setcap cap_net_bind_service=+ep $DAEMON" + # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME diff --git a/contrib/update_dependencies.sh b/contrib/update_dependencies.sh deleted file mode 100755 index 5700a19897..0000000000 --- a/contrib/update_dependencies.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -grep 'git' go.mod | grep '\.com' | grep -v indirect | grep -v replace | cut -f 2 | cut -d ' ' -f 1 | while read line; do - go get -u "$line" - make vendor - git add . - git commit -m "update $line" -done diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index b752a81ca9..2498050f5d 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -41,10 +41,10 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; App name that shows in every page title -APP_NAME = ; Gitea: Git with a cup of tea +;APP_NAME = Gitea: Git with a cup of tea ;; ;; RUN_USER will automatically detect the current user - but you can set it here change it if you run locally -RUN_USER = ; git +;RUN_USER = ;; ;; Application run mode, affects performance and debugging: "dev" or "prod", default is "prod" ;; Mode "dev" makes Gitea easier to develop and debug, values other than "dev" are treated as "prod" which is for production use. @@ -175,14 +175,15 @@ RUN_USER = ; git ;; The port number the builtin SSH server should listen on, defaults to SSH_PORT ;SSH_LISTEN_PORT = ;; -;; Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'. +;; Root path of SSH user directory for the system's standalone SSH server if Gitea is not using its builtin SSH server. +;; Default is the '.ssh' directory in the run user's home directory. ;SSH_ROOT_PATH = ;; -;; Gitea will create a authorized_keys file by default when it is not using the internal ssh server +;; Gitea will create an authorized_keys file by default when it is not using the builtin SSH server ;; If you intend to use the AuthorizedKeysCommand functionality then you should turn this off. ;SSH_CREATE_AUTHORIZED_KEYS_FILE = true ;; -;; Gitea will create a authorized_principals file by default when it is not using the internal ssh server +;; Gitea will create an authorized_principals file by default when it is not using the builtin SSH server ;; If you intend to use the AuthorizedPrincipalsCommand functionality then you should turn this off. ;SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE = true ;; @@ -382,7 +383,7 @@ USER = root ;; ;DB_TYPE = sqlite3 ;PATH= ; defaults to data/gitea.db -;SQLITE_TIMEOUT = ; Query timeout defaults to: 500 +;SQLITE_TIMEOUT = ; Query timeout in milliseconds, defaults to: 20000 ;SQLITE_JOURNAL_MODE = ; defaults to sqlite database default (often DELETE), can be used to enable WAL mode. https://www.sqlite.org/pragma.html#pragma_journal_mode ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -460,6 +461,11 @@ INTERNAL_TOKEN = ;; Name of cookie used to store authentication information. ;COOKIE_REMEMBER_NAME = gitea_incredible ;; +;; URL or path that Gitea should redirect users to *after* performing its own logout. +;; Use this, if needed, when authentication is handled by a reverse proxy or SSO. +;; For example: "/my-sso/logout?return=/my-sso/home" +;REVERSE_PROXY_LOGOUT_REDIRECT = +;; ;; Reverse proxy authentication header name of user name, email, and full name ;REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER ;REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL @@ -519,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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -586,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 = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1155,6 +1169,11 @@ LEVEL = Info ;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo. ;RETARGET_CHILDREN_ON_MERGE = true ;; +;; Default source for the pull request title when opening a new PR. +;; "first-commit" uses the oldest commit's summary. +;; "auto" uses commit's summary if the PR only has one commit, normalizes the branch name if multiple commits. +;DEFAULT_TITLE_SOURCE = first-commit +;; ;; Delay mergeable check until page view or API access, for pull requests that have not been updated in the specified days when their base branches get updated. ;; Use "-1" to always check all pull requests (old behavior). Use "0" to always delay the checks. ;DELAY_CHECK_FOR_INACTIVE_DAYS = 7 @@ -1178,16 +1197,16 @@ LEVEL = Info ;[repository.release] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. +;; Comma-separated list of allowed release attachment file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. ;ALLOWED_TYPES = ;; ;; Number of releases that are displayed on release page ;DEFAULT_PAGING_NUM = 10 ;; -;; Max size of each file in megabytes. Defaults to 2GB +;; Max size of each release attachment file in megabytes. Defaults to 2GB ;FILE_MAX_SIZE = 2048 ;; -;; Max number of files per upload. Defaults to 5 +;; Max number of release attachment files per upload. Defaults to 5 ;MAX_FILES = 5 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1505,7 +1524,7 @@ LEVEL = Info ;; Issue Indexer settings ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;; Issue indexer type, currently support: bleve, db, elasticsearch or meilisearch default is bleve +;; Issue indexer type, currently support: bleve, db, elasticsearch (also compatible with OpenSearch) or meilisearch default is bleve ;ISSUE_INDEXER_TYPE = bleve ;; ;; Issue indexer storage path, available when ISSUE_INDEXER_TYPE is bleve @@ -1532,7 +1551,7 @@ LEVEL = Info ;; If empty then it defaults to `sources` only, as if you'd like to disable fully please see REPO_INDEXER_ENABLED. ;REPO_INDEXER_REPO_TYPES = sources,forks,mirrors,templates ;; -;; Code search engine type, could be `bleve` or `elasticsearch`. +;; Code search engine type, could be `bleve` or `elasticsearch` (also compatible with OpenSearch). ;REPO_INDEXER_TYPE = bleve ;; ;; Index file used for code search. available when `REPO_INDEXER_TYPE` is bleve @@ -1994,16 +2013,18 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;; Whether issue and pull request attachments are enabled. Defaults to `true` +;; Whether issue, pull-request and release attachments are enabled. Defaults to `true` +;; ALLOWED_TYPES/MAX_SIZE/MAX_FILES in this section only affect issue and pull-request attachments, not release attachments. +;; Release attachment has its own config options in [repository.release] section. ;ENABLED = true ;; -;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. +;; Comma-separated list of allowed issue/pull-request attachment file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. ;ALLOWED_TYPES = .avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip ;; -;; Max size of each file. Defaults to 100MB +;; Max size of each issue/pull-request attachment file. Defaults to 100MB ;MAX_SIZE = 100 ;; -;; Max number of files per upload. Defaults to 5 +;; Max number of issue/pull-request attachment files per upload. Defaults to 5 ;MAX_FILES = 5 ;; ;; Storage type for attachments, `local` for local disk or `minio` for s3 compatible @@ -2276,6 +2297,22 @@ LEVEL = Info ;; Unreferenced blobs created more than OLDER_THAN ago are subject to deletion ;OLDER_THAN = 24h +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Synchronize repository licenses +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.sync_repo_licenses] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Whether to enable the job +;ENABLED = false +;; Whether to always run at least once at start up time (if ENABLED) +;RUN_AT_START = false +;; Whether to emit notice on successful execution too +;NOTICE_ON_SUCCESS = false +;; Time interval for job to run +;SCHEDULE = @annually + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2337,6 +2374,18 @@ LEVEL = Info ;NOTICE_ON_SUCCESS = false ;SCHEDULE = @every 72h +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Update the '.ssh/authorized_principals' file +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.resync_all_sshprincipals] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;ENABLED = false +;RUN_AT_START = false +;NOTICE_ON_SUCCESS = false +;SCHEDULE = @every 72h + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Resynchronize git hooks of all repositories (pre-receive, update, post-receive, proc-receive, ...) @@ -2445,6 +2494,70 @@ LEVEL = Info ;Check at least this proportion of LFSMetaObjects per repo. (This may cause all stale LFSMetaObjects to be checked.) ;PROPORTION_TO_CHECK_PER_REPO = 0.6 +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Rebuild issue index +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.rebuild_issue_indexer] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;ENABLED = false +;RUN_AT_START = false +;NO_SUCCESS_NOTICE = false +;SCHEDULE = @annually + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Actions cron tasks +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Stop running tasks which haven't been updated for a long time +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.stop_zombie_tasks] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;ENABLED = true +;RUN_AT_START = true +;NO_SUCCESS_NOTICE = false +;SCHEDULE = @every 5m + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Stop running tasks which have running status and continuous updates but don't end for a long time +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.stop_endless_tasks] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;ENABLED = true +;RUN_AT_START = true +;NO_SUCCESS_NOTICE = false +;SCHEDULE = @every 30m + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Cancel jobs which haven't been picked up for a long time +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.cancel_abandoned_jobs] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;ENABLED = true +;RUN_AT_START = false +;NO_SUCCESS_NOTICE = false +;SCHEDULE = @every 6h + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Start cron based actions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.start_schedule_tasks] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;ENABLED = true +;RUN_AT_START = false +;NO_SUCCESS_NOTICE = false +;SCHEDULE = @every 1m + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[mirror] @@ -2698,6 +2811,8 @@ LEVEL = Info ;LIMIT_SIZE_SWIFT = -1 ;; Maximum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) ;LIMIT_SIZE_VAGRANT = -1 +;; Maximum size of a Terraform state upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) +;LIMIT_SIZE_TERRAFORM_STATE = -1 ;; Enable RPM re-signing by default. (It will overwrite the old signature ,using v4 format, not compatible with CentOS 6 or older) ;DEFAULT_RPM_SIGN_ENABLED = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2866,6 +2981,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/docker/root/etc/s6/openssh/finish b/docker/root/etc/s6/openssh/finish index 06bd986563..8e89b35c51 100755 --- a/docker/root/etc/s6/openssh/finish +++ b/docker/root/etc/s6/openssh/finish @@ -1,2 +1,8 @@ #!/bin/bash +# $1 = exit code of the run script, $2 = signal +if [ "$1" -ne 0 ]; then + # avoid immediately restarting the sshd service, which may cause CPU 100% if the error (permission, configuration) is not fixed + echo "openssh failed with exit code $1 - waiting a short delay before attempting a restart" + sleep 3 +fi exit 0 diff --git a/docs/community-governance.md b/docs/community-governance.md new file mode 100644 index 0000000000..dbf2481329 --- /dev/null +++ b/docs/community-governance.md @@ -0,0 +1,198 @@ +# Community governance and review process + +This document describes maintainer expectations, project governance, and the detailed pull request review workflow (labels, merge queue, commit message format for mergers). For what contributors should do when opening and updating a PR, see [CONTRIBUTING.md](../CONTRIBUTING.md). + +## Code review + +### Milestone + +A PR should only be assigned to a milestone if it will likely be merged into the given version. \ +PRs without a milestone may not be merged. + +### Labels + +Almost all labels used inside Gitea can be classified as one of the following: + +- `modifies/…`: Determines which parts of the codebase are affected. These labels will be set through the CI. +- `topic/…`: Determines the conceptual component of Gitea that is affected, i.e. issues, projects, or authentication. At best, PRs should only target one component but there might be overlap. Must be set manually. +- `type/…`: Determines the type of an issue or PR (feature, refactoring, docs, bug, …). If GitHub supported scoped labels, these labels would be exclusive, so you should set **exactly** one, not more or less (every PR should fall into one of the provided categories, and only one). +- `issue/…` / `lgtm/…`: Labels that are specific to issues or PRs respectively and that are only necessary in a given context, i.e. `issue/not-a-bug` or `lgtm/need 2` + +Every PR should be labeled correctly with every label that applies. + +There are also some labels that will be managed automatically.\ +In particular, these are + +- the amount of pending required approvals +- has all `backport`s or needs a manual backport + +### Reviewing PRs + +Maintainers are encouraged to review pull requests in areas where they have expertise or particular interest. + +#### For reviewers + +- **Verification**: Verify that the PR accurately reflects the changes, and verify that the tests and documentation are complete and aligned with the implementation. +- **Actionable feedback**: Say what should change and why, and distinguish required changes from optional suggestions. +- **Feedback**: Focus feedback on the issue itself and avoid comments about the contributor's abilities. +- **Request changes**: If you request changes (i.e., block a PR), give a clear rationale and, whenever possible, a concrete path to resolution. +- **Approval**: Only approve a PR when you are fully satisfied with its current state - "rubber-stamp" approvals need to be highlighted as such. + +### Getting PRs merged + +Changes to Gitea must be reviewed before they are accepted, including changes from owners and maintainers. The exception is critical bugs that prevent Gitea from compiling or starting. + +We require two maintainer approvals for every PR. When that is satisfied, your PR gets the `lgtm/done` label. After that, you mainly fix merge conflicts and respond to or implement maintainer requests; maintainers drive getting the PR merged. + +If a PR has `lgtm/done`, no open discussions, and no merge conflicts, any maintainer may add `reviewed/wait-merge`. That puts the PR in the merge queue. PRs are merged from the queue in the order of this list: + + + +Gitea uses its own tool, , to automate parts of the review process. The backporter: + +- Creates a backport PR when needed after the initial PR merges. +- Removes the PR from the merge queue after it merges. +- Keeps the oldest branch in the merge queue up to date with merges. + +### Final call + +If a PR has been ignored for more than 7 days with no comments or reviews, and the author or any maintainer believes it will not survive a long wait (such as a refactoring PR), they can send "final call" to the TOC by mentioning them in a comment. + +After another 7 days, if there is still zero approval, this is considered a polite refusal, and the PR will be closed to avoid wasting further time. Therefore, the "final call" has a cost, and should be used cautiously. + +However, if there are no objections from maintainers, the PR can be merged with only one approval from the TOC (not the author). + +### Commit messages + +Mergers are required to rewrite the PR title and the first comment (the summary) when necessary so the squash commit message is clear. + +The final commit message should not hedge: replace phrases like `hopefully, won't happen anymore` with definite wording. + +#### PR Co-authors + +A person counts as a PR co-author once they (co-)authored a commit that is not simply a `Merge base branch into branch` commit. Mergers must remove such false-positive co-authors when writing the squash message. Every true co-author must remain in the commit message. + +#### PRs targeting `main` + +The commit message of PRs targeting `main` is always + +```bash +$PR_TITLE ($PR_INDEX) + +$REWRITTEN_PR_SUMMARY +``` + +#### Backport PRs + +The commit message of backport PRs is always + +```bash +$PR_TITLE ($INITIAL_PR_INDEX) ($BACKPORT_PR_INDEX) + +$REWRITTEN_PR_SUMMARY +``` + +## Maintainers + +We list [maintainers](../MAINTAINERS) so every PR gets proper review. + +#### Review expectations + +Every PR **must** be reviewed by at least two maintainers (or owners) before merge. **Exception:** after one week, refactoring PRs and documentation-only PRs need only one maintainer approval. + +Maintainers are expected to spend time on code reviews. + +#### Becoming a maintainer + +A maintainer should already be a Gitea contributor with at least four merged PRs. To apply, use the [Discord](https://discord.gg/Gitea) `#develop` channel. Maintainer teams may also invite contributors. + +#### Stepping down, advisors, and inactivity + +If you cannot keep reviewing, apply to leave the maintainers team. You can join the [advisors team](https://github.com/orgs/go-gitea/teams/advisors); advisors who want to review again are welcome back as maintainers. + +If a maintainer is inactive for more than three months and has not left the team, owners may move them to the advisors team. + +#### Account security + +For security, maintainers should enable 2FA and sign commits with GPG when possible: + +- [Two-factor authentication](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication) +- [Signing commits with GPG](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) + +Any account with write access (including bots and TOC members) **must** use [2FA](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication). + +## Technical Oversight Committee (TOC) + +At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company. +https://blog.gitea.com/quarterly-23q1/ + +### TOC election process + +Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company. +A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election. +If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate. + +The TOC is elected for one year, the TOC election happens yearly. +After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat. +If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat. +Refusals result in the person with the next highest vote getting the same choice. +As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat. + +If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration. + +### Current TOC members + +- 2024-01-01 ~ 2024-12-31 + - Company + - [Jason Song](https://gitea.com/wolfogre) + - [Lunny Xiao](https://gitea.com/lunny) + - [Matti Ranta](https://gitea.com/techknowlogick) + - Community + - [6543](https://gitea.com/6543) <6543@obermui.de> + - [delvh](https://gitea.com/delvh) + - [John Olheiser](https://gitea.com/jolheiser) + +### Previous TOC/owners members + +Here's the history of the owners and the time they served: + +- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 +- [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017 +- [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017 +- [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801) +- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 +- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 +- [6543](https://gitea.com/6543) - 2023 +- [John Olheiser](https://gitea.com/jolheiser) - 2023 +- [Jason Song](https://gitea.com/wolfogre) - 2023 + +## Governance Compensation + +Each member of the community elected TOC will be granted $500 each month as compensation for their work. + +Furthermore, any community release manager for a specific release or LTS will be compensated $500 for the delivery of said release. + +These funds will come from community sources like the OpenCollective rather than directly from the company. +Only non-company members are eligible for this compensation, and if a member of the community TOC takes the responsibility of release manager, they would only be compensated for their TOC duties. +Gitea Ltd employees are not eligible to receive any funds from the OpenCollective unless it is reimbursement for a purchase made for the Gitea project itself. + +## TOC & Working groups + +With Gitea covering many projects outside of the main repository, several groups will be created to help focus on specific areas instead of requiring maintainers to be a jack-of-all-trades. Maintainers are of course more than welcome to be part of multiple groups should they wish to contribute in multiple places. + +The currently proposed groups are: + +- **Core Group**: maintain the primary Gitea repository +- **Integration Group**: maintain the Gitea ecosystem's related tools, including go-sdk/tea/changelog/bots etc. +- **Documentation Group**: maintain related documents and repositories +- **Translation Group**: coordinate with translators and maintain translations +- **Security Group**: managed by TOC directly, members are decided by TOC, maintains security patches/responsible for security items + +## Roadmap + +Each year a roadmap will be discussed with the entire Gitea maintainers team, and feedback will be solicited from various stakeholders. +TOC members need to review the roadmap every year and work together on the direction of the project. + +When a vote is required for a proposal or other change, the vote of community elected TOC members count slightly more than the vote of company elected TOC members. With this approach, we both avoid ties and ensure that changes align with the mission statement and community opinion. + +You can visit our roadmap on the wiki. diff --git a/docs/guideline-backend.md b/docs/guideline-backend.md new file mode 100644 index 0000000000..bc3e71113f --- /dev/null +++ b/docs/guideline-backend.md @@ -0,0 +1,58 @@ +# Backend development + +This document covers backend-specific contribution expectations. For general contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md). + +For coding style and architecture, see also the [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend) on the documentation site. + +## Dependencies + +Go dependencies are managed using [Go Modules](https://go.dev/cmd/go/#hdr-Module_maintenance). \ +You can find more details in the [go mod documentation](https://go.dev/ref/mod) and the [Go Modules Wiki](https://github.com/golang/go/wiki/Modules). + +Pull requests should only modify `go.mod` and `go.sum` where it is related to your change, be it a bugfix or a new feature. \ +Apart from that, these files should only be modified by Pull Requests whose only purpose is to update dependencies. + +The `go.mod`, `go.sum` update needs to be justified as part of the PR description, +and must be verified by the reviewers and/or merger to always reference +an existing upstream commit. + +## API v1 + +The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest). + +### GitHub API compatibility + +Gitea's API should use the same endpoints and fields as the GitHub API as far as possible, unless there are good reasons to deviate. \ +If Gitea provides functionality that GitHub does not, a new endpoint can be created. \ +If information is provided by Gitea that is not provided by the GitHub API, a new field can be used that doesn't collide with any GitHub fields. \ +Updating an existing API should not remove existing fields unless there is a really good reason to do so. \ +The same applies to status responses. If you notice a problem, feel free to leave a comment in the code for future refactoring to API v2 (which is currently not planned). + +### Adding/Maintaining API routes + +All expected results (errors, success, fail messages) must be documented ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L319-L327)). \ +All JSON input types must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L76-L91)) \ +and referenced in [routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go). \ +They can then be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L318). \ +All JSON responses must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L36-L68)) \ +and referenced in its category in [routers/api/v1/swagger/](routers/api/v1/swagger/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16)) \ +They can be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279). + +### When to use what HTTP method + +In general, HTTP methods are chosen as follows: + +- **GET** endpoints return the requested object(s) and status **OK (200)** +- **DELETE** endpoints return the status **No Content (204)** and no content either +- **POST** endpoints are used to **create** new objects (e.g. a User) and return the status **Created (201)** and the created object +- **PUT** endpoints are used to **add/assign** existing Objects (e.g. a user to a team) and return the status **No Content (204)** and no content either +- **PATCH** endpoints are used to **edit/change** an existing object and return the changed object and the status **OK (200)** + +### Requirements for API routes + +All parameters of endpoints changing/editing an object must be optional (except the ones to identify the object, which are required). + +Endpoints returning lists must + +- support pagination (`page` & `limit` options in query) +- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444)) diff --git a/docs/guideline-frontend.md b/docs/guideline-frontend.md new file mode 100644 index 0000000000..80ebe82177 --- /dev/null +++ b/docs/guideline-frontend.md @@ -0,0 +1,17 @@ +# Frontend development + +This document covers frontend-specific contribution expectations. For general contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md). + +## Dependencies + +For the frontend, we use [npm](https://www.npmjs.com/). + +The same restrictions apply for frontend dependencies as for [backend dependencies](guideline-backend.md#dependencies), with the exceptions that the files for it are `package.json` and `package-lock.json`, and that new versions must always reference an existing version. + +## Design guideline + +Depending on your change, please read the + +- [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend) +- [frontend development guideline](https://docs.gitea.com/contributing/guidelines-frontend) +- [refactoring guideline](https://docs.gitea.com/contributing/guidelines-refactoring) diff --git a/docs/release-management.md b/docs/release-management.md new file mode 100644 index 0000000000..be8d9e1abf --- /dev/null +++ b/docs/release-management.md @@ -0,0 +1,115 @@ +# Release management + +This document describes the release cycle, backports, versioning, and the release manager checklist. For everyday contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md). + +## Backports and Frontports + +### What is backported? + +We backport PRs given the following circumstances: + +1. Feature freeze is active, but `-rc0` has not been released yet. Here, we backport as much as possible. +2. `rc0` has been released. Here, we only backport bug- and security-fixes, and small enhancements. Large PRs such as refactors are not backported anymore. +3. We never backport new features. +4. We never backport breaking changes except when + 1. The breaking change has no effect on the vast majority of users + 2. The component triggering the breaking change is marked as experimental + +### How to backport? + +In the past, it was necessary to manually backport your PRs. \ +Now, that's not a requirement anymore as our [backport bot](https://github.com/GiteaBot) tries to create backports automatically once the PR is merged when the PR + +- does not have the label `backport/manual` +- has the label `backport/` + +The `backport/manual` label signifies either that you want to backport the change yourself, or that there were conflicts when backporting, thus you **must** do it yourself. + +### Format of backport PRs + +The title of backport PRs should be + +``` + (#) +``` + +The first two lines of the summary of the backporting PR should be + +``` +Backport # + +``` + +with the rest of the summary and labels matching the original PR. + +### Frontports + +Frontports behave exactly as described above for backports. + +## Release Cycle + +We use a release schedule so work, stabilization, and releases stay predictable. + +### Cadence + +- Aim for a major release about every three or four months. +- Roughly two or three months of general development, then about one month of testing and polish called the **release freeze**. +- *Starting with v1.26 the release cycle will be more predictable and follow a more regular schedule.* + +### Release schedule + +We will try to publish a new major version every three months: + +- v1.26.0 in April 2026 +- v1.27.0 in June 2026 +- v1.28.0 in September 2026 +- v1.29.0 in December 2026 + +#### How is the release handled? +- The release manager will tag the release candidate (e.g. `v1.26.0-rc0`) and publish it for testing in the **first week of the release month**. +- If there are no major issues, the release manager will check with the other maintainers and then tag the final release (e.g. `v1.26.0`) in the **one or two weeks following the release candidate**. + +### Feature freeze + +- Merge feature PRs before the freeze when you can. +- Feature PRs still open at the freeze move to the next milestone. Watch Discord for the freeze announcement. +- During the freeze, a **release branch** takes fixes backported from `main`. Release candidates ship for testing; the final release for that line is maintained from that branch. + +### Patch releases + +During a cycle we may ship patch releases for an older line. For example, if the latest release is v1.2, we can still publish v1.1.1 after v1.1.0. + +### End of life (EOL) + +We support per standard the last major release. For example, if the latest release is v1.26, we support v1.26 and v1.25, but not v1.24 anymore. We will only publish security fixes for the last major release, so if you are using an older release, please upgrade to a supported release as soon as possible. +Also we always try to support the latest on main branch, so if you are using the latest on main, you should be fine. + +## Versions + +Gitea has the `main` branch as a tip branch and has version branches +such as `release/v1.19`. `release/v1.19` is a release branch and we will +tag `v1.19.0` for binary download. If `v1.19.0` has bugs, we will accept +pull requests on the `release/v1.19` branch and publish a `v1.19.1` tag, +after bringing the bug fix also to the main branch. + +Since the `main` branch is a tip version, if you wish to use Gitea +in production, please download the latest release tag version. All the +branches will be protected via GitHub, all the PRs to every branch must +be reviewed by two maintainers and must pass the automatic tests. + +## Releasing Gitea + +- Let MAJOR, MINOR and PATCH be Major, Minor and Patch version numbers, PATCH should be rc1, rc2, 0, 1, ...... MAJOR.MINOR will be kept the same as milestones on github or gitea in future. +- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours. +- If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps: + - Create `-dev` tag as `git tag -s -F release.notes vMAJOR.MINOR.0-dev` and push the tag as `git push origin vMAJOR.MINOR.0-dev`. + - When CI has finished building tag then you have to create a new branch named `release/vMAJOR.MINOR` +- If it is bugfix version create PR for changelog on branch `release/vMAJOR.MINOR` and wait till it is reviewed and merged. +- Add a tag as `git tag -s -F release.notes vMAJOR.MINOR.PATCH`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`. +- And then push the tag as `git push origin vMAJOR.MINOR.$`. CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.) +- If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version. +- Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release. +- Verify all release assets were correctly published through CI on dl.gitea.com and GitHub releases. Once ACKed: + - bump the version of https://dl.gitea.com/gitea/version.json + - merge the blog post PR + - announce the release in discord `#announcements` diff --git a/eslint.config.ts b/eslint.config.ts index 9f98adf859..91adc06e19 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -329,7 +329,7 @@ export default defineConfig([ 'github/no-innerText': [2], 'github/no-then': [2], 'github/no-useless-passive': [2], - 'github/prefer-observers': [2], + 'github/prefer-observers': [0], 'github/require-passive-events': [2], 'github/unescaped-html-literal': [2], 'grouped-accessor-pairs': [2], @@ -570,9 +570,9 @@ export default defineConfig([ 'no-redeclare': [0], // must be disabled for typescript overloads 'no-regex-spaces': [2], 'no-restricted-exports': [0], - 'no-restricted-globals': [2, ...restrictedGlobals], - 'no-restricted-properties': [2, ...restrictedProperties], - 'no-restricted-imports': [0], + 'no-restricted-imports': [2, {paths: [ + {name: 'jquery', message: 'Use the global $ instead', allowTypeImports: true}, + ]}], 'no-restricted-syntax': [2, 'WithStatement', 'ForInStatement', 'LabeledStatement', 'SequenceExpression'], 'no-return-assign': [0], 'no-script-url': [2], @@ -762,6 +762,7 @@ export default defineConfig([ 'unicorn/catch-error-name': [0], 'unicorn/consistent-destructuring': [2], 'unicorn/consistent-empty-array-spread': [2], + 'unicorn/consistent-template-literal-escape': [2], 'unicorn/consistent-existence-index-check': [0], 'unicorn/consistent-function-scoping': [0], 'unicorn/custom-error-definition': [0], @@ -817,6 +818,7 @@ export default defineConfig([ 'unicorn/no-unused-properties': [2], 'unicorn/no-useless-collection-argument': [2], 'unicorn/no-useless-fallback-in-spread': [2], + 'unicorn/no-useless-iterator-to-array': [2], 'unicorn/no-useless-length-check': [2], 'unicorn/no-useless-promise-resolve-reject': [2], 'unicorn/no-useless-spread': [2], @@ -866,6 +868,7 @@ export default defineConfig([ 'unicorn/prefer-response-static-json': [2], 'unicorn/prefer-set-has': [0], 'unicorn/prefer-set-size': [2], + 'unicorn/prefer-simple-condition-first': [0], 'unicorn/prefer-spread': [0], 'unicorn/prefer-string-raw': [0], 'unicorn/prefer-string-replace-all': [0], @@ -884,6 +887,7 @@ export default defineConfig([ 'unicorn/require-post-message-target-origin': [0], 'unicorn/string-content': [0], 'unicorn/switch-case-braces': [0], + 'unicorn/switch-case-break-position': [2], 'unicorn/template-indent': [2], 'unicorn/text-encoding-identifier-case': [0], 'unicorn/throw-new-error': [2], @@ -918,6 +922,7 @@ export default defineConfig([ { ...playwright.configs['flat/recommended'], files: ['tests/e2e/**/*.test.ts'], + languageOptions: {globals: {...globals.nodeBuiltin, ...globals.browser}}, rules: { ...playwright.configs['flat/recommended'].rules, 'playwright/expect-expect': [0], @@ -1009,11 +1014,15 @@ export default defineConfig([ }, }, { - files: ['*', 'tools/**/*'], + files: ['*', 'tools/**/*', 'tests/**/*'], languageOptions: {globals: globals.nodeBuiltin}, }, { files: ['web_src/**/*'], - languageOptions: {globals: {...globals.browser, ...globals.webpack}}, + languageOptions: {globals: {...globals.browser, ...globals.jquery}}, + rules: { + 'no-restricted-globals': [2, ...restrictedGlobals], + 'no-restricted-properties': [2, ...restrictedProperties], + }, }, ]); diff --git a/eslint.json.config.ts b/eslint.json.config.ts index 45696fb79c..aa6ed0e4c6 100644 --- a/eslint.json.config.ts +++ b/eslint.json.config.ts @@ -13,12 +13,18 @@ export default defineConfig([ language: 'json/json', extends: ['json/recommended'], }, + { + files: ['**/*.json5'], + plugins: {json}, + language: 'json/json5', + extends: ['json/recommended'], + }, { files: [ 'tsconfig.json', '.devcontainer/*.json', '.vscode/*.json', - 'contrib/ide/vscode/*.json', + 'contrib/development/vscode/*.json', ], plugins: {json}, language: 'json/jsonc', diff --git a/flake.lock b/flake.lock index 8c7ac0c196..839eaed572 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1773821835, - "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", + "lastModified": 1776877367, + "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", + "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 6fb3891963..ca88b581a2 100644 --- a/flake.nix +++ b/flake.nix @@ -33,9 +33,9 @@ inherit (pkgs) lib; # only bump toolchain versions here - go = pkgs.go_1_25; + go = pkgs.go_1_26; nodejs = pkgs.nodejs_24; - python3 = pkgs.python312; + python3 = pkgs.python314; pnpm = pkgs.pnpm_10; # Platform-specific dependencies @@ -83,7 +83,7 @@ GO = "${go}/bin/go"; GOROOT = "${go}/share/go"; - TAGS = "sqlite sqlite_unlock_notify"; + TAGS = ""; STATIC = "true"; } // linuxOnlyEnv; diff --git a/go.mod b/go.mod index 4ab25e2dc2..701e61bfb1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module code.gitea.io/gitea -go 1.26.1 +go 1.26.3 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // But some CAs use negative serial number, just relax the check. related: @@ -9,36 +9,37 @@ godebug x509negativeserial=1 require ( code.gitea.io/actions-proto-go v0.4.1 - code.gitea.io/sdk/gitea v0.23.2 + code.gitea.io/sdk/gitea v0.25.0 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 - connectrpc.com/connect v1.19.1 - gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed + connectrpc.com/connect v1.19.2 + gitea.com/gitea/runner v1.0.0 + gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a gitea.com/go-chi/cache v0.2.1 gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 - github.com/42wim/httpsig v1.2.3 - github.com/42wim/sshsig v0.0.0-20250502153856-5100632e8920 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 - github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 + github.com/42wim/httpsig v1.2.4 + github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 + github.com/Azure/go-ntlmssp v0.1.1 github.com/Microsoft/go-winio v0.6.2 - github.com/ProtonMail/go-crypto v1.3.0 - github.com/PuerkitoBio/goquery v1.11.0 + github.com/ProtonMail/go-crypto v1.4.1 + 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.7 - github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.8 + github.com/alecthomas/chroma/v2 v2.24.1 + github.com/aws/aws-sdk-go-v2/credentials v1.19.16 + github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.14 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb - github.com/blevesearch/bleve/v2 v2.5.7 + github.com/blevesearch/bleve/v2 v2.6.0 github.com/bohde/codel v0.2.0 github.com/buildkite/terminal-to-html/v3 v3.16.8 - github.com/caddyserver/certmagic v0.25.1 - github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21 + github.com/caddyserver/certmagic v0.25.3 + github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635 github.com/chi-middleware/proxy v1.1.1 github.com/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 @@ -46,162 +47,165 @@ require ( github.com/emirpasic/gods v1.18.1 github.com/ethantkoenig/rupture v1.0.1 github.com/felixge/fgprof v0.9.5 - github.com/fsnotify/fsnotify v1.9.0 + github.com/fsnotify/fsnotify v1.10.1 + github.com/getkin/kin-openapi v0.137.0 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 v1.37.0 - github.com/go-enry/go-enry/v2 v2.9.4 - github.com/go-git/go-billy/v5 v5.7.0 - github.com/go-git/go-git/v5 v5.16.5 - github.com/go-ldap/ldap/v3 v3.4.12 - github.com/go-redsync/redsync/v4 v4.15.0 - github.com/go-sql-driver/mysql v1.9.3 - github.com/go-webauthn/webauthn v0.13.4 - github.com/goccy/go-json v0.10.5 + github.com/go-co-op/gocron/v2 v2.21.1 + github.com/go-enry/go-enry/v2 v2.9.6 + github.com/go-git/go-billy/v5 v5.9.0 + github.com/go-git/go-git/v5 v5.19.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.10.0 + github.com/go-webauthn/webauthn v0.17.2 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 - github.com/google/go-github/v74 v74.0.0 + github.com/google/go-github/v85 v85.0.0 github.com/google/licenseclassifier/v2 v2.0.0 - github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef + github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.2.0 github.com/gorilla/sessions v1.4.0 - github.com/hashicorp/go-version v1.8.0 + github.com/hashicorp/go-version v1.9.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/huandu/xstrings v1.5.0 - github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 - github.com/jhillyerd/enmime v1.3.0 + github.com/jaytaylor/html2text v0.0.0-20260303211410-1a4bdc82ecec + github.com/jhillyerd/enmime/v2 v2.3.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/klauspost/compress v1.18.3 + github.com/klauspost/compress v1.18.6 github.com/klauspost/cpuid/v2 v2.3.0 - github.com/lib/pq v1.11.1 + github.com/lib/pq v1.12.3 github.com/markbates/goth v1.82.0 - github.com/mattn/go-isatty v0.0.20 - github.com/mattn/go-sqlite3 v1.14.33 - github.com/meilisearch/meilisearch-go v0.36.0 + github.com/mattn/go-isatty v0.0.22 + github.com/mattn/go-sqlite3 v1.14.44 + github.com/meilisearch/meilisearch-go v0.36.2 github.com/mholt/archives v0.1.5 github.com/microcosm-cc/bluemonday v1.0.27 - github.com/microsoft/go-mssqldb v1.9.6 - github.com/minio/minio-go/v7 v7.0.98 - github.com/msteinert/pam v1.2.0 - github.com/nektos/act v0.2.63 + github.com/microsoft/go-mssqldb v1.9.7 + github.com/minio/minio-go/v7 v7.1.0 + github.com/msteinert/pam/v2 v2.1.0 github.com/niklasfasching/go-org v1.9.1 - github.com/olivere/elastic/v7 v7.0.32 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 github.com/pquerna/otp v1.5.0 github.com/prometheus/client_golang v1.23.2 github.com/quasoft/websspi v1.1.2 - github.com/redis/go-redis/v9 v9.17.3 + github.com/redis/go-redis/v9 v9.19.0 github.com/robfig/cron/v3 v3.0.1 - github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/sassoftware/go-rpmutils v0.4.0 github.com/sergi/go-diff v1.4.0 github.com/stretchr/testify v1.11.1 github.com/syndtr/goleveldb v1.0.0 github.com/tstranex/u2f v1.0.0 github.com/ulikunitz/xz v0.5.15 - github.com/urfave/cli-docs/v3 v3.0.0-alpha6 - github.com/urfave/cli/v3 v3.4.1 + github.com/urfave/cli-docs/v3 v3.1.0 + github.com/urfave/cli/v3 v3.6.1 github.com/wneessen/go-mail v0.7.2 github.com/xeipuuv/gojsonschema v1.2.0 github.com/yohcop/openid-go v1.0.1 - github.com/yuin/goldmark v1.7.16 + github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc - github.com/yuin/goldmark-meta v1.1.0 - gitlab.com/gitlab-org/api/client-go v0.142.4 - go.yaml.in/yaml/v4 v4.0.0-rc.2 - golang.org/x/crypto v0.47.0 - golang.org/x/image v0.35.0 - golang.org/x/net v0.49.0 - golang.org/x/oauth2 v0.34.0 - golang.org/x/sync v0.19.0 - golang.org/x/sys v0.40.0 - golang.org/x/text v0.33.0 - google.golang.org/grpc v1.78.0 + gitlab.com/gitlab-org/api/client-go/v2 v2.24.1 + go.yaml.in/yaml/v4 v4.0.0-rc.3 + golang.org/x/crypto v0.50.0 + golang.org/x/image v0.39.0 + golang.org/x/net v0.53.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.20.0 + golang.org/x/sys v0.44.0 + golang.org/x/text v0.36.0 + google.golang.org/grpc v1.81.0 google.golang.org/protobuf v1.36.11 - gopkg.in/ini.v1 v1.67.1 + gopkg.in/ini.v1 v1.67.2 gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.50.0 mvdan.cc/xurls/v2 v2.6.0 - strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 + strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab xorm.io/builder v0.3.13 xorm.io/xorm v1.3.11 ) require ( cloud.google.com/go/compute/metadata v0.9.0 // indirect - code.gitea.io/gitea-vet v0.2.3 // indirect dario.cat/mergo v1.0.2 // indirect - filippo.io/edwards25519 v1.1.0 // indirect + filippo.io/edwards25519 v1.2.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/DataDog/zstd v1.5.7 // indirect - github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect + github.com/RoaringBitmap/roaring/v2 v2.16.0 // indirect github.com/STARRY-S/zip v0.2.3 // indirect - github.com/andybalholm/brotli v1.2.0 // indirect + 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.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect - github.com/aws/smithy-go v1.24.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect + github.com/aws/smithy-go v1.25.1 // 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.0 // indirect - github.com/blevesearch/bleve_index_api v1.2.11 // indirect - github.com/blevesearch/geo v0.2.4 // indirect - github.com/blevesearch/go-faiss v1.0.26 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect + github.com/blevesearch/bleve_index_api v1.3.11 // indirect + github.com/blevesearch/geo v0.2.5 // indirect + github.com/blevesearch/go-faiss v1.1.0 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect - github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect + github.com/blevesearch/mmap-go v1.2.0 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.4.7 // indirect github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect - github.com/blevesearch/vellum v1.1.0 // indirect - github.com/blevesearch/zapx/v11 v11.4.2 // indirect - github.com/blevesearch/zapx/v12 v12.4.2 // indirect - github.com/blevesearch/zapx/v13 v13.4.2 // indirect - github.com/blevesearch/zapx/v14 v14.4.2 // indirect - github.com/blevesearch/zapx/v15 v15.4.2 // indirect - github.com/blevesearch/zapx/v16 v16.2.8 // indirect - github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/blevesearch/vellum v1.2.0 // indirect + github.com/blevesearch/zapx/v11 v11.4.3 // indirect + github.com/blevesearch/zapx/v12 v12.4.3 // indirect + github.com/blevesearch/zapx/v13 v13.4.3 // indirect + github.com/blevesearch/zapx/v14 v14.4.3 // indirect + github.com/blevesearch/zapx/v15 v15.4.3 // indirect + github.com/blevesearch/zapx/v16 v16.3.4 // indirect + github.com/blevesearch/zapx/v17 v17.1.2 // indirect + github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/boombuler/barcode v1.1.0 // indirect github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect - github.com/caddyserver/zerossl v0.1.4 // indirect + github.com/caddyserver/zerossl v0.1.5 // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/couchbase/go-couchbase v0.1.1 // indirect - github.com/couchbase/gomemcached v0.3.3 // indirect - github.com/couchbase/goutils v0.1.2 // indirect + github.com/couchbase/gomemcached v0.3.4 // indirect + github.com/couchbase/goutils v0.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.1 // indirect github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-ini/ini v1.67.0 // indirect - github.com/go-webauthn/x v0.1.24 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/go-webauthn/x v0.2.3 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/flatbuffers v25.2.10+incompatible // indirect - github.com/google/go-querystring v1.1.0 // indirect - github.com/google/go-tpm v0.9.5 // indirect + github.com/google/flatbuffers v25.12.19+incompatible // indirect + github.com/google/go-querystring v1.2.0 // indirect + github.com/google/go-tpm v0.9.8 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect @@ -209,78 +213,86 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/inbucket/html2text v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/kevinburke/ssh_config v1.6.0 // indirect github.com/klauspost/crc32 v1.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/libdns/libdns v1.1.1 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/markbates/going v1.0.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect - github.com/mholt/acmez/v3 v3.1.4 // indirect - github.com/miekg/dns v1.1.69 // indirect + github.com/mholt/acmez/v3 v3.1.6 // indirect + github.com/miekg/dns v1.1.72 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/minlz v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/minio/minlz v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nwaples/rardecode/v2 v2.2.0 // indirect - github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00 // indirect - github.com/olekukonko/errors v1.1.0 // indirect - github.com/olekukonko/ll v0.1.0 // indirect - github.com/olekukonko/tablewriter v1.0.9 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/nwaples/rardecode/v2 v2.2.2 // indirect + github.com/oasdiff/yaml v0.0.9 // indirect + github.com/oasdiff/yaml3 v0.0.12 // indirect + github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect + github.com/olekukonko/errors v1.2.0 // indirect + github.com/olekukonko/ll v0.1.8 // indirect + github.com/olekukonko/tablewriter v1.1.4 // indirect github.com/onsi/ginkgo v1.16.5 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/philhofer/fwd v1.2.0 // indirect - github.com/pierrec/lz4/v4 v4.1.22 // indirect - github.com/pjbgf/sha1cd v0.4.0 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.17.0 // indirect - github.com/rhysd/actionlint v1.7.7 // indirect - github.com/rivo/uniseg v0.4.7 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rhysd/actionlint v1.7.12 // indirect github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.3.1 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/skeema/knownhosts v1.3.2 // indirect 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.1 // indirect + github.com/tinylib/msgp v1.6.4 // indirect github.com/unknwon/com v1.0.1 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect - github.com/zeebo/assert v1.3.0 // indirect github.com/zeebo/blake3 v0.2.4 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect go.etcd.io/bbolt v1.4.3 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.uber.org/zap/exp v0.3.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.40.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect + go4.org v0.0.0-20260112195520-a5071408f32f // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.44.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + modernc.org/libc v1.72.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) ignore ( @@ -288,16 +300,16 @@ ignore ( ./node_modules ) -replace github.com/jaytaylor/html2text => github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347 +// When doing "go get -u ./...", Golang will try to update all dependencies +// But not all latest versions of dependencies are compatible with other packages or our codebase, so we need to pin some dependencies to specific versions +// Need to regularly maintain this list to try to update them to latest versions, especially the TODO ones -replace github.com/nektos/act => gitea.com/gitea/act v0.261.8 +replace github.com/jaytaylor/html2text => github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347 // jaytaylor/html2text is unmaintained -exclude github.com/gofrs/uuid v3.2.0+incompatible +replace go.yaml.in/yaml/v4 => go.yaml.in/yaml/v4 v4.0.0-rc.3 // rc.4 changes block scalar serialization, wait for stable release -exclude github.com/gofrs/uuid v4.0.0+incompatible +replace github.com/Azure/azure-sdk-for-go/sdk/azcore => github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 // v1.21.0+ uses API version unsupported by Azurite in CI -exclude github.com/goccy/go-json v0.4.11 +replace github.com/Azure/azure-sdk-for-go/sdk/storage/azblob => github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 // v1.6.4+ uses API version unsupported by Azurite in CI -exclude github.com/satori/go.uuid v1.2.0 - -tool code.gitea.io/gitea-vet +replace github.com/microsoft/go-mssqldb => github.com/microsoft/go-mssqldb v1.9.7 // downgraded with Azure SDK diff --git a/go.sum b/go.sum index 02e6532542..ccdc062172 100644 --- a/go.sum +++ b/go.sum @@ -1,40 +1,23 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls= code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas= -code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI= -code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= -code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg= -code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= +code.gitea.io/sdk/gitea v0.25.0 h1:wSJlL0Qv+ODY2OdF0L7fwt86wgf1C/0g3xIXZ6eC5zI= +code.gitea.io/sdk/gitea v0.25.0/go.mod h1:uDFWYBU8dgZsgOHwe6C/6olxvf8FHguNB3wW1i83fgg= +code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= +code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY= codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM= -connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= -connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo= +connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -gitea.com/gitea/act v0.261.8 h1:rUWB5GOZOubfe2VteKb7XP3HRIbcW3UUmfh7bVAgQcA= -gitea.com/gitea/act v0.261.8/go.mod h1:lTp4136rwbZiZS3ZVQeHCvd4qRAZ7LYeiRBqOSdMY/4= -gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso= -gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +gitea.com/gitea/runner v1.0.0 h1:s3AS5u8r+B5W+Gy69sYYvCVTb0f23fRM6H+CbrFrobE= +gitea.com/gitea/runner v1.0.0/go.mod h1:bn+8Qt3KyvdVQHD3OR2yRoKgEXs8dXqwBVtmfYVrNIE= +gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a h1:JHoBrfuTSF9Ke9aNfSYj1XRPBHjKPgCApVprnt2Am0M= +gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a/go.mod h1:FOsLJIMdpiHzBp3Vby6Wfkdw2ppGscrjgU1IC7E4/zQ= gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g= gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q= gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo= @@ -47,10 +30,10 @@ gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHq gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= -github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= -github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= -github.com/42wim/sshsig v0.0.0-20250502153856-5100632e8920 h1:mWAVGlovzUfREJBhm0GwJnDNu21yRrL9QH9NIzAU3rg= -github.com/42wim/sshsig v0.0.0-20250502153856-5100632e8920/go.mod h1:zWxcT7BIWOe05xVJL0VMvO/PJ6RpoCux10heb77H6Q8= +github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU= +github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps= +github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 h1:3Fcz1QzlS7Jv4FT2KI3cHNSZL+KPN3dXxurn9f3YL/Y= +github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432/go.mod h1:BLWe6Nol65Xxncvaw07yYMxiyk02We1lBrbRYsMYsjE= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 h1:ci6Yd6nysBRLEodoziB6ah1+YOzZbZk+NYneoA6q+6E= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= @@ -65,12 +48,11 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuo github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 h1:FwladfywkNirM+FZYLBR2kBz5C8Tg0fw5w5Y7meRXWI= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2/go.mod h1:vv5Ad0RrIoT1lJFdWBZwt4mB1+j+V8DUroixmKDTCdk= -github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= -github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= +github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= @@ -79,14 +61,14 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347 h1:3JhDl+JysaO8nhNU1XMaw35VSGjV4IEQAefaG4Lyok4= github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347/go.mod h1:2ErI0aycD43Ufr6CFK5lT/NrHGmoZuVbn1nlPThw69o= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= -github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= -github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= +github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= +github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= +github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo= +github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I= -github.com/RoaringBitmap/roaring/v2 v2.10.0 h1:HbJ8Cs71lfCJyvmSptxeMX2PtvOC8yonlU0GQcy2Ak0= -github.com/RoaringBitmap/roaring/v2 v2.10.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= +github.com/RoaringBitmap/roaring/v2 v2.16.0 h1:Kys1UNf49d5W8Tq3bpuAhIr/Z8/yPB+59CO8A6c/BbE= +github.com/RoaringBitmap/roaring/v2 v2.16.0/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0 h1:tgjwQrDH5m6jIYB7kac5IQZmfUzQNseac/e3H4VoCNE= @@ -94,15 +76,15 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0/go.mod h1:1HmmMEVsr+0R github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= -github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM= +github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -110,49 +92,48 @@ 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.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= -github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= -github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= -github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.8 h1:KxKGfYvkVOe/U/Z4yAd0ZySRJHavuL31VOC+fn7WEAs= -github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.8/go.mod h1:cznnFD3BzYY+NB+4WoQ7SxdTACOsMqGCbQ5QaByPz4w= -github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= -github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= +github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.14 h1:3N664oayz66ttIkc8B9/OLntMWhoGhKqPudRRfsZQ20= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.14/go.mod h1:M+6j5lOmtDMjLlFMO8lfTs0gI3qBsSjM8L7F1cQF5Ng= +github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= +github.com/aws/smithy-go v1.25.1/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= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.1.10/go.mod h1:w0XsmFg8qg6cmpTtJ0z3pKgjTDBMMnI/+I2syrE6XBE= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM= -github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/blevesearch/bleve/v2 v2.0.5/go.mod h1:ZjWibgnbRX33c+vBRgla9QhPb4QOjD6fdVJ+R1Bk8LM= -github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8= -github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA= +github.com/blevesearch/bleve/v2 v2.6.0 h1:Cyd3dd4q5tCbOV8MnKUVRUDYMHOir9xn12NZzXVSEd4= +github.com/blevesearch/bleve/v2 v2.6.0/go.mod h1:gLmI8lWgHgrIYf7UpUX7JISI1CaqC6VScu46mHThuAY= github.com/blevesearch/bleve_index_api v1.0.0/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4= -github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s= -github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= -github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk= -github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= -github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI= -github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= +github.com/blevesearch/bleve_index_api v1.3.11 h1:x29vbV8OjWfLcrDVd7Lr1q+BkLNS0JWNEig0MCVnKH4= +github.com/blevesearch/bleve_index_api v1.3.11/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= +github.com/blevesearch/geo v0.2.5 h1:yJg9FX1oRwLnjXSXF+ECHfXFTF4diF02Ca/qUGVjJhE= +github.com/blevesearch/geo v0.2.5/go.mod h1:Jhq7WE2K6mJTx1xS44M2pUO6Io+wjCSHh1+co3YOgH4= +github.com/blevesearch/go-faiss v1.1.0 h1:xM7Jc0ZUCv5lssG9Ohj3Jv0SdTpxcUABU1dDt9XVsc4= +github.com/blevesearch/go-faiss v1.1.0/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA= -github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= -github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= +github.com/blevesearch/mmap-go v1.2.0 h1:l33nNKPFcBjJUMwem6sAYJPUzhUCABoK9FxZDGiFNBI= +github.com/blevesearch/mmap-go v1.2.0/go.mod h1:Vd6+20GBhEdwJnU1Xohgt88XCD/CTWcqbCNxkZpyBo0= github.com/blevesearch/scorch_segment_api/v2 v2.0.1/go.mod h1:lq7yK2jQy1yQjtjTfU931aVqz7pYxEudHaDwOt1tXfU= -github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY= -github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc= +github.com/blevesearch/scorch_segment_api/v2 v2.4.7 h1:GlMzW08hcsM3DnLUxhyF/1PcDal1qtvvIuytuph5djw= +github.com/blevesearch/scorch_segment_api/v2 v2.4.7/go.mod h1://IJ7tG3QCf0cWW/aVSXqy77tc1AvLu3fcJLYEvOAFs= github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= @@ -163,27 +144,29 @@ github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMG github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= github.com/blevesearch/vellum v1.0.3/go.mod h1:2u5ax02KeDuNWu4/C+hVQMD6uLN4txH1JbtpaDNLJRo= github.com/blevesearch/vellum v1.0.4/go.mod h1:cMhywHI0de50f7Nj42YgvyD6bFJ2WkNRvNBlNMrEVgY= -github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w= -github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= +github.com/blevesearch/vellum v1.2.0 h1:xkDiOEsHc2t3Cp0NsNZZ36pvc130sCzcGKOPMzXe+e0= +github.com/blevesearch/vellum v1.2.0/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k= github.com/blevesearch/zapx/v11 v11.2.0/go.mod h1:gN/a0alGw1FZt/YGTo1G6Z6XpDkeOfujX5exY9sCQQM= -github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs= -github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc= +github.com/blevesearch/zapx/v11 v11.4.3 h1:PTZOO5loKpHC/x/GzmPZNa9cw7GZIQxd5qRjwij9tHY= +github.com/blevesearch/zapx/v11 v11.4.3/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc= github.com/blevesearch/zapx/v12 v12.2.0/go.mod h1:fdjwvCwWWwJW/EYTYGtAp3gBA0geCYGLcVTtJEZnY6A= -github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE= -github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58= +github.com/blevesearch/zapx/v12 v12.4.3 h1:eElXvAaAX4m04t//CGBQAtHNPA+Q6A1hHZVrN3LSFYo= +github.com/blevesearch/zapx/v12 v12.4.3/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58= github.com/blevesearch/zapx/v13 v13.2.0/go.mod h1:o5rAy/lRS5JpAbITdrOHBS/TugWYbkcYZTz6VfEinAQ= -github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks= -github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk= +github.com/blevesearch/zapx/v13 v13.4.3 h1:qsdhRhaSpVnqDFlRiH9vG5+KJ+dE7KAW9WyZz/KXAiE= +github.com/blevesearch/zapx/v13 v13.4.3/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk= github.com/blevesearch/zapx/v14 v14.2.0/go.mod h1:GNgZusc1p4ot040cBQMRGEZobvwjCquiEKYh1xLFK9g= -github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0= -github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= +github.com/blevesearch/zapx/v14 v14.4.3 h1:GY4Hecx0C6UTmiNC2pKdeA2rOKiLR5/rwpU9WR51dgM= +github.com/blevesearch/zapx/v14 v14.4.3/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= github.com/blevesearch/zapx/v15 v15.2.0/go.mod h1:MmQceLpWfME4n1WrBFIwplhWmaQbQqLQARpaKUEOs/A= -github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k= -github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= -github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI= -github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14= -github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= -github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2lc/0/CU= +github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= +github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaTbp0Po0= +github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg= +github.com/blevesearch/zapx/v17 v17.1.2 h1:avbOk2igaASNoiy0BE/jPgcxAnRI2PGeydeP4hg7Ikk= +github.com/blevesearch/zapx/v17 v17.1.2/go.mod h1:WQObxKrqUX7cd0G1GMvDfc/bmZzQvoy7APOPimx7DiI= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= @@ -204,29 +187,28 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/buildkite/terminal-to-html/v3 v3.16.8 h1:QN/daUob6cmK8GcdKnwn9+YTlPr1vNj+oeAIiJK6fPc= github.com/buildkite/terminal-to-html/v3 v3.16.8/go.mod h1:+k1KVKROZocrTLsEQ9PEf9A+8+X8uaVV5iO1ZIOwKYM= -github.com/caddyserver/certmagic v0.25.1 h1:4sIKKbOt5pg6+sL7tEwymE1x2bj6CHr80da1CRRIPbY= -github.com/caddyserver/certmagic v0.25.1/go.mod h1:VhyvndxtVton/Fo/wKhRoC46Rbw1fmjvQ3GjHYSQTEY= -github.com/caddyserver/zerossl v0.1.4 h1:CVJOE3MZeFisCERZjkxIcsqIH4fnFdlYWnPYeFtBHRw= -github.com/caddyserver/zerossl v0.1.4/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A= +github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA= +github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= +github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21 h1:2d64+4Jek9vjYwhY93AjbleiVH+AeWvPwPmDi1mfKFQ= -github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21/go.mod h1:fNlYtCHWTRC8MofQERZkVUNUWaOvZeTBqHn/amSbKZI= +github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635 h1:RwCfD5XyO8jAermEy6pauh5Q5o6mCvbeNcCPOZlIA5o= +github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635/go.mod h1:R+SetERD4+IL7QH0WHp9MLifvITvh9gL3g8vX1j2Fcs= github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ= github.com/chi-middleware/proxy v1.1.1/go.mod h1:jQwMEJct2tz9VmtCELxvnXoMfa+SOdikvbVJVHv/M+0= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -235,17 +217,17 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k= github.com/couchbase/go-couchbase v0.1.1 h1:ClFXELcKj/ojyoTYbsY34QUrrYCBi/1G749sXSCkdhk= github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A= -github.com/couchbase/gomemcached v0.3.3 h1:D7qqXLO8wNa4pn5oE65lT3pA3IeStn4joT7/JgGXzKc= -github.com/couchbase/gomemcached v0.3.3/go.mod h1:pISAjweI42vljCumsJIo7CVhqIMIIP9g3Wfhl1JJw68= -github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9BCs= +github.com/couchbase/gomemcached v0.3.4 h1:VGdrZUJbt5lLyI/MXnyVCZKHKYXg/vaud08lJIAeZps= +github.com/couchbase/gomemcached v0.3.4/go.mod h1:pISAjweI42vljCumsJIo7CVhqIMIIP9g3Wfhl1JJw68= github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE= +github.com/couchbase/goutils v0.3.0 h1:rsv72B6BDjW9jmwlfiDUrdu3EpNvPuo5WLULHzQ0DLE= +github.com/couchbase/goutils v0.3.0/go.mod h1:7Gm+D3vXfV4HS+hQWvKfy6e6ILCptGXNqBKvQXhplhk= github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -259,8 +241,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= @@ -281,22 +263,20 @@ github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTe github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ethantkoenig/rupture v1.0.1 h1:6aAXghmvtnngMgQzy7SMGdicMvkV86V4n9fT0meE5E4= github.com/ethantkoenig/rupture v1.0.1/go.mod h1:Sjqo/nbffZp1pVVXNGhpugIjsWmuS9KiIB4GtpEBur4= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= +github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/getkin/kin-openapi v0.137.0 h1:Q3HhawNQV0GfvO2mIYMUBUSEFrDsVlzcYz4VydL9YEo= +github.com/getkin/kin-openapi v0.137.0/go.mod h1:vUYWaKyMqj7PfTybelXtLuLN9tReS12vxnzMRK+z2GY= github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0= github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= @@ -310,50 +290,56 @@ 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 v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= -github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= -github.com/go-enry/go-enry/v2 v2.9.4 h1:DS4l06/NgMzYjsJ2J52wORo6UsfFDjDCwfAn7w3gG44= -github.com/go-enry/go-enry/v2 v2.9.4/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= +github.com/go-co-op/gocron/v2 v2.21.1 h1:QYOK6iOQVCut+jDcs4zRdWRTBHRxRCEeeFi1TnAmgbU= +github.com/go-co-op/gocron/v2 v2.21.1/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= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8= github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= -github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= -github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc= +github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= -github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= +github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/go-redsync/redsync/v4 v4.15.0 h1:KH/XymuxSV7vyKs6z1Cxxj+N+N18JlPxgXeP6x4JY54= -github.com/go-redsync/redsync/v4 v4.15.0/go.mod h1:qNp+lLs3vkfZbtA/aM/OjlZHfEr5YTAYhRktFPKHC7s= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-redsync/redsync/v4 v4.16.0 h1:bNcOzeHH9d3s6pghU9NJFMPrQa41f5Nx3L4YKr3BdEU= +github.com/go-redsync/redsync/v4 v4.16.0/go.mod h1:V4gagqgyASWBZuwx4xGzu72aZNb/6Mo05byUa3mVmKQ= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= -github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-webauthn/webauthn v0.13.4 h1:q68qusWPcqHbg9STSxBLBHnsKaLxNO0RnVKaAqMuAuQ= -github.com/go-webauthn/webauthn v0.13.4/go.mod h1:MglN6OH9ECxvhDqoq1wMoF6P6JRYDiQpC9nc5OomQmI= -github.com/go-webauthn/x v0.1.24 h1:6LaWf2zzWqbyKT8IyQkhje1/1KCGhlEkMz4V1tDnt/A= -github.com/go-webauthn/x v0.1.24/go.mod h1:2o5XKJ+X1AKqYKGgHdKflGnoQFQZ6flJ2IFCBKSbSOw= +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.17.2 h1:e9YtSZTVnxnMWFezXi6JvnqOSxmH4Er8QDHK2a/mM40= +github.com/go-webauthn/webauthn v0.17.2/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= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0= @@ -364,20 +350,10 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -392,14 +368,11 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8= github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= -github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -408,30 +381,24 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM= -github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= -github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-github/v85 v85.0.0 h1:1+TLFX/akTFXK7o9Z9uAloQGufOn4ySa5DItUM1VWT4= +github.com/google/go-github/v85 v85.0.0/go.mod h1:jYkBnqN+SzR2A2fGKYfbt6DEEQAyxeK0Q2XpPV9ZFsU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc= +github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/licenseclassifier/v2 v2.0.0 h1:1Y57HHILNf4m0ABuMVb6xk4vAJYEUO0gDxNpog0pyeA= github.com/google/licenseclassifier/v2 v2.0.0/go.mod h1:cOjbdH0kyC9R22sdQbYsFkto4NGCAc+ZSwbeThazEtM= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef h1:xpF9fUHpoIrrjX24DURVKiwHcFpw19ndIs+FwTSMbno= -github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw= github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -451,6 +418,8 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/graph-gophers/graphql-go v1.9.0 h1:yu0ucKHLc5qGpRwLYKIWtr9bOoxovkWasuBrPQwlHls= +github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -464,10 +433,8 @@ github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVU github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= -github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= +github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -476,8 +443,9 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/inbucket/html2text v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs= +github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= @@ -493,25 +461,24 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw= -github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs= +github.com/jhillyerd/enmime/v2 v2.3.0 h1:Y/pzQanyU8nkSgB2npXX8Dha5OItJE/QwbDJM4sf/kU= +github.com/jhillyerd/enmime/v2 v2.3.0/go.mod h1:mGKXAP45l6pF6HZiaLhgSYsgteJskaSIYmEZXpw6ZpI= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= -github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= +github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= -github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -522,8 +489,6 @@ github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -533,87 +498,92 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= -github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI= -github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU= +github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk= +github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U= +github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE= github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o= github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ= github.com/markbates/goth v1.82.0/go.mod h1:/DRlcq0pyqkKToyZjsL2KgiA1zbF1HIjE7u2uC79rUk= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= -github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/meilisearch/meilisearch-go v0.36.0 h1:N1etykTektXt5KPcSbhBO0d5Xx5NaKj4pJWEM7WA5dI= -github.com/meilisearch/meilisearch-go v0.36.0/go.mod h1:HBfHzKMxcSbTOvqdfuRA/yf6Vk9IivcwKocWRuW7W78= -github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ= -github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= +github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +github.com/meilisearch/meilisearch-go v0.36.2 h1:MYaMPCpdLh2aYPt+zK+19mLoA4dfBY3S1L7T0FADCjU= +github.com/meilisearch/meilisearch-go v0.36.2/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM= +github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk= +github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY= github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ= github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/microsoft/go-mssqldb v1.9.6 h1:1MNQg5UiSsokiPz3++K2KPx4moKrwIqly1wv+RyCKTw= -github.com/microsoft/go-mssqldb v1.9.6/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4= -github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= -github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= +github.com/microsoft/go-mssqldb v1.9.7 h1:I+JEk79gYsc6bdVzDHFSSYE9dtNa7dxRwJ0WQbt6i8w= +github.com/microsoft/go-mssqldb v1.9.7/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0= -github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM= -github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= -github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8= +github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA= +github.com/minio/minlz v1.1.0 h1:rUOGu3EP4EqJC5k3qCsIwEnZiJULKqtRyDdqbhlvMmQ= +github.com/minio/minlz v1.1.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= -github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE= -github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0= +github.com/msteinert/pam/v2 v2.1.0 h1:er5F9TKV5nGFuTt12ubtqPHEUdeBwReP7vd3wovidGY= +github.com/msteinert/pam/v2 v2.1.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0= github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48= -github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= -github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU= +github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00 h1:ZCnkxe9GgWqqBxAk3cIKlQJuaqgOUF/nUtQs8flVTHM= -github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= -github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= -github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.0 h1:7nX5bgpvfyxsvI90IJpOIU5zd4MBV6nRkD49e/dEx98= -github.com/olekukonko/ll v0.1.0/go.mod h1:2dJo+hYZcJMLMbKwHEWvxCUbAOLc/CXWS9noET22Mdo= -github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8= -github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= -github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= -github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k= +github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48= +github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM= +github.com/oasdiff/yaml3 v0.0.12 h1:75urAtPeDg2/iDEWwzNrLOWxI9N/dCh81nTTJtokt2M= +github.com/oasdiff/yaml3 v0.0.12/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= +github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= +github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= +github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= +github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= @@ -630,16 +600,17 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= -github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY= -github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -649,35 +620,28 @@ github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= -github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw= github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4= -github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= -github.com/redis/rueidis v1.0.69 h1:WlUefRhuDekji5LsD387ys3UCJtSFeBVf0e5yI0B8b4= -github.com/redis/rueidis v1.0.69/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA= -github.com/redis/rueidis/rueidiscompat v1.0.69 h1:IWVYY9lXdjNO3do2VpJT7aDFi8zbCUuQxZB6E2Grahs= -github.com/redis/rueidis/rueidiscompat v1.0.69/go.mod h1:iC4Y8DoN0Uth0Uezg9e2trvNRC7QAgGeuP2OPLb5ccI= +github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k= +github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= +github.com/redis/rueidis v1.0.71 h1:pODtnAR5GAB7j4ekhldZ29HKOxe4Hph0GTDGk1ayEQY= +github.com/redis/rueidis v1.0.71/go.mod h1:lfdcZzJ1oKGKL37vh9fO3ymwt+0TdjkkUCJxbgpmcgQ= +github.com/redis/rueidis/rueidiscompat v1.0.71 h1:wNZ//kEjMZgBM0KCk7ncOX8KmAgROU2kDdDNpwheG4w= +github.com/redis/rueidis/rueidiscompat v1.0.71/go.mod h1:esmCLJvaRzZoKlgB82G1bY7Iky5TnO9Rz+NlhbEccFI= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rhysd/actionlint v1.7.7 h1:0KgkoNTrYY7vmOCs9BW2AHxLvvpoY9nEUzgBHiPUr0k= -github.com/rhysd/actionlint v1.7.7/go.mod h1:AE6I6vJEkNaIfWqC2GNE5spIJNhxf8NCtLEKU4NnUXg= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY= +github.com/rhysd/actionlint v1.7.12/go.mod h1:krOUhujIsJusovkaYzQ/VNH8PFexjNKqU0q5XI/4w+g= github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= @@ -685,9 +649,8 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI= github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= @@ -697,13 +660,12 @@ github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepq github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= -github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= +github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.1.1 h1:T/YLemO5Yp7KPzS+lVtu+WsHn8yoSwTfItdAd1r3cck= -github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -724,8 +686,9 @@ github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -734,7 +697,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -744,23 +706,27 @@ 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.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= -github.com/tinylib/msgp v1.6.1/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= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= -github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI= -github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU= -github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= -github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= +github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= +github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= +github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -779,54 +745,48 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js= github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBzPCRjkCNs= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= -github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= -github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= -github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -gitlab.com/gitlab-org/api/client-go v0.142.4 h1:tTm+hUPrOcTavmKpM9YIP503IE0EdAkg4TG3t6QGbiw= -gitlab.com/gitlab-org/api/client-go v0.142.4/go.mod h1:Ru5IRauphXt9qwmTzJD7ou1dH7Gc6pnsdFWEiMMpmB0= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +gitlab.com/gitlab-org/api/client-go/v2 v2.24.1 h1:XuTWCNmVSnXzjb9ty8XPR60BOHsXbzqgOlujW3+5DTM= +gitlab.com/gitlab-org/api/client-go/v2 v2.24.1/go.mod h1:OSJITkIrT0UuA3JCucEK9UEGcC1PWBkQg5WW6W4nWuo= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= -go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= -go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= +go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= +go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= +go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= @@ -837,61 +797,25 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I= -golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= +golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -905,21 +829,12 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -929,30 +844,20 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -972,8 +877,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -984,12 +889,9 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -999,86 +901,29 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 h1:YedBIttDguBl/zy2wJauEUm+DZZg4UXseWj0g/3N+yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1088,14 +933,12 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= -gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss= +gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= @@ -1105,46 +948,44 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= -lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= -modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= -modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= -modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= -modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= -modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= -modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE= -modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= +modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= +modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= +modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c= +modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM= +modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= pgregory.net/rapid v0.4.2 h1:lsi9jhvZTYvzVpeG93WWgimPRmiJQfGFRNTEZh1dtY0= pgregory.net/rapid v0.4.2/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs= -strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY= +strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab h1:3IZDVyI8uBmZko6pwm39f7mE0aTY7LViNdQHMeH7U60= +strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY= xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= xorm.io/xorm v1.3.11 h1:i4tlVUASogb0ZZFJHA7dZqoRU2pUpUsutnNdaOlFyMI= diff --git a/models/actions/artifact.go b/models/actions/artifact.go index ec5cc0e32f..1de83a29c5 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -// This artifact server is inspired by https://github.com/nektos/act/blob/master/pkg/artifacts/server.go. +// This artifact server is inspired by the Gitea runner artifact server implementation. // It updates url setting and uses ObjectStore to handle artifacts persistence. package actions @@ -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" @@ -53,36 +54,53 @@ func init() { db.RegisterModel(new(ActionArtifact)) } +const ( + ContentEncodingV3Gzip = "gzip" + ContentTypeZip = "application/zip" +) + // 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 CommitSHA string - StoragePath string // The path to the artifact in the storage - FileSize int64 // The size of the artifact in bytes - FileCompressedSize int64 // The size of the artifact in bytes after gzip compression - ContentEncoding string // The content encoding of the artifact - 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 - CreatedUnix timeutil.TimeStamp `xorm:"created"` - UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` - ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired + StoragePath string // The path to the artifact in the storage + FileSize int64 // The size of the artifact in bytes + FileCompressedSize int64 // The size of the artifact in bytes after gzip compression + + // The content encoding or content type of the artifact + // * empty or null: legacy (v3) uncompressed content + // * magic string "gzip" (ContentEncodingV3Gzip): v3 gzip compressed content + // * requires gzip decoding before storing in a zip for download + // * requires gzip content-encoding header when downloaded single files within a workflow + // * mime type for "Content-Type": + // * "application/zip" (ContentTypeZip), seems to be an abuse, fortunately there is no conflict, and it won't cause problems? + // * "application/pdf", "text/html", etc.: real content type of the artifact + ContentEncodingOrType string `xorm:"content_encoding"` + + 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 } func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiredDays int64) (*ActionArtifact, error) { 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, @@ -107,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 { @@ -129,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 @@ -148,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}) } @@ -156,7 +178,8 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond { } if opts.FinalizedArtifactsV4 { cond = cond.And(builder.Eq{"status": ArtifactStatusUploadConfirmed}.Or(builder.Eq{"status": ArtifactStatusExpired})) - cond = cond.And(builder.Eq{"content_encoding": "application/zip"}) + // see the comment of ActionArtifact.ContentEncodingOrType: "*/*" means the field is a content type + cond = cond.And(builder.Like{"content_encoding", "%/%"}) } return cond @@ -167,15 +190,17 @@ type ActionArtifactMeta struct { ArtifactName string FileSize int64 Status ArtifactStatus + 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"). + Select("artifact_name, sum(file_size) as file_size, max(status) as status, max(expired_unix) as expired_unix"). Find(&arts) } @@ -200,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 e293a6056f..5d7f51ade3 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -7,7 +7,6 @@ import ( "context" "errors" "fmt" - "slices" "strings" "time" @@ -30,7 +29,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 +49,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() { @@ -66,11 +70,11 @@ func init() { db.RegisterModel(new(ActionRunIndex)) } -func (run *ActionRun) HTMLURL() string { +func (run *ActionRun) HTMLURL(ctxOpt ...context.Context) string { if run.Repo == nil { return "" } - return fmt.Sprintf("%s/actions/runs/%d", run.Repo.HTMLURL(), run.ID) + return fmt.Sprintf("%s/actions/runs/%d", run.Repo.HTMLURL(ctxOpt...), run.ID) } func (run *ActionRun) Link() string { @@ -116,10 +120,6 @@ func (run *ActionRun) RefTooltip() string { // LoadAttributes load Repo TriggerUser if not loaded func (run *ActionRun) LoadAttributes(ctx context.Context) error { - if run == nil { - return nil - } - if err := run.LoadRepo(ctx); err != nil { return err } @@ -128,19 +128,19 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error { return err } - if run.TriggerUser == nil { - u, err := user_model.GetPossibleUserByID(ctx, run.TriggerUserID) - if err != nil { - return err - } - run.TriggerUser = u - } + return run.LoadTriggerUser(ctx) +} - return nil +func (run *ActionRun) LoadTriggerUser(ctx context.Context) (err error) { + if run.TriggerUser != nil { + return nil + } + run.TriggerUserID, run.TriggerUser, err = user_model.GetPossibleUserByID(ctx, run.TriggerUserID) + return err } func (run *ActionRun) LoadRepo(ctx context.Context) error { - if run == nil || run.Repo != nil { + if run.Repo != nil { return nil } @@ -153,7 +153,36 @@ func (run *ActionRun) LoadRepo(ctx context.Context) error { } func (run *ActionRun) Duration() time.Duration { - return calculateDuration(run.Started, run.Stopped, run.Status) + run.PreviousDuration + d := calculateDuration(run.Started, run.Stopped, run.Status, run.Updated) + run.PreviousDuration + if d < 0 { + return 0 + } + 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) { @@ -194,30 +223,34 @@ func (run *ActionRun) IsSchedule() bool { } // UpdateRepoRunsNumbers updates the number of runs and closed runs of a repository. -func UpdateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { - _, err := db.GetEngine(ctx).ID(repo.ID). - NoAutoTime(). - Cols("num_action_runs", "num_closed_action_runs"). - SetExpr("num_action_runs", - builder.Select("count(*)").From("action_run"). - Where(builder.Eq{"repo_id": repo.ID}), - ). - SetExpr("num_closed_action_runs", - builder.Select("count(*)").From("action_run"). - Where(builder.Eq{ - "repo_id": repo.ID, - }.And( - builder.In("status", - StatusSuccess, - StatusFailure, - StatusCancelled, - StatusSkipped, - ), - ), - ), - ). - Update(repo) - return err +// Callers MUST invoke this from outside any transaction that has X-locked action_run rows for the same repo, otherwise, transaction deadlock +func UpdateRepoRunsNumbers(ctx context.Context, repoID int64) { + if db.InTransaction(ctx) { + setting.PanicInDevOrTesting("UpdateRepoRunsNumbers must not be called inside a transaction") + } + + e := db.GetEngine(ctx) + + numActionRuns, err := e.Where("repo_id = ?", repoID).Count(new(ActionRun)) + if err != nil { + log.Error("UpdateRepoRunsNumbers count num_action_runs for repo %d: %v", repoID, err) + return + } + + numClosedActionRuns, err := e.Where("repo_id = ?", repoID). + In("status", StatusSuccess, StatusFailure, StatusCancelled, StatusSkipped). + Count(new(ActionRun)) + if err != nil { + log.Error("UpdateRepoRunsNumbers count num_closed_action_runs for repo %d: %v", repoID, err) + return + } + + if _, err := e.ID(repoID).Cols("num_action_runs", "num_closed_action_runs").NoAutoTime().Update(&repo_model.Repository{ + NumActionRuns: int(numActionRuns), + NumClosedActionRuns: int(numClosedActionRuns), + }); err != nil { + log.Error("UpdateRepoRunsNumbers update repo %d: %v", repoID, err) + } } // CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event. @@ -322,16 +355,16 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er return &run, nil } -func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error) { +func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionRun, error) { run := &ActionRun{ RepoID: repoID, - Index: index, + Index: runIndex, } has, err := db.GetEngine(ctx).Get(run) if err != nil { return nil, err } else if !has { - return nil, fmt.Errorf("run with index %d %d: %w", repoID, index, util.ErrNotExist) + return nil, fmt.Errorf("run with repo_id %d and index %d: %w", repoID, runIndex, util.ErrNotExist) } return run, nil @@ -385,31 +418,16 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { // It's impossible that the run is not found, since Gitea never deletes runs. } - if run.Status != 0 || slices.Contains(cols, "status") { - if run.RepoID == 0 { - setting.PanicInDevOrTesting("RepoID should not be 0") - } - if err = run.LoadRepo(ctx); err != nil { - return err - } - if err := UpdateRepoRunsNumbers(ctx, run.Repo); err != nil { - return err - } - } - return nil } 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{ @@ -421,36 +439,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 0000000000..857247b068 --- /dev/null +++ b/models/actions/run_attempt.go @@ -0,0 +1,140 @@ +// 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.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 0000000000..77a5b8f15c --- /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 616e298dc9..f0d41ef4b4 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -18,6 +18,11 @@ import ( "xorm.io/builder" ) +// MaxJobNumPerRun is the maximum number of jobs in a single run. +// https://docs.github.com/en/actions/reference/limits#existing-system-limits +// TODO: check this limit when creating jobs +const MaxJobNumPerRun = 256 + // ActionRunJob represents a job of a run type ActionRunJob struct { ID int64 @@ -29,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 @@ -38,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 @@ -56,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"` @@ -67,7 +86,14 @@ func init() { } func (job *ActionRunJob) Duration() time.Duration { - return calculateDuration(job.Started, job.Stopped, job.Status) + 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 { @@ -94,10 +120,6 @@ func (job *ActionRunJob) LoadRepo(ctx context.Context) error { // LoadAttributes load Run if not loaded func (job *ActionRunJob) LoadAttributes(ctx context.Context) error { - if job == nil { - return nil - } - if err := job.LoadRun(ctx); err != nil { return err } @@ -147,9 +169,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 @@ -191,25 +254,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) + } } } @@ -264,7 +353,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) } @@ -272,12 +361,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 10f76d3641..5f4e482720 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -5,10 +5,13 @@ package actions import ( "context" + "slices" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "xorm.io/builder" @@ -22,6 +25,22 @@ func (jobs ActionJobList) GetRunIDs() []int64 { }) } +// SortMatrixGroupsByName natural-sorts each contiguous run of jobs that share a JobID +// so matrix expansions (e.g. "test (1)", "test (2)", "test (10)") appear in human order. +// Input is expected to be in DB id order so JobID groups are contiguous; cross-group order is preserved. +func (jobs ActionJobList) SortMatrixGroupsByName() { + for i := 0; i < len(jobs); { + j := i + 1 + for j < len(jobs) && jobs[j].JobID == jobs[i].JobID { + j++ + } + slices.SortFunc(jobs[i:j], func(a, b *ActionRunJob) int { + return base.NaturalSortCompare(a.Name, b.Name) + }) + i = j + } +} + func (jobs ActionJobList) LoadRepos(ctx context.Context) error { repoIDs := container.FilterSlice(jobs, func(j *ActionRunJob) (int64, bool) { return j.RepoID, j.RepoID != 0 && j.Repo == nil @@ -55,8 +74,10 @@ func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error { return err } for _, j := range jobs { - if j.RunID > 0 && j.Run == nil { + if j.Run == nil { j.Run = runs[j.RunID] + } + if j.Run != nil { j.Run.Repo = j.Repo } } @@ -70,12 +91,19 @@ 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 Statuses []Status UpdatedBefore timeutil.TimeStamp ConcurrencyGroup string + OrderBy db.SearchOrderBy +} + +var JobOrderByMap = map[string]map[string]db.SearchOrderBy{ + "asc": {"id": "`action_run_job`.id ASC"}, + "desc": {"id": "`action_run_job`.id DESC"}, } func (opts FindRunJobOptions) ToConds() builder.Cond { @@ -83,6 +111,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}) } @@ -115,3 +146,9 @@ func (opts FindRunJobOptions) ToJoins() []db.JoinFunc { } return nil } + +func (opts FindRunJobOptions) ToOrders() string { + return string(opts.OrderBy) +} + +var _ db.FindOptionsOrder = FindRunJobOptions{} diff --git a/models/actions/run_job_list_test.go b/models/actions/run_job_list_test.go new file mode 100644 index 0000000000..271031b4cd --- /dev/null +++ b/models/actions/run_job_list_test.go @@ -0,0 +1,61 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestActionJobList_SortMatrixGroupsByName(t *testing.T) { + mk := func(jobID, name string) *ActionRunJob { + return &ActionRunJob{JobID: jobID, Name: name} + } + names := func(jobs ActionJobList) []string { + out := make([]string, len(jobs)) + for i, j := range jobs { + out[i] = j.Name + } + return out + } + + t.Run("matrix group sorted naturally", func(t *testing.T) { + jobs := ActionJobList{ + mk("build", "build"), + mk("test", "test (10)"), + mk("test", "test (2)"), + mk("test", "test (1)"), + mk("deploy", "deploy"), + } + jobs.SortMatrixGroupsByName() + assert.Equal(t, []string{"build", "test (1)", "test (2)", "test (10)", "deploy"}, names(jobs)) + }) + + t.Run("non-adjacent same JobID stays in input order", func(t *testing.T) { + jobs := ActionJobList{ + mk("test", "test (10)"), + mk("build", "build"), + mk("test", "test (1)"), + } + jobs.SortMatrixGroupsByName() + assert.Equal(t, []string{"test (10)", "build", "test (1)"}, names(jobs)) + }) + + t.Run("groups stay in input order", func(t *testing.T) { + jobs := ActionJobList{ + mk("z", "z"), + mk("a", "a"), + } + jobs.SortMatrixGroupsByName() + assert.Equal(t, []string{"z", "a"}, names(jobs)) + }) + + t.Run("empty and singleton", func(t *testing.T) { + ActionJobList(nil).SortMatrixGroupsByName() + jobs := ActionJobList{mk("only", "only")} + jobs.SortMatrixGroupsByName() + assert.Equal(t, []string{"only"}, names(jobs)) + }) +} diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 2628c4712f..0a0840648d 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -18,46 +18,40 @@ import ( type RunList []*ActionRun -// GetUserIDs returns a slice of user's id -func (runs RunList) GetUserIDs() []int64 { - return container.FilterSlice(runs, func(run *ActionRun) (int64, bool) { - return run.TriggerUserID, true - }) -} - -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() + userIDs := container.FilterSlice(runs, func(run *ActionRun) (int64, bool) { + return run.TriggerUserID, run.TriggerUser == nil + }) users := make(map[int64]*user_model.User, len(userIDs)) if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil { return err } for _, run := range runs { - if run.TriggerUserID == user_model.ActionsUserID { - run.TriggerUser = user_model.NewActionsUser() - } else { - run.TriggerUser = users[run.TriggerUserID] - if run.TriggerUser == nil { - run.TriggerUser = user_model.NewGhostUser() - } + if run.TriggerUser != nil { + continue + } + run.TriggerUser = users[run.TriggerUserID] + if run.TriggerUserID < 0 { + run.TriggerUserID, run.TriggerUser, _ = user_model.GetPossibleUserByID(ctx, run.TriggerUserID) + } else if run.TriggerUser == nil { + run.TriggerUserID, run.TriggerUser, _ = user_model.GetPossibleUserByID(ctx, user_model.GhostUserID) } } return nil } func (runs RunList) LoadRepos(ctx context.Context) error { - repoIDs := runs.GetRepoIDs() + repoIDs := container.FilterSlice(runs, func(run *ActionRun) (int64, bool) { + return run.RepoID, run.Repo == nil + }) repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs) if err != nil { return err } for _, run := range runs { - run.Repo = repos[run.RepoID] + if run.Repo == nil { + run.Repo = repos[run.RepoID] + } } return nil } @@ -102,12 +96,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/run_test.go b/models/actions/run_test.go index bd2b92f4f6..6c8160beae 100644 --- a/models/actions/run_test.go +++ b/models/actions/run_test.go @@ -5,10 +5,12 @@ package actions import ( "testing" + "time" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" ) @@ -27,9 +29,18 @@ func TestUpdateRepoRunsNumbers(t *testing.T) { assert.Equal(t, 2, repo.NumClosedActionRuns) // now update will correct them, only num_actionr_runs and num_closed_action_runs should be updated - err = UpdateRepoRunsNumbers(t.Context(), repo) - assert.NoError(t, err) + UpdateRepoRunsNumbers(t.Context(), repo.ID) repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) - assert.Equal(t, 5, repo.NumActionRuns) + assert.Equal(t, 4, repo.NumActionRuns) assert.Equal(t, 3, repo.NumClosedActionRuns) } + +func TestActionRun_Duration_NonNegative(t *testing.T) { + run := &ActionRun{ + Started: timeutil.TimeStamp(100), + Stopped: timeutil.TimeStamp(200), + Status: StatusSuccess, + PreviousDuration: -time.Hour, + } + assert.Equal(t, time.Duration(0), run.Duration()) +} diff --git a/models/actions/runner.go b/models/actions/runner.go index f5d40ca7d6..f0088491bb 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -171,9 +171,8 @@ func (r *ActionRunner) LoadAttributes(ctx context.Context) error { return nil } -func (r *ActionRunner) GenerateToken() (err error) { - r.Token, r.TokenSalt, r.TokenHash, _, err = generateSaltedToken() - return err +func (r *ActionRunner) GenerateAndFillToken() { + r.Token, r.TokenSalt, r.TokenHash, _ = generateSaltedToken() } // CanMatchLabels checks whether the runner's labels can match a job's "runs-on" diff --git a/models/actions/runner_token.go b/models/actions/runner_token.go index bbd2af73b6..f7b7c9fdf0 100644 --- a/models/actions/runner_token.go +++ b/models/actions/runner_token.go @@ -97,10 +97,7 @@ func NewRunnerTokenWithValue(ctx context.Context, ownerID, repoID int64, token s } func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { - token, err := util.CryptoRandomString(40) - if err != nil { - return nil, err - } + token := util.CryptoRandomString(40) return NewRunnerTokenWithValue(ctx, ownerID, repoID, token) } diff --git a/models/actions/schedule_list.go b/models/actions/schedule_list.go index 5361b94801..6b5cae94fe 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 e092d6fbbd..7a97eadc79 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -77,7 +77,7 @@ func init() { } func (task *ActionTask) Duration() time.Duration { - return calculateDuration(task.Started, task.Stopped, task.Status) + return calculateDuration(task.Started, task.Stopped, task.Status, task.Updated) } func (task *ActionTask) IsStopped() bool { @@ -125,9 +125,6 @@ func (task *ActionTask) LoadJob(ctx context.Context) error { // LoadAttributes load Job Steps if not loaded func (task *ActionTask) LoadAttributes(ctx context.Context) error { - if task == nil { - return nil - } if err := task.LoadJob(ctx); err != nil { return err } @@ -147,9 +144,8 @@ func (task *ActionTask) LoadAttributes(ctx context.Context) error { return nil } -func (task *ActionTask) GenerateToken() (err error) { - task.Token, task.TokenSalt, task.TokenHash, task.TokenLastEight, err = generateSaltedToken() - return err +func (task *ActionTask) GenerateAndFillToken() { + task.Token, task.TokenSalt, task.TokenHash, task.TokenLastEight = generateSaltedToken() } func GetTaskByID(ctx context.Context, id int64) (*ActionTask, error) { @@ -273,7 +269,6 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask } now := timeutil.TimeStampNow() - job.Attempt++ job.Started = now job.Status = StatusRunning @@ -288,9 +283,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask CommitSHA: job.CommitSHA, IsForkPullRequest: job.IsForkPullRequest, } - if err := task.GenerateToken(); err != nil { - return nil, false, err - } + task.GenerateAndFillToken() workflowJob, err := job.ParseJob() if err != nil { diff --git a/models/actions/task_step.go b/models/actions/task_step.go index 03ffbf1931..3b477d8483 100644 --- a/models/actions/task_step.go +++ b/models/actions/task_step.go @@ -28,7 +28,7 @@ type ActionTaskStep struct { } func (step *ActionTaskStep) Duration() time.Duration { - return calculateDuration(step.Started, step.Stopped, step.Status) + return calculateDuration(step.Started, step.Stopped, step.Status, step.Updated) } func init() { diff --git a/models/actions/utils.go b/models/actions/utils.go index f6ba661ae3..e5704d0377 100644 --- a/models/actions/utils.go +++ b/models/actions/utils.go @@ -13,22 +13,17 @@ import ( "time" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) -func generateSaltedToken() (string, string, string, string, error) { - salt, err := util.CryptoRandomString(10) - if err != nil { - return "", "", "", "", err - } - buf, err := util.CryptoRandomBytes(20) - if err != nil { - return "", "", "", "", err - } +func generateSaltedToken() (string, string, string, string) { + salt := util.CryptoRandomString(10) + buf := util.CryptoRandomBytes(20) token := hex.EncodeToString(buf) hash := auth_model.HashToken(token, salt) - return token, salt, hash, token[len(token)-8:], nil + return token, salt, hash, token[len(token)-8:] } /* @@ -72,13 +67,25 @@ func (indexes *LogIndexes) ToDB() ([]byte, error) { var timeSince = time.Since -func calculateDuration(started, stopped timeutil.TimeStamp, status Status) time.Duration { +// calculateDuration computes wall time for a run, job, task, or step. When status is terminal +// but stopped is missing or inconsistent with started, fallbackEnd (typically the row Updated +// time) is used so duration still reflects approximate elapsed time instead of 0 or a negative. +func calculateDuration(started, stopped timeutil.TimeStamp, status Status, fallbackEnd timeutil.TimeStamp) time.Duration { if started == 0 { return 0 } s := started.AsTime() if status.IsDone() { - return stopped.AsTime().Sub(s) + end := stopped + if stopped.IsZero() || stopped < started { + if !fallbackEnd.IsZero() && fallbackEnd >= started { + end = fallbackEnd + } else { + log.Trace("actions: invalid duration timestamps (started=%d, stopped=%d, fallbackEnd=%d, status=%s)", started, stopped, fallbackEnd, status) + return 0 + } + } + return end.AsTime().Sub(s) } return timeSince(s).Truncate(time.Second) } diff --git a/models/actions/utils_test.go b/models/actions/utils_test.go index 98c048d4ef..2f7e7da360 100644 --- a/models/actions/utils_test.go +++ b/models/actions/utils_test.go @@ -45,9 +45,10 @@ func Test_calculateDuration(t *testing.T) { return timeutil.TimeStamp(1000).AsTime().Sub(t) } type args struct { - started timeutil.TimeStamp - stopped timeutil.TimeStamp - status Status + started timeutil.TimeStamp + stopped timeutil.TimeStamp + status Status + fallbackEnd timeutil.TimeStamp } tests := []struct { name string @@ -81,10 +82,48 @@ func Test_calculateDuration(t *testing.T) { }, want: 100 * time.Second, }, + { + name: "done_stopped_zero_no_fallback", + args: args{ + started: 500, + stopped: 0, + status: StatusSuccess, + }, + want: 0, + }, + { + name: "done_stopped_zero_uses_fallback", + args: args{ + started: 500, + stopped: 0, + status: StatusSuccess, + fallbackEnd: 600, + }, + want: 100 * time.Second, + }, + { + name: "done_stopped_before_started_no_fallback", + args: args{ + started: 600, + stopped: 550, + status: StatusSuccess, + }, + want: 0, + }, + { + name: "done_stopped_before_started_uses_fallback", + args: args{ + started: 600, + stopped: 550, + status: StatusSuccess, + fallbackEnd: 650, + }, + want: 50 * time.Second, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, calculateDuration(tt.args.started, tt.args.stopped, tt.args.status), "calculateDuration(%v, %v, %v)", tt.args.started, tt.args.stopped, tt.args.status) + assert.Equalf(t, tt.want, calculateDuration(tt.args.started, tt.args.stopped, tt.args.status, tt.args.fallbackEnd), "calculateDuration(%v, %v, %v, %v)", tt.args.started, tt.args.stopped, tt.args.status, tt.args.fallbackEnd) }) } } diff --git a/models/activities/action.go b/models/activities/action.go index 8e589eda88..4ffdca842a 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 29ff2fdf7a..5b07a8e080 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/activities/user_heatmap.go b/models/activities/user_heatmap.go index e24d44c519..2d1635917e 100644 --- a/models/activities/user_heatmap.go +++ b/models/activities/user_heatmap.go @@ -62,6 +62,7 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi return nil, err } + // HINT: USER-ACTIVITY-PUSH-COMMITS: it only uses the doer's action time, it doesn't use git commit's time return hdata, db.GetEngine(ctx). Select(groupBy+" AS timestamp, count(user_id) as contributions"). Table("action"). diff --git a/models/admin/task.go b/models/admin/task.go index 5d2b9bbff6..7056a8359e 100644 --- a/models/admin/task.go +++ b/models/admin/task.go @@ -137,6 +137,11 @@ func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) { log.Error("Unable to decrypt AuthToken, maybe SECRET_KEY is wrong: %v", err) } } + if opts.AWSSecretAccessKeyEncrypted != "" { + if opts.AWSSecretAccessKey, err = secret.DecryptSecret(setting.SecretKey, opts.AWSSecretAccessKeyEncrypted); err != nil { + log.Error("Unable to decrypt AWSSecretAccessKey, maybe SECRET_KEY is wrong: %v", err) + } + } return &opts, nil } @@ -201,6 +206,8 @@ func FinishMigrateTask(ctx context.Context, task *Task) error { conf.AuthPasswordEncrypted = "" conf.AuthTokenEncrypted = "" conf.CloneAddrEncrypted = "" + conf.AWSSecretAccessKey = "" + conf.AWSSecretAccessKeyEncrypted = "" confBytes, err := json.Marshal(conf) if err != nil { return err diff --git a/models/asymkey/error.go b/models/asymkey/error.go index b765624579..5df7beb8cd 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.go b/models/asymkey/ssh_key.go index 98784b36bd..1873c30859 100644 --- a/models/asymkey/ssh_key.go +++ b/models/asymkey/ssh_key.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models/perm" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -64,7 +65,12 @@ func (key *PublicKey) AfterLoad() { // OmitEmail returns content of public key without email address. func (key *PublicKey) OmitEmail() string { - return strings.Join(strings.Split(key.Content, " ")[:2], " ") + fields := strings.Split(key.Content, " ") // format: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... comment + if len(fields) < 2 { + setting.PanicInDevOrTesting("invalid public key %d content: %s", key.ID, key.Content) + return "" // not a valid public key, it shouldn't really happen, the value is managed internally + } + return strings.Join(fields[:2], " ") } func addKey(ctx context.Context, key *PublicKey) (err error) { diff --git a/models/asymkey/ssh_key_deploy.go b/models/asymkey/ssh_key_deploy.go index 4ab84eabcf..ea3d93e8c8 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/asymkey/ssh_key_principals.go b/models/asymkey/ssh_key_principals.go index e8b97d306e..2d4c74740d 100644 --- a/models/asymkey/ssh_key_principals.go +++ b/models/asymkey/ssh_key_principals.go @@ -40,7 +40,7 @@ func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content if !email.IsActivated { continue } - if content == email.Email { + if strings.EqualFold(content, email.LowerEmail) { return content, nil } } diff --git a/models/auth/access_token.go b/models/auth/access_token.go index 63331b4841..7578528be8 100644 --- a/models/auth/access_token.go +++ b/models/auth/access_token.go @@ -98,19 +98,13 @@ func init() { // NewAccessToken creates new access token. func NewAccessToken(ctx context.Context, t *AccessToken) error { - salt, err := util.CryptoRandomString(10) - if err != nil { - return err - } - token, err := util.CryptoRandomBytes(20) - if err != nil { - return err - } + salt := util.CryptoRandomString(10) + token := util.CryptoRandomBytes(20) t.TokenSalt = salt t.Token = hex.EncodeToString(token) t.TokenHash = HashToken(t.Token, t.TokenSalt) t.TokenLastEight = t.Token[len(t.Token)-8:] - _, err = db.GetEngine(ctx).Insert(t) + _, err := db.GetEngine(ctx).Insert(t) return err } diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index e2bb72b722..104a7ba9f4 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -5,9 +5,8 @@ package auth import ( "context" - "crypto/sha256" + "crypto/subtle" "encoding/base32" - "encoding/base64" "errors" "fmt" "net" @@ -24,6 +23,7 @@ import ( uuid "github.com/google/uuid" "golang.org/x/crypto/bcrypt" + "golang.org/x/oauth2" "xorm.io/builder" "xorm.io/xorm" ) @@ -31,7 +31,10 @@ import ( // Authorization codes should expire within 10 minutes per https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2 const oauth2AuthorizationCodeValidity = 10 * time.Minute -var ErrOAuth2AuthorizationCodeInvalidated = errors.New("oauth2 authorization code already invalidated") +var ( + ErrOAuth2AuthorizationCodeInvalidated = errors.New("oauth2 authorization code already invalidated") + ErrOAuth2GrantStaleCounter = errors.New("oauth2 grant state changed during token refresh") +) // OAuth2Application represents an OAuth2 client (RFC 6749) type OAuth2Application struct { @@ -151,30 +154,40 @@ func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool { // https://www.rfc-editor.org/rfc/rfc6819#section-5.2.3.3 // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-12#section-3.1 - contains := func(s string) bool { - s = strings.TrimSuffix(strings.ToLower(s), "/") - for _, u := range app.RedirectURIs { - if strings.TrimSuffix(strings.ToLower(u), "/") == s { + redirectCandidates := []string{redirectURI} + if !app.ConfidentialClient { + loopbackRedirect, ok := normalizePublicClientRedirectURI(redirectURI) + if ok { + redirectCandidates = append(redirectCandidates, loopbackRedirect) + } + } + + for _, candidate := range redirectCandidates { + normalizedCandidate := normalizeRedirectURIForComparison(candidate) + for _, registeredURI := range app.RedirectURIs { + if normalizeRedirectURIForComparison(registeredURI) == normalizedCandidate { return true } } - return false } - if !app.ConfidentialClient { - uri, err := url.Parse(redirectURI) - // ignore port for http loopback uris following https://datatracker.ietf.org/doc/html/rfc8252#section-7.3 - if err == nil && uri.Scheme == "http" && uri.Port() != "" { - ip := net.ParseIP(uri.Hostname()) - if ip != nil && ip.IsLoopback() { - // strip port - uri.Host = uri.Hostname() - if contains(uri.String()) { - return true - } - } - } + + return false +} + +func normalizeRedirectURIForComparison(redirectURI string) string { + return strings.TrimSuffix(util.ToLowerASCII(redirectURI), "/") +} + +func normalizePublicClientRedirectURI(redirectURI string) (string, bool) { + parsedURI, err := url.Parse(redirectURI) + if err != nil || parsedURI.Scheme != "http" || parsedURI.Port() == "" { + return "", false } - return contains(redirectURI) + if ip := net.ParseIP(parsedURI.Hostname()); ip == nil || !ip.IsLoopback() { + return "", false + } + parsedURI.Host = parsedURI.Hostname() + return parsedURI.String(), true } // Base32 characters, but lowercased. @@ -185,10 +198,7 @@ var base32Lower = base32.NewEncoding(lowerBase32Chars).WithPadding(base32.NoPadd // GenerateClientSecret will generate the client secret and returns the plaintext and saves the hash at the database func (app *OAuth2Application) GenerateClientSecret(ctx context.Context) (string, error) { - rBytes, err := util.CryptoRandomBytes(32) - if err != nil { - return "", err - } + rBytes := util.CryptoRandomBytes(32) // Add a prefix to the base32, this is in order to make it easier // for code scanners to grab sensitive tokens. clientSecret := "gto_" + base32Lower.EncodeToString(rBytes) @@ -220,7 +230,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, @@ -427,22 +437,34 @@ func (code *OAuth2AuthorizationCode) Invalidate(ctx context.Context) error { return nil } +func (code *OAuth2AuthorizationCode) requiresCodeVerifier() bool { + return code.CodeChallengeMethod != "" || code.CodeChallenge != "" +} + +func deriveCodeChallenge(method, verifier string) (string, bool) { + switch method { + case "S256": + return oauth2.S256ChallengeFromVerifier(verifier), true + case "plain": + return verifier, true + default: + return "", false + } +} + // ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation. func (code *OAuth2AuthorizationCode) ValidateCodeChallenge(verifier string) bool { - switch code.CodeChallengeMethod { - case "S256": - // base64url(SHA256(verifier)) see https://tools.ietf.org/html/rfc7636#section-4.6 - h := sha256.Sum256([]byte(verifier)) - hashedVerifier := base64.RawURLEncoding.EncodeToString(h[:]) - return hashedVerifier == code.CodeChallenge - case "plain": - return verifier == code.CodeChallenge - case "": + if !code.requiresCodeVerifier() { return true - default: - // unsupported method -> return false + } + if verifier == "" || code.CodeChallengeMethod == "" { return false } + expectedChallenge, ok := deriveCodeChallenge(code.CodeChallengeMethod, verifier) + if !ok { + return false + } + return subtle.ConstantTimeCompare([]byte(expectedChallenge), []byte(code.CodeChallenge)) == 1 } // GetOAuth2AuthorizationByCode returns an authorization by its code @@ -464,7 +486,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)"` @@ -484,10 +506,7 @@ func (grant *OAuth2Grant) TableName() string { // GenerateNewAuthorizationCode generates a new authorization code for a grant and saves it to the database func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redirectURI, codeChallenge, codeChallengeMethod string) (code *OAuth2AuthorizationCode, err error) { - rBytes, err := util.CryptoRandomBytes(32) - if err != nil { - return &OAuth2AuthorizationCode{}, err - } + rBytes := util.CryptoRandomBytes(32) // Add a prefix to the base32, this is in order to make it easier // for code scanners to grab sensitive tokens. codeSecret := "gta_" + base32Lower.EncodeToString(rBytes) @@ -510,15 +529,18 @@ func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redi // IncreaseCounter increases the counter and updates the grant func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error { - _, err := db.GetEngine(ctx).ID(grant.ID).Incr("counter").Update(new(OAuth2Grant)) + affected, err := db.GetEngine(ctx). + Where("id = ?", grant.ID). + And("counter = ?", grant.Counter). + Incr("counter"). + Update(new(OAuth2Grant)) if err != nil { return err } - updatedGrant, err := GetOAuth2GrantByID(ctx, grant.ID) - if err != nil { - return err + if affected == 0 { + return ErrOAuth2GrantStaleCounter } - grant.Counter = updatedGrant.Counter + grant.Counter++ return nil } @@ -633,7 +655,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 88ae065652..465dfb14f2 100644 --- a/models/auth/oauth2_test.go +++ b/models/auth/oauth2_test.go @@ -12,19 +12,31 @@ import ( "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" ) -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 +46,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) }) } @@ -104,6 +117,47 @@ func TestOAuth2Application_ContainsRedirect_Slash(t *testing.T) { assert.False(t, app.ContainsRedirectURI("http://127.0.0.1/other")) } +func TestOAuth2Application_ContainsRedirectURI_ASCIIOnlyNormalization(t *testing.T) { + testCases := []struct { + name string + registered []string + redirectURI string + allowed bool + }{ + { + name: "exact-match", + registered: []string{"https://signin.example.test/callback"}, + redirectURI: "https://signin.example.test/callback", + allowed: true, + }, + { + name: "ascii-case-insensitive", + registered: []string{"https://signin.example.test/callback"}, + redirectURI: "https://signIN.example.test/callback", + allowed: true, + }, + { + name: "non-ascii-not-folded", + registered: []string{"https://signin.example.test/callback"}, + redirectURI: "https://signİn.example.test/callback", + allowed: false, + }, + { + name: "loopback-strips-port", + registered: []string{"http://127.0.0.1/callback"}, + redirectURI: "http://127.0.0.1:12345/callback", + allowed: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + app := &auth_model.OAuth2Application{RedirectURIs: tc.registered} + assert.Equal(t, tc.allowed, app.ContainsRedirectURI(tc.redirectURI)) + }) + } +} + func TestOAuth2Application_ValidateClientSecret(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) @@ -181,6 +235,16 @@ func TestOAuth2Grant_IncreaseCounter(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 2}) } +func TestOAuth2Grant_IncreaseCounterRejectsStaleCounter(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 1}) + stale := *grant + + assert.NoError(t, grant.IncreaseCounter(t.Context())) + err := stale.IncreaseCounter(t.Context()) + assert.ErrorIs(t, err, auth_model.ErrOAuth2GrantStaleCounter) +} + func TestOAuth2Grant_ScopeContains(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Scope: "openid profile"}) @@ -224,49 +288,39 @@ 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{ - CodeChallengeMethod: "plain", - CodeChallenge: "test123", - } - assert.True(t, code.ValidateCodeChallenge("test123")) - assert.False(t, code.ValidateCodeChallenge("ierwgjoergjio")) + s256Verifier := "s256-verifier" + s256Challenge := oauth2.S256ChallengeFromVerifier(s256Verifier) + missingVerifierChallenge := oauth2.S256ChallengeFromVerifier("verifier-not-supplied") - // test S256 - code = &auth_model.OAuth2AuthorizationCode{ - CodeChallengeMethod: "S256", - CodeChallenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", + testCases := []struct { + name string + method string + challenge string + verifier string + valid bool + }{ + {"plain-success", "plain", "plain-secret", "plain-secret", true}, + {"plain-failure", "plain", "plain-secret", "ierwgjoergjio", false}, + {"s256-success", "S256", s256Challenge, s256Verifier, true}, + {"s256-failure", "S256", s256Challenge, "wiogjerogorewngoenrgoiuenorg", false}, + {"unsupported-method", "monkey", "foiwgjioriogeiogjerger", "foiwgjioriogeiogjerger", false}, + {"no-pkce-configured", "", "", "", true}, + {"s256-missing-verifier", "S256", missingVerifierChallenge, "", false}, + {"plain-missing-verifier", "plain", "plain-secret", "", false}, + {"missing-method-with-challenge", "", "foierjiogerogerg", "", false}, + {"missing-method-rejects-even-matching-verifier", "", "foierjiogerogerg", "foierjiogerogerg", false}, } - assert.True(t, code.ValidateCodeChallenge("N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt")) - assert.False(t, code.ValidateCodeChallenge("wiogjerogorewngoenrgoiuenorg")) - // test unknown - code = &auth_model.OAuth2AuthorizationCode{ - CodeChallengeMethod: "monkey", - CodeChallenge: "foiwgjioriogeiogjerger", + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + code := &auth_model.OAuth2AuthorizationCode{ + CodeChallengeMethod: tc.method, + CodeChallenge: tc.challenge, + } + assert.Equal(t, tc.valid, code.ValidateCodeChallenge(tc.verifier)) + }) } - assert.False(t, code.ValidateCodeChallenge("foiwgjioriogeiogjerger")) - - // test no code challenge - code = &auth_model.OAuth2AuthorizationCode{ - CodeChallengeMethod: "", - CodeChallenge: "foierjiogerogerg", - } - assert.True(t, code.ValidateCodeChallenge("")) } func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) { @@ -284,13 +338,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/source.go b/models/auth/source.go index 7a008f08a8..16f36eb0c2 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -100,7 +100,7 @@ var registeredConfigs = map[Type]func() Config{} // RegisterTypeConfig register a config for a provided type func RegisterTypeConfig(typ Type, exemplar Config) { - if reflect.TypeOf(exemplar).Kind() == reflect.Ptr { + if reflect.TypeOf(exemplar).Kind() == reflect.Pointer { // Pointer: registeredConfigs[typ] = func() Config { return reflect.New(reflect.ValueOf(exemplar).Elem().Type()).Interface().(Config) diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go index 4263495650..80c34ba6ad 100644 --- a/models/auth/twofactor.go +++ b/models/auth/twofactor.go @@ -65,14 +65,11 @@ func init() { // GenerateScratchToken recreates the scratch token the user is using. func (t *TwoFactor) GenerateScratchToken() (string, error) { - tokenBytes, err := util.CryptoRandomBytes(6) - if err != nil { - return "", err - } + tokenBytes := util.CryptoRandomBytes(6) // these chars are specially chosen, avoid ambiguous chars like `0`, `O`, `1`, `I`. const base32Chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" token := base32.NewEncoding(base32Chars).WithPadding(base32.NoPadding).EncodeToString(tokenBytes) - t.ScratchSalt, _ = util.CryptoRandomString(10) + t.ScratchSalt = util.CryptoRandomString(10) t.ScratchHash = HashToken(token, t.ScratchSalt) return token, nil } diff --git a/models/auth/webauthn.go b/models/auth/webauthn.go index 6d8b542957..7bd79ed3f5 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/db/conn.go b/models/db/conn.go new file mode 100644 index 0000000000..944b6e7b97 --- /dev/null +++ b/models/db/conn.go @@ -0,0 +1,187 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db + +import ( + "errors" + "fmt" + "net" + "net/url" + "os" + "path/filepath" + "slices" + "strings" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +type ConnOptions struct { + Type setting.DatabaseType + Host string + Database string + User string + Passwd string + Schema string + SSLMode string + + SQLitePath string + SQLiteBusyTimeout int + SQLiteJournalMode string +} + +type SQLiteConnStrOptions struct { + FilePath string + // how long a concurrent query can wait for others (milliseconds), + // if timeout is reached, the error is something like "database is locked (SQLITE_BUSY)" + BusyTimeout int + JournalMode string +} + +func GlobalConnOptions() ConnOptions { + return ConnOptions{ + Type: setting.Database.Type, + Host: setting.Database.Host, + Database: setting.Database.Name, + User: setting.Database.User, + Passwd: setting.Database.Passwd, + Schema: setting.Database.Schema, + SSLMode: setting.Database.SSLMode, + + SQLitePath: setting.Database.Path, + SQLiteBusyTimeout: setting.Database.SQLiteBusyTimeout, + SQLiteJournalMode: setting.Database.SQLiteJournalMode, + } +} + +const ( + sqlDriverPostgresSchema = "postgresschema" + sqlDriverSQLite3 = "sqlite3" // although database type also has "sqlite3", they are different, for different purposes +) + +var makeSQLiteConnStr = func(opts SQLiteConnStrOptions) (string, string, error) { + return "", "", errors.New(`this Gitea binary was not built with SQLite3 support, get an official release or rebuild with correct "-tags"`) +} + +func registerSQLiteConnStrMaker(fn func(opts SQLiteConnStrOptions) (string, string, error)) { + if slices.Contains(setting.SupportedDatabaseTypes, setting.DatabaseTypeSQLite3) { + panic("another sqlite3 driver has been registered") + } + setting.SupportedDatabaseTypes = append(setting.SupportedDatabaseTypes, setting.DatabaseTypeSQLite3) + makeSQLiteConnStr = fn +} + +func ConnStrDefaultDatabase(opts ConnOptions) (string, string, error) { + opts.Database, opts.Schema = "", "" + return ConnStr(opts) +} + +func ConnStr(opts ConnOptions) (string, string, error) { + switch { + case opts.Type.IsMySQL(): + // use unix socket or tcp socket + connType := util.Iif(strings.HasPrefix(opts.Host, "/"), "unix", "tcp") + // allow (Postgres-inspired) default value to work in MySQL + tls := util.Iif(opts.SSLMode == "disable", "false", opts.SSLMode) + // in case the database name is a partial connection string which contains "?" parameters + paramSep := util.Iif(strings.Contains(opts.Database, "?"), "&", "?") + connStr := fmt.Sprintf("%s:%s@%s(%s)/%s%sparseTime=true&tls=%s", opts.User, opts.Passwd, connType, opts.Host, opts.Database, paramSep, tls) + return "mysql", connStr, nil + + case opts.Type.IsPostgreSQL(): + connStr := makePgSQLConnStr(opts.Host, opts.User, opts.Passwd, opts.Database, opts.SSLMode) + driver := util.Iif(opts.Schema == "", "postgres", sqlDriverPostgresSchema) + registerPostgresSchemaDriver() + return driver, connStr, nil + + case opts.Type.IsMSSQL(): + host, port := parseMSSQLHostPort(opts.Host) + connStr := fmt.Sprintf("server=%s; port=%s; user id=%s; password=%s;", host, port, opts.User, opts.Passwd) + if opts.Database != "" { + connStr += "; database=" + opts.Database + } + return "mssql", connStr, nil + + case opts.Type.IsSQLite3(): + if opts.SQLitePath == "" { + return "", "", errors.New("sqlite3 database path cannot be empty") + } + if err := os.MkdirAll(filepath.Dir(opts.SQLitePath), os.ModePerm); err != nil { + return "", "", fmt.Errorf("failed to create directories: %w", err) + } + return makeSQLiteConnStr(SQLiteConnStrOptions{ + FilePath: opts.SQLitePath, + JournalMode: opts.SQLiteJournalMode, + BusyTimeout: opts.SQLiteBusyTimeout, + }) + } + return "", "", fmt.Errorf("unknown database type: %s", opts.Type) +} + +// parsePgSQLHostPort parses given input in various forms defined in +// https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING +// and returns proper host and port number. +func parsePgSQLHostPort(info string) (host, port string) { + if h, p, err := net.SplitHostPort(info); err == nil { + host, port = h, p + } else { + // treat the "info" as "host", if it's an IPv6 address, remove the wrapper + host = info + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + host = host[1 : len(host)-1] + } + } + + // set fallback values + if host == "" { + host = "127.0.0.1" + } + if port == "" { + port = "5432" + } + return host, port +} + +func makePgSQLConnStr(dbHost, dbUser, dbPasswd, dbName, dbsslMode string) (connStr string) { + dbName, dbParam, _ := strings.Cut(dbName, "?") + host, port := parsePgSQLHostPort(dbHost) + connURL := url.URL{ + Scheme: "postgres", + User: url.UserPassword(dbUser, dbPasswd), + Host: net.JoinHostPort(host, port), + Path: dbName, + OmitHost: false, + RawQuery: dbParam, + } + query := connURL.Query() + if strings.HasPrefix(host, "/") { // looks like a unix socket + query.Add("host", host) + connURL.Host = ":" + port + } + query.Set("sslmode", dbsslMode) + connURL.RawQuery = query.Encode() + return connURL.String() +} + +// parseMSSQLHostPort splits the host into host and port +func parseMSSQLHostPort(info string) (string, string) { + // the default port "0" might be related to MSSQL's dynamic port, maybe it should be double-confirmed in the future + host, port := "127.0.0.1", "0" + if strings.Contains(info, ":") { + host = strings.Split(info, ":")[0] + port = strings.Split(info, ":")[1] + } else if strings.Contains(info, ",") { + host = strings.Split(info, ",")[0] + port = strings.TrimSpace(strings.Split(info, ",")[1]) + } else if len(info) > 0 { + host = info + } + if host == "" { + host = "127.0.0.1" + } + if port == "" { + port = "0" + } + return host, port +} diff --git a/modules/setting/database_test.go b/models/db/conn_test.go similarity index 88% rename from modules/setting/database_test.go rename to models/db/conn_test.go index a742d54f8c..ba33d252f2 100644 --- a/modules/setting/database_test.go +++ b/models/db/conn_test.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package setting +package db import ( "testing" @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_parsePostgreSQLHostPort(t *testing.T) { +func TestParsePgSQLHostPort(t *testing.T) { tests := map[string]struct { HostPort string Host string @@ -49,14 +49,14 @@ func Test_parsePostgreSQLHostPort(t *testing.T) { for k, test := range tests { t.Run(k, func(t *testing.T) { t.Log(test.HostPort) - host, port := parsePostgreSQLHostPort(test.HostPort) + host, port := parsePgSQLHostPort(test.HostPort) assert.Equal(t, test.Host, host) assert.Equal(t, test.Port, port) }) } } -func Test_getPostgreSQLConnectionString(t *testing.T) { +func TestMakePgSQLConnStr(t *testing.T) { tests := []struct { Host string User string @@ -103,7 +103,7 @@ func Test_getPostgreSQLConnectionString(t *testing.T) { } for _, test := range tests { - connStr := getPostgreSQLConnectionString(test.Host, test.User, test.Passwd, test.Name, test.SSLMode) + connStr := makePgSQLConnStr(test.Host, test.User, test.Passwd, test.Name, test.SSLMode) assert.Equal(t, test.Output, connStr) } } diff --git a/models/db/context.go b/models/db/context.go index 8bb14f1389..5e74f458ec 100644 --- a/models/db/context.go +++ b/models/db/context.go @@ -17,12 +17,15 @@ import ( "xorm.io/xorm" ) -type engineContextKeyType struct{} +type contextKey struct{ key string } -var engineContextKey = engineContextKeyType{} +var ( + contextKeyEngine = contextKey{"engine"} + ContextKeyTestFixtures = contextKey{"test-fixtures"} +) func withContextEngine(ctx context.Context, e Engine) context.Context { - return context.WithValue(ctx, engineContextKey, e) + return context.WithValue(ctx, contextKeyEngine, e) } var ( @@ -68,7 +71,7 @@ func contextSafetyCheck(e Engine) { // GetEngine gets an existing db Engine/Statement or creates a new Session func GetEngine(ctx context.Context) Engine { - if engine, ok := ctx.Value(engineContextKey).(Engine); ok { + if engine, ok := ctx.Value(contextKeyEngine).(Engine); ok { // if reusing the existing session, need to do "contextSafetyCheck" because the Iterate creates a "autoResetStatement=false" session contextSafetyCheck(engine) return engine @@ -309,7 +312,7 @@ func InTransaction(ctx context.Context) bool { } func getTransactionSession(ctx context.Context) *xorm.Session { - e, _ := ctx.Value(engineContextKey).(Engine) + e, _ := ctx.Value(contextKeyEngine).(Engine) if sess, ok := e.(*xorm.Session); ok && sess.IsInTx() { return sess } diff --git a/models/db/sql_postgres_with_schema.go b/models/db/driver_postgresschema.go similarity index 92% rename from models/db/sql_postgres_with_schema.go rename to models/db/driver_postgresschema.go index 812fe4a6a6..b673500763 100644 --- a/models/db/sql_postgres_with_schema.go +++ b/models/db/driver_postgresschema.go @@ -18,8 +18,8 @@ var registerOnce sync.Once func registerPostgresSchemaDriver() { registerOnce.Do(func() { - sql.Register("postgresschema", &postgresSchemaDriver{}) - dialects.RegisterDriver("postgresschema", dialects.QueryDriver("postgres")) + sql.Register(sqlDriverPostgresSchema, &postgresSchemaDriver{}) + dialects.RegisterDriver(sqlDriverPostgresSchema, dialects.QueryDriver("postgres")) }) } diff --git a/models/db/driver_sqlite_mattn.go b/models/db/driver_sqlite_mattn.go new file mode 100644 index 0000000000..a25cb09990 --- /dev/null +++ b/models/db/driver_sqlite_mattn.go @@ -0,0 +1,31 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build sqlite_mattn && sqlite_unlock_notify + +package db + +import ( + "fmt" + "strconv" + "strings" + + _ "github.com/mattn/go-sqlite3" +) + +func init() { + registerSQLiteConnStrMaker(makeSQLiteConnStrMattnCGO) +} + +func makeSQLiteConnStrMattnCGO(opts SQLiteConnStrOptions) (string, string, error) { + var params []string + params = append(params, "cache=shared") + params = append(params, "mode=rwc") + params = append(params, "_busy_timeout="+strconv.Itoa(opts.BusyTimeout)) + params = append(params, "_txlock=immediate") + if opts.JournalMode != "" { + params = append(params, "_journal_mode="+opts.JournalMode) + } + connStr := fmt.Sprintf("file:%s?%s", opts.FilePath, strings.Join(params, "&")) + return sqlDriverSQLite3, connStr, nil +} diff --git a/models/db/driver_sqlite_modernc.go b/models/db/driver_sqlite_modernc.go new file mode 100644 index 0000000000..6bd5498a55 --- /dev/null +++ b/models/db/driver_sqlite_modernc.go @@ -0,0 +1,41 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build !sqlite_mattn + +// modernc driver is chosen as the default one (compared to mattn, ncruces) +// * mattn was used as default, but it requires CGO +// * the CI times are almost the same for these three (race detector must be disabled) +// * modernc increases the binary size about 2MB, ncruces increases about 7MB +// * compiling time: modernc is slightly slower than mattn, ncruces is the slowest + +package db + +import ( + "database/sql" + "fmt" + "strings" + + "modernc.org/sqlite" +) + +func init() { + // this driver contains huge amount of Golang code, so it is much slower when "-race" check is enabled. + registerSQLiteConnStrMaker(makeSQLiteConnStrModerncCCGO) + sql.Register(sqlDriverSQLite3, &sqlite.Driver{}) +} + +func makeSQLiteConnStrModerncCCGO(opts SQLiteConnStrOptions) (string, string, error) { + var params []string + // TODO: there is a changed behavior from mattn driver: + // * mattn driver can wait for pretty long time for concurrent accesses (not limited by the busy timeout) + // * but other drivers will report something like "database is locked (5) (SQLITE_BUSY)" if the timeout is reached + // Maybe we need to relax the busy timeout to a reasonable long time in the future + params = append(params, fmt.Sprintf("_pragma=busy_timeout(%d)", opts.BusyTimeout)) + params = append(params, "_txlock=immediate") + if opts.JournalMode != "" { + params = append(params, fmt.Sprintf("_pragma=journal_mode(%s)", opts.JournalMode)) + } + connStr := fmt.Sprintf("file:%s?%s", opts.FilePath, strings.Join(params, "&")) + return sqlDriverSQLite3, connStr, nil +} diff --git a/models/db/engine.go b/models/db/engine.go index b08799210e..fbcc3fa15e 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -11,11 +11,11 @@ import ( "reflect" "strings" - "xorm.io/xorm" - _ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver _ "github.com/lib/pq" // Needed for the Postgresql driver _ "github.com/microsoft/go-mssqldb" // Needed for the MSSQL driver + + "xorm.io/xorm" ) var ( diff --git a/models/db/engine_dump.go b/models/db/engine_dump.go index 63f2d4e093..1d8d555b44 100644 --- a/models/db/engine_dump.go +++ b/models/db/engine_dump.go @@ -3,10 +3,14 @@ package db -import "xorm.io/xorm/schemas" +import ( + "code.gitea.io/gitea/modules/setting" + + "xorm.io/xorm/schemas" +) // DumpDatabase dumps all data from database according the special database SQL syntax to file system. -func DumpDatabase(filePath, dbType string) error { +func DumpDatabase(filePath string, dbType setting.DatabaseType) error { var tbs []*schemas.Table for _, t := range registeredModels { t, err := xormEngine.TableInfo(t) diff --git a/models/db/engine_hook.go b/models/db/engine_hook.go index 8709a2c2a1..8d8ed3992c 100644 --- a/models/db/engine_hook.go +++ b/models/db/engine_hook.go @@ -22,11 +22,17 @@ type EngineHook struct { var _ contexts.Hook = (*EngineHook)(nil) func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { + if c.Ctx.Value(ContextKeyTestFixtures) != nil { + return c.Ctx, nil + } ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase) return ctx, nil } func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error { + if c.Ctx.Value(ContextKeyTestFixtures) != nil { + return nil + } span := gtprof.GetContextSpan(c.Ctx) if span != nil { // Do not record SQL parameters here: diff --git a/models/db/engine_init.go b/models/db/engine_init.go index ef5db3ff5e..65192d3327 100644 --- a/models/db/engine_init.go +++ b/models/db/engine_init.go @@ -6,7 +6,6 @@ package db import ( "context" "fmt" - "strings" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -24,31 +23,23 @@ func init() { // newXORMEngine returns a new XORM engine from the configuration func newXORMEngine() (*xorm.Engine, error) { - connStr, err := setting.DBConnStr() + connOpts := GlobalConnOptions() + driver, connStr, err := ConnStr(connOpts) if err != nil { return nil, err } - var engine *xorm.Engine - - if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 { - // OK whilst we sort out our schema issues - create a schema aware postgres - registerPostgresSchemaDriver() - engine, err = xorm.NewEngine("postgresschema", connStr) - } else { - engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr) - } - + engine, err := xorm.NewEngine(driver, connStr) if err != nil { return nil, err } - switch setting.Database.Type { - case "mysql": + switch { + case connOpts.Type.IsMySQL(): engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"}) - case "mssql": + case connOpts.Type.IsMSSQL(): engine.Dialect().SetParams(map[string]string{"DEFAULT_VARCHAR": "nvarchar"}) } - engine.SetSchema(setting.Database.Schema) + engine.SetSchema(connOpts.Schema) return engine, nil } @@ -56,10 +47,7 @@ func newXORMEngine() (*xorm.Engine, error) { func InitEngine(ctx context.Context) error { xe, err := newXORMEngine() if err != nil { - if strings.Contains(err.Error(), "SQLite3 support") { - return fmt.Errorf("sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n%w", err) - } - return fmt.Errorf("failed to connect to database: %w", err) + return fmt.Errorf("failed to init database engine: %w", err) } xe.SetMapper(names.GonicMapper{}) diff --git a/models/db/engine_test.go b/models/db/engine_test.go index 1c218df77f..6a6264b535 100644 --- a/models/db/engine_test.go +++ b/models/db/engine_test.go @@ -30,7 +30,7 @@ func TestDumpDatabase(t *testing.T) { assert.NoError(t, db.GetEngine(t.Context()).Sync(new(Version))) for _, dbType := range setting.SupportedDatabaseTypes { - assert.NoError(t, db.DumpDatabase(filepath.Join(dir, dbType+".sql"), dbType)) + assert.NoError(t, db.DumpDatabase(filepath.Join(dir, dbType+".sql"), setting.DatabaseType(dbType))) } } diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml index 596046e950..c0aa06c86d 100644 --- a/models/fixtures/access.yml +++ b/models/fixtures/access.yml @@ -177,3 +177,5 @@ user_id: 40 repo_id: 1 mode: 2 + +# 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/access_token.yml b/models/fixtures/access_token.yml index 0744255f66..d85d785da5 100644 --- a/models/fixtures/access_token.yml +++ b/models/fixtures/access_token.yml @@ -31,3 +31,5 @@ created_unix: 946687980 updated_unix: 946687980 # commented out tokens so you can see what they are in plaintext + +# 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/action.yml b/models/fixtures/action.yml index af9ce93ba5..32f2ae8764 100644 --- a/models/fixtures/action.yml +++ b/models/fixtures/action.yml @@ -73,3 +73,5 @@ is_private: false created_unix: 1680454039 content: '4|' # issueId 5 + +# 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/action_artifact.yml b/models/fixtures/action_artifact.yml index ee8ef0d5ce..5fcc70aa53 100644 --- a/models/fixtures/action_artifact.yml +++ b/models/fixtures/action_artifact.yml @@ -141,3 +141,41 @@ created_unix: 1730330775 updated_unix: 1730330775 expired_unix: 1738106775 + +- + id: 26 + run_id: 792 + runner_id: 1 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/pdf" + artifact_path: "report.pdf" + artifact_name: "report.pdf" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 + +- + id: 27 + run_id: 792 + runner_id: 1 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/html" + artifact_path: "report.html" + artifact_name: "report.html" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 + +# 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/action_run.yml b/models/fixtures/action_run.yml index ac5e8303c3..1df02e4809 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 @@ -140,23 +140,4 @@ need_approval: 0 approved_by: 0 -- - id: 805 - title: "update actions" - repo_id: 4 - owner_id: 1 - workflow_id: "artifact.yaml" - index: 191 - trigger_user_id: 1 - ref: "refs/heads/master" - commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" - event: "push" - trigger_event: "push" - is_fork_pull_request: 0 - status: 5 - started: 1683636528 - stopped: 1683636626 - created: 1683636108 - updated: 1683636626 - need_approval: 0 - approved_by: 0 +# 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/action_run_job.yml b/models/fixtures/action_run_job.yml index 04799b73ca..2a4e64285f 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -130,17 +130,4 @@ started: 1683636528 stopped: 1683636626 -- - id: 206 - run_id: 805 - repo_id: 4 - owner_id: 1 - commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 - is_fork_pull_request: 0 - name: job_2 - attempt: 1 - job_id: job_2 - task_id: 56 - status: 3 - started: 1683636528 - stopped: 1683636626 +# 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/action_runner.yml b/models/fixtures/action_runner.yml index ecb7214006..110ff627a2 100644 --- a/models/fixtures/action_runner.yml +++ b/models/fixtures/action_runner.yml @@ -49,3 +49,5 @@ repo_id: 0 description: "This runner is going to be deleted" agent_labels: '["runner_to_be_deleted","linux"]' + +# 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/action_runner_token.yml b/models/fixtures/action_runner_token.yml index 6520b7f6fb..3af8a28c9c 100644 --- a/models/fixtures/action_runner_token.yml +++ b/models/fixtures/action_runner_token.yml @@ -33,3 +33,5 @@ is_active: 1 created: 1695617751 updated: 1695617751 + +# 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/action_task.yml b/models/fixtures/action_task.yml index e1bc588dc5..13efe378c4 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -178,22 +178,4 @@ log_size: 0 log_expired: 0 -- - id: 56 - attempt: 1 - runner_id: 1 - status: 3 # 3 is the status code for "cancelled" - started: 1683636528 - stopped: 1683636626 - repo_id: 4 - owner_id: 1 - commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 - is_fork_pull_request: 0 - token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4240c64a69a2cc1508825121b7b8394e48e00b1bf3718b2aaaab - token_salt: eeeeeeee - token_last_eight: eeeeeeee - log_filename: artifact-test2/2f/47.log - log_in_storage: 1 - log_length: 707 - log_size: 90179 - log_expired: 0 +# 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/action_task_output.yml b/models/fixtures/action_task_output.yml index 314e9f7115..741b193b13 100644 --- a/models/fixtures/action_task_output.yml +++ b/models/fixtures/action_task_output.yml @@ -18,3 +18,5 @@ task_id: 50 output_key: output_b output_value: bbb + +# 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/attachment.yml b/models/fixtures/attachment.yml index 570d4a27da..0870895a71 100644 --- a/models/fixtures/attachment.yml +++ b/models/fixtures/attachment.yml @@ -166,3 +166,5 @@ download_count: 0 size: 0 created_unix: 946684800 + +# 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/badge.yml b/models/fixtures/badge.yml index 438cd0ca5d..72550be79a 100644 --- a/models/fixtures/badge.yml +++ b/models/fixtures/badge.yml @@ -3,3 +3,5 @@ slug: badge1 description: just a test badge image_url: badge1.png + +# 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/branch.yml b/models/fixtures/branch.yml index a17999091e..e09022a614 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -249,3 +249,5 @@ is_deleted: false deleted_by_id: 0 deleted_unix: 0 + +# 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/collaboration.yml b/models/fixtures/collaboration.yml index 4c3ac367f6..2de3448809 100644 --- a/models/fixtures/collaboration.yml +++ b/models/fixtures/collaboration.yml @@ -63,3 +63,5 @@ repo_id: 32 user_id: 10 mode: 2 # write + +# 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/comment.yml b/models/fixtures/comment.yml index 8fde386e22..6930dbb58e 100644 --- a/models/fixtures/comment.yml +++ b/models/fixtures/comment.yml @@ -102,3 +102,5 @@ review_id: 22 assignee_id: 5 created_unix: 946684817 + +# 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/commit_status.yml b/models/fixtures/commit_status.yml index 87c652e53a..df3bc42505 100644 --- a/models/fixtures/commit_status.yml +++ b/models/fixtures/commit_status.yml @@ -57,3 +57,5 @@ context: deploy/awesomeness context_hash: ae9547713a6665fc4261d0756904932085a41cf2 creator_id: 2 + +# 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/commit_status_index.yml b/models/fixtures/commit_status_index.yml index f63343b042..9157911bac 100644 --- a/models/fixtures/commit_status_index.yml +++ b/models/fixtures/commit_status_index.yml @@ -3,3 +3,5 @@ repo_id: 1 sha: "1234123412341234123412341234123412341234" max_index: 5 + +# 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/deploy_key.yml b/models/fixtures/deploy_key.yml index ca780a73aa..0d1b2f0098 100644 --- a/models/fixtures/deploy_key.yml +++ b/models/fixtures/deploy_key.yml @@ -1 +1,3 @@ [] # empty + +# 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/email_address.yml b/models/fixtures/email_address.yml index 0f6bd9ee6d..b3c78120af 100644 --- a/models/fixtures/email_address.yml +++ b/models/fixtures/email_address.yml @@ -317,3 +317,5 @@ lower_email: user40@example.com is_activated: true is_primary: true + +# 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/external_login_user.yml b/models/fixtures/external_login_user.yml index ca780a73aa..0d1b2f0098 100644 --- a/models/fixtures/external_login_user.yml +++ b/models/fixtures/external_login_user.yml @@ -1 +1,3 @@ [] # empty + +# 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/follow.yml b/models/fixtures/follow.yml index b8d35828bf..f8de0e039d 100644 --- a/models/fixtures/follow.yml +++ b/models/fixtures/follow.yml @@ -17,3 +17,5 @@ id: 4 user_id: 31 follow_id: 33 + +# 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/gpg_key.yml b/models/fixtures/gpg_key.yml index 2d54313fdf..3d2895dc1c 100644 --- a/models/fixtures/gpg_key.yml +++ b/models/fixtures/gpg_key.yml @@ -21,3 +21,5 @@ can_encrypt_comms: true can_encrypt_storage: true can_certify: true + +# 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/gpg_key_import.yml b/models/fixtures/gpg_key_import.yml index ca780a73aa..0d1b2f0098 100644 --- a/models/fixtures/gpg_key_import.yml +++ b/models/fixtures/gpg_key_import.yml @@ -1 +1,3 @@ [] # empty + +# 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/hook_task.yml b/models/fixtures/hook_task.yml index 6023719b1e..01918b35ee 100644 --- a/models/fixtures/hook_task.yml +++ b/models/fixtures/hook_task.yml @@ -1,37 +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/issue.yml b/models/fixtures/issue.yml index ca5b1c6cd1..6da3c9e279 100644 --- a/models/fixtures/issue.yml +++ b/models/fixtures/issue.yml @@ -372,3 +372,5 @@ created_unix: 1707270422 updated_unix: 1707270422 is_locked: 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/fixtures/issue_assignees.yml b/models/fixtures/issue_assignees.yml index c40ecad676..a0bf422dc9 100644 --- a/models/fixtures/issue_assignees.yml +++ b/models/fixtures/issue_assignees.yml @@ -18,3 +18,5 @@ id: 5 assignee_id: 10 issue_id: 6 + +# 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/issue_index.yml b/models/fixtures/issue_index.yml index 5aabc08e38..5110068447 100644 --- a/models/fixtures/issue_index.yml +++ b/models/fixtures/issue_index.yml @@ -33,3 +33,5 @@ - group_id: 51 max_index: 1 + +# 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/issue_label.yml b/models/fixtures/issue_label.yml index f4ecb1f923..3754bd7828 100644 --- a/models/fixtures/issue_label.yml +++ b/models/fixtures/issue_label.yml @@ -17,3 +17,5 @@ id: 4 issue_id: 2 label_id: 4 + +# 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/issue_pin.yml b/models/fixtures/issue_pin.yml index 14b7a72d84..dc3d1c60d9 100644 --- a/models/fixtures/issue_pin.yml +++ b/models/fixtures/issue_pin.yml @@ -4,3 +4,5 @@ issue_id: 4 is_pull: false pin_order: 1 + +# 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/issue_user.yml b/models/fixtures/issue_user.yml index 64824316ea..756cb7be4b 100644 --- a/models/fixtures/issue_user.yml +++ b/models/fixtures/issue_user.yml @@ -18,3 +18,5 @@ issue_id: 1 is_read: false is_mentioned: true + +# 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/issue_watch.yml b/models/fixtures/issue_watch.yml index 4bc3ff1b8b..edc1041abc 100644 --- a/models/fixtures/issue_watch.yml +++ b/models/fixtures/issue_watch.yml @@ -29,3 +29,5 @@ is_watching: false created_unix: 946684800 updated_unix: 946684800 + +# 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/label.yml b/models/fixtures/label.yml index acfac74968..064f790a77 100644 --- a/models/fixtures/label.yml +++ b/models/fixtures/label.yml @@ -107,3 +107,5 @@ num_issues: 0 num_closed_issues: 0 archived_unix: 0 + +# 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/lfs_meta_object.yml b/models/fixtures/lfs_meta_object.yml index ae5ae56542..0fe430f147 100644 --- a/models/fixtures/lfs_meta_object.yml +++ b/models/fixtures/lfs_meta_object.yml @@ -30,3 +30,5 @@ size: 25 repository_id: 54 created_unix: 1671607299 + +# 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/login_source.yml b/models/fixtures/login_source.yml index ca780a73aa..0d1b2f0098 100644 --- a/models/fixtures/login_source.yml +++ b/models/fixtures/login_source.yml @@ -1 +1,3 @@ [] # empty + +# 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/milestone.yml b/models/fixtures/milestone.yml index 87c30cc96c..c4ed2aea78 100644 --- a/models/fixtures/milestone.yml +++ b/models/fixtures/milestone.yml @@ -52,3 +52,5 @@ num_closed_issues: 0 completeness: 0 deadline_unix: 253370764800 + +# 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/mirror.yml b/models/fixtures/mirror.yml index 97bc4ae60d..1f690654cb 100644 --- a/models/fixtures/mirror.yml +++ b/models/fixtures/mirror.yml @@ -47,3 +47,5 @@ next_update_unix: 0 lfs_enabled: false lfs_endpoint: "" + +# 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/notice.yml b/models/fixtures/notice.yml index af08f07bfa..17e26d7634 100644 --- a/models/fixtures/notice.yml +++ b/models/fixtures/notice.yml @@ -12,3 +12,5 @@ id: 3 type: 1 # NoticeRepository description: description3 + +# 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/notification.yml b/models/fixtures/notification.yml index bd279d4bb2..dcfbeada39 100644 --- a/models/fixtures/notification.yml +++ b/models/fixtures/notification.yml @@ -52,3 +52,5 @@ issue_id: 4 created_unix: 946688800 updated_unix: 946688820 + +# 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 2f38cb58b6..3426ccffac 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 @@ -18,3 +18,5 @@ created_unix: 1546869730 updated_unix: 1546869730 confidential_client: 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/fixtures/oauth2_authorization_code.yml b/models/fixtures/oauth2_authorization_code.yml index d29502164e..01918b35ee 100644 --- a/models/fixtures/oauth2_authorization_code.yml +++ b/models/fixtures/oauth2_authorization_code.yml @@ -1,15 +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/oauth2_grant.yml b/models/fixtures/oauth2_grant.yml index e63286878b..54f4e45e62 100644 --- a/models/fixtures/oauth2_grant.yml +++ b/models/fixtures/oauth2_grant.yml @@ -29,3 +29,5 @@ scope: "whatever" created_unix: 1546869730 updated_unix: 1546869730 + +# 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/org_user.yml b/models/fixtures/org_user.yml index 73a3e9dba9..dc35701182 100644 --- a/models/fixtures/org_user.yml +++ b/models/fixtures/org_user.yml @@ -135,3 +135,5 @@ uid: 20 org_id: 17 is_public: 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/fixtures/project.yml b/models/fixtures/project.yml index 44d87bce04..e61781fd7f 100644 --- a/models/fixtures/project.yml +++ b/models/fixtures/project.yml @@ -69,3 +69,5 @@ type: 2 created_unix: 1688973000 updated_unix: 1688973000 + +# 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/project_board.yml b/models/fixtures/project_board.yml index 3293dea6ed..91d2198171 100644 --- a/models/fixtures/project_board.yml +++ b/models/fixtures/project_board.yml @@ -75,3 +75,5 @@ default: true created_unix: 1588117528 updated_unix: 1588117528 + +# 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/project_issue.yml b/models/fixtures/project_issue.yml index b1af05908a..7d9d511882 100644 --- a/models/fixtures/project_issue.yml +++ b/models/fixtures/project_issue.yml @@ -21,3 +21,5 @@ issue_id: 5 project_id: 1 project_board_id: 3 + +# 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/protected_branch.yml b/models/fixtures/protected_branch.yml index ca780a73aa..0d1b2f0098 100644 --- a/models/fixtures/protected_branch.yml +++ b/models/fixtures/protected_branch.yml @@ -1 +1,3 @@ [] # empty + +# 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/protected_tag.yml b/models/fixtures/protected_tag.yml index 1944e7bd84..cb83439645 100644 --- a/models/fixtures/protected_tag.yml +++ b/models/fixtures/protected_tag.yml @@ -22,3 +22,5 @@ allowlist_team_i_ds: "[]" created_unix: 1715596037 updated_unix: 1715596037 + +# 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/public_key.yml b/models/fixtures/public_key.yml index 856b0e3fb2..756bca86b6 100644 --- a/models/fixtures/public_key.yml +++ b/models/fixtures/public_key.yml @@ -10,3 +10,5 @@ updated_unix: 1565224552 login_source_id: 0 verified: 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/fixtures/pull_request.yml b/models/fixtures/pull_request.yml index 9a16316e5a..b8da7fe081 100644 --- a/models/fixtures/pull_request.yml +++ b/models/fixtures/pull_request.yml @@ -117,3 +117,5 @@ index: 1 head_repo_id: 61 base_repo_id: 61 + +# 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/reaction.yml b/models/fixtures/reaction.yml index ee571a73a4..9effcc98f7 100644 --- a/models/fixtures/reaction.yml +++ b/models/fixtures/reaction.yml @@ -37,3 +37,5 @@ comment_id: 2 user_id: 1 created_unix: 1573248005 + +# 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/release.yml b/models/fixtures/release.yml index 372a79509f..be44e331ec 100644 --- a/models/fixtures/release.yml +++ b/models/fixtures/release.yml @@ -150,3 +150,5 @@ is_prerelease: false is_tag: false created_unix: 946684803 + +# 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/renamed_branch.yml b/models/fixtures/renamed_branch.yml index efa5130a2b..9055080ff8 100644 --- a/models/fixtures/renamed_branch.yml +++ b/models/fixtures/renamed_branch.yml @@ -3,3 +3,5 @@ repo_id: 1 from: dev to: master + +# 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/repo_archiver.yml b/models/fixtures/repo_archiver.yml index ca780a73aa..0d1b2f0098 100644 --- a/models/fixtures/repo_archiver.yml +++ b/models/fixtures/repo_archiver.yml @@ -1 +1,3 @@ [] # empty + +# 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/repo_indexer_status.yml b/models/fixtures/repo_indexer_status.yml index ca780a73aa..0d1b2f0098 100644 --- a/models/fixtures/repo_indexer_status.yml +++ b/models/fixtures/repo_indexer_status.yml @@ -1 +1,3 @@ [] # empty + +# 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/repo_license.yml b/models/fixtures/repo_license.yml index ca780a73aa..0d1b2f0098 100644 --- a/models/fixtures/repo_license.yml +++ b/models/fixtures/repo_license.yml @@ -1 +1,3 @@ [] # empty + +# 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/repo_redirect.yml b/models/fixtures/repo_redirect.yml index 8850c8d780..60459d638d 100644 --- a/models/fixtures/repo_redirect.yml +++ b/models/fixtures/repo_redirect.yml @@ -3,3 +3,5 @@ owner_id: 2 lower_name: oldrepo1 redirect_repo_id: 1 + +# 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/repo_topic.yml b/models/fixtures/repo_topic.yml index f166faccc1..3a4e7edaa9 100644 --- a/models/fixtures/repo_topic.yml +++ b/models/fixtures/repo_topic.yml @@ -25,3 +25,5 @@ - repo_id: 2 topic_id: 6 + +# 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/repo_transfer.yml b/models/fixtures/repo_transfer.yml index b12e6b207f..0a26eaec8e 100644 --- a/models/fixtures/repo_transfer.yml +++ b/models/fixtures/repo_transfer.yml @@ -29,3 +29,5 @@ repo_id: 5 created_unix: 1553610671 updated_unix: 1553610671 + +# 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/repo_unit.yml b/models/fixtures/repo_unit.yml index 4c3e37500f..69f083ccd7 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -747,3 +747,5 @@ type: 10 config: "{}" created_unix: 946684810 + +# 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/repository.yml b/models/fixtures/repository.yml index dfa514db37..d8eb796207 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1788,3 +1788,5 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: 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/fixtures/review.yml b/models/fixtures/review.yml index 5b8bbceca9..abcc9d3bb2 100644 --- a/models/fixtures/review.yml +++ b/models/fixtures/review.yml @@ -214,3 +214,5 @@ original_author_id: 0 updated_unix: 946684817 created_unix: 946684817 + +# 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/star.yml b/models/fixtures/star.yml index 39b51b3736..96db493448 100644 --- a/models/fixtures/star.yml +++ b/models/fixtures/star.yml @@ -17,3 +17,5 @@ id: 4 uid: 10 repo_id: 32 + +# 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/stopwatch.yml b/models/fixtures/stopwatch.yml index b7919d6fbb..bbb3852069 100644 --- a/models/fixtures/stopwatch.yml +++ b/models/fixtures/stopwatch.yml @@ -9,3 +9,5 @@ user_id: 2 issue_id: 2 created_unix: 1500988002 + +# 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/system_setting.yml b/models/fixtures/system_setting.yml index dcad176c89..ae612fa9f9 100644 --- a/models/fixtures/system_setting.yml +++ b/models/fixtures/system_setting.yml @@ -13,3 +13,5 @@ version: 1 created: 1653533198 updated: 1653533198 + +# 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/team.yml b/models/fixtures/team.yml index b549d0589b..3c2cb7802a 100644 --- a/models/fixtures/team.yml +++ b/models/fixtures/team.yml @@ -261,3 +261,5 @@ num_members: 1 includes_all_repositories: true can_create_org_repo: 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/fixtures/team_repo.yml b/models/fixtures/team_repo.yml index a29078107e..c91f74467a 100644 --- a/models/fixtures/team_repo.yml +++ b/models/fixtures/team_repo.yml @@ -75,3 +75,5 @@ org_id: 41 team_id: 22 repo_id: 61 + +# 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/team_unit.yml b/models/fixtures/team_unit.yml index 110019eee3..bb95087009 100644 --- a/models/fixtures/team_unit.yml +++ b/models/fixtures/team_unit.yml @@ -340,3 +340,5 @@ team_id: 24 type: 1 # code access_mode: 2 + +# 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/team_user.yml b/models/fixtures/team_user.yml index 6b2d153278..4cceffee6a 100644 --- a/models/fixtures/team_user.yml +++ b/models/fixtures/team_user.yml @@ -159,3 +159,5 @@ org_id: 35 team_id: 24 uid: 2 + +# 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/topic.yml b/models/fixtures/topic.yml index 055addf510..97ac821fc1 100644 --- a/models/fixtures/topic.yml +++ b/models/fixtures/topic.yml @@ -27,3 +27,5 @@ id: 6 name: topicname2 repo_count: 2 + +# 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/tracked_time.yml b/models/fixtures/tracked_time.yml index 768af38d9e..7c2145a6d8 100644 --- a/models/fixtures/tracked_time.yml +++ b/models/fixtures/tracked_time.yml @@ -69,3 +69,5 @@ time: 100000 created_unix: 947688815 deleted: true + +# 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/two_factor.yml b/models/fixtures/two_factor.yml index d8cb85274b..13b421b7b4 100644 --- a/models/fixtures/two_factor.yml +++ b/models/fixtures/two_factor.yml @@ -7,3 +7,5 @@ last_used_passcode: created_unix: 1564253724 updated_unix: 1564253724 + +# 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/user.yml b/models/fixtures/user.yml index 976a236011..1a33947e04 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -1556,3 +1556,5 @@ repo_admin_change_team_access: false theme: "" keep_activity_private: 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/fixtures/user_blocking.yml b/models/fixtures/user_blocking.yml index 2ec9d99df5..c1714e40c8 100644 --- a/models/fixtures/user_blocking.yml +++ b/models/fixtures/user_blocking.yml @@ -17,3 +17,5 @@ id: 4 blocker_id: 50 blockee_id: 34 + +# 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/user_open_id.yml b/models/fixtures/user_open_id.yml index d3a367b99d..72fe7e34b8 100644 --- a/models/fixtures/user_open_id.yml +++ b/models/fixtures/user_open_id.yml @@ -15,3 +15,5 @@ uid: 2 uri: https://domain1.tld/user2/ show: true + +# 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/user_redirect.yml b/models/fixtures/user_redirect.yml index c668cb6c3b..1b0f7a9494 100644 --- a/models/fixtures/user_redirect.yml +++ b/models/fixtures/user_redirect.yml @@ -6,3 +6,5 @@ id: 2 lower_name: olduser2 redirect_user_id: 2 + +# 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/watch.yml b/models/fixtures/watch.yml index 18bcd2ed2b..b7ee121f24 100644 --- a/models/fixtures/watch.yml +++ b/models/fixtures/watch.yml @@ -39,3 +39,5 @@ user_id: 10 repo_id: 32 mode: 1 # normal + +# 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/webauthn_credential.yml b/models/fixtures/webauthn_credential.yml index bc43127fcd..d188a4d76a 100644 --- a/models/fixtures/webauthn_credential.yml +++ b/models/fixtures/webauthn_credential.yml @@ -7,3 +7,5 @@ clone_warning: false created_unix: 946684800 updated_unix: 946684800 + +# 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 ec282914b8..01918b35ee 100644 --- a/models/fixtures/webhook.yml +++ b/models/fixtures/webhook.yml @@ -1,52 +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.go b/models/git/branch.go index 698c43f147..db53ca0ac7 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -261,7 +261,7 @@ func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix"). Update(&Branch{ CommitID: commit.ID.String(), - CommitMessage: commit.Summary(), + CommitMessage: commit.MessageTitle(), PusherID: pusherID, CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), IsDeleted: false, diff --git a/models/git/branch_list.go b/models/git/branch_list.go index 25e84526d2..1445f3a5a0 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/git/branch_test.go b/models/git/branch_test.go index 3832df9350..be67439285 100644 --- a/models/git/branch_test.go +++ b/models/git/branch_test.go @@ -36,7 +36,7 @@ func TestAddDeletedBranch(t *testing.T) { commit := &git.Commit{ ID: git.MustIDFromString(secondBranch.CommitID), - CommitMessage: secondBranch.CommitMessage, + CommitMessage: git.CommitMessage{MessageRaw: secondBranch.CommitMessage}, Committer: &git.Signature{ When: secondBranch.CommitTime.AsLocalTime(), }, diff --git a/models/git/commit_status_test.go b/models/git/commit_status_test.go index f565550c53..f18d3470b0 100644 --- a/models/git/commit_status_test.go +++ b/models/git/commit_status_test.go @@ -139,7 +139,7 @@ func Test_CalcCommitStatus(t *testing.T) { }, }, expected: &git_model.CommitStatus{ - State: commitstatus.CommitStatusPending, + State: commitstatus.CommitStatusFailure, }, }, { diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index f4567a0aac..32014615e6 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -43,6 +43,9 @@ type ProtectedBranch struct { WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"` MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` + EnableBypassAllowlist bool `xorm:"NOT NULL DEFAULT false"` + BypassAllowlistUserIDs []int64 `xorm:"JSON TEXT"` + BypassAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` CanForcePush bool `xorm:"NOT NULL DEFAULT false"` EnableForcePushAllowlist bool `xorm:"NOT NULL DEFAULT false"` ForcePushAllowlistUserIDs []int64 `xorm:"JSON TEXT"` @@ -204,6 +207,29 @@ func IsUserMergeWhitelisted(ctx context.Context, protectBranch *ProtectedBranch, return in } +// CanBypassBranchProtection reports whether the user can bypass branch protection checks (status checks, approvals, protected files) +// Either a repo admin (when not blocked) or a user/team on the bypass allowlist can bypass. +func CanBypassBranchProtection(ctx context.Context, protectBranch *ProtectedBranch, user *user_model.User, isRepoAdmin bool) bool { + if isRepoAdmin && !protectBranch.BlockAdminMergeOverride { + return true + } + if !protectBranch.EnableBypassAllowlist { + return false + } + if slices.Contains(protectBranch.BypassAllowlistUserIDs, user.ID) { + return true + } + if len(protectBranch.BypassAllowlistTeamIDs) == 0 { + return false + } + in, err := organization.IsUserInTeams(ctx, user.ID, protectBranch.BypassAllowlistTeamIDs) + if err != nil { + log.Error("IsUserInTeams failed: userID=%d, repoID=%d, allowlistTeamIDs=%v, err=%v", user.ID, protectBranch.RepoID, protectBranch.BypassAllowlistTeamIDs, err) + return false + } + return in +} + // IsUserOfficialReviewer check if user is official reviewer for the branch (counts towards required approvals) func IsUserOfficialReviewer(ctx context.Context, protectBranch *ProtectedBranch, user *user_model.User) (bool, error) { repo, err := repo_model.GetRepositoryByID(ctx, protectBranch.RepoID) @@ -347,6 +373,9 @@ type WhitelistOptions struct { ApprovalsUserIDs []int64 ApprovalsTeamIDs []int64 + + BypassUserIDs []int64 + BypassTeamIDs []int64 } // UpdateProtectBranch saves branch protection options of repository. @@ -387,6 +416,12 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote } protectBranch.ApprovalsWhitelistUserIDs = whitelist + whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.BypassAllowlistUserIDs, opts.BypassUserIDs) + if err != nil { + return err + } + protectBranch.BypassAllowlistUserIDs = whitelist + // if the repo is in an organization whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.WhitelistTeamIDs, opts.TeamIDs) if err != nil { @@ -412,6 +447,12 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote } protectBranch.ApprovalsWhitelistTeamIDs = whitelist + whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.BypassAllowlistTeamIDs, opts.BypassTeamIDs) + if err != nil { + return err + } + protectBranch.BypassAllowlistTeamIDs = whitelist + // Looks like it's a new rule if protectBranch.ID == 0 { // as it's a new rule and if priority was not set, we need to calc it. @@ -495,9 +536,9 @@ func updateUserWhitelist(ctx context.Context, repo *repo_model.Repository, curre if err != nil { return nil, fmt.Errorf("GetUserByID [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err) } - perm, err := access_model.GetUserRepoPermission(ctx, repo, user) + perm, err := access_model.GetIndividualUserRepoPermission(ctx, repo, user) if err != nil { - return nil, fmt.Errorf("GetUserRepoPermission [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err) + return nil, fmt.Errorf("GetIndividualUserRepoPermission [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err) } if !perm.CanWrite(unit.TypeCode) { diff --git a/models/git/protected_branch_test.go b/models/git/protected_branch_test.go index 3aa1d7daa8..043e24cbe8 100644 --- a/models/git/protected_branch_test.go +++ b/models/git/protected_branch_test.go @@ -9,6 +9,7 @@ import ( "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" "github.com/stretchr/testify/assert" ) @@ -153,3 +154,51 @@ func TestNewProtectBranchPriority(t *testing.T) { assert.NoError(t, err) assert.Equal(t, int64(2), savedPB2.Priority) } + +func TestCanBypassBranchProtection(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // not in team 1 + teamMember := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + pb := &ProtectedBranch{ + EnableBypassAllowlist: true, + BypassAllowlistUserIDs: []int64{user.ID}, + } + + testBypass := func(t *testing.T, expected bool, pb *ProtectedBranch, doer *user_model.User, isAdmin bool) { + assert.Equal(t, expected, CanBypassBranchProtection(t.Context(), pb, doer, isAdmin)) + } + // User bypasses via explicit allowlist. + testBypass(t, true, pb, user, false) + + // Non-admin cannot bypass when allowlist is disabled. + pb.EnableBypassAllowlist = false + testBypass(t, false, pb, user, false) + + // Repo admin can bypass independently of allowlist when not blocked. + testBypass(t, true, pb, user, true) + + // Admin override block still allows bypass for allowlisted users. + pb.EnableBypassAllowlist = true + pb.BlockAdminMergeOverride = true + testBypass(t, true, pb, user, false) + + // admin cannot bypass without allowlist membership. + pb.BypassAllowlistUserIDs = nil + testBypass(t, false, pb, user, true) + + // admin can bypass when allowlisted. + pb.BypassAllowlistUserIDs = []int64{user.ID} + testBypass(t, true, pb, user, true) + + // User bypasses via team allowlist membership. + pb.EnableBypassAllowlist = true + pb.BlockAdminMergeOverride = false + pb.BypassAllowlistUserIDs = nil + pb.BypassAllowlistTeamIDs = []int64{1} // team 1 contains user 2 in test fixtures + testBypass(t, true, pb, teamMember, false) + + // User does not bypass when not in allowlisted teams. + testBypass(t, false, pb, user, false) +} diff --git a/models/issues/comment.go b/models/issues/comment.go index 25e74c01ea..acfc07ff22 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -7,6 +7,7 @@ package issues import ( "context" + "errors" "fmt" "html/template" "slices" @@ -21,7 +22,9 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/structs" @@ -326,21 +329,34 @@ type Comment struct { RefIssue *Issue `xorm:"-"` RefComment *Comment `xorm:"-"` - Commits []*git_model.SignCommitWithStatuses `xorm:"-"` - OldCommit string `xorm:"-"` - NewCommit string `xorm:"-"` - CommitsNum int64 `xorm:"-"` - IsForcePush bool `xorm:"-"` + Commits []*git_model.SignCommitWithStatuses `xorm:"-"` + OldCommit string `xorm:"-"` + NewCommit string `xorm:"-"` + CommitsNum int64 `xorm:"-"` + + // Templates still use it. It is not persisted in database, it is only set when creating or loading + IsForcePush bool `xorm:"-"` } func init() { db.RegisterModel(new(Comment)) } -// PushActionContent is content of push pull comment +// PushActionContent is content of pull request's push comment type PushActionContent struct { - IsForcePush bool `json:"is_force_push"` - CommitIDs []string `json:"commit_ids"` + IsForcePush bool `json:"is_force_push"` + // if IsForcePush=true, CommitIDs contains the commit pair [old head, new head] + // if IsForcePush=false, CommitIDs contains the new commits newly pushed to the head branch + CommitIDs []string `json:"commit_ids"` +} + +func (c *Comment) GetPushActionContent() (*PushActionContent, error) { + if c.Type != CommentTypePullRequestPush { + return nil, errors.New("not a pull request push comment") + } + var data PushActionContent + _ = json.Unmarshal(util.UnsafeStringToBytes(c.Content), &data) + return &data, nil } // LoadIssue loads the issue reference for the comment @@ -384,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 } @@ -528,6 +535,12 @@ func (c *Comment) EventTag() string { return fmt.Sprintf("event-%d", c.ID) } +func (c *Comment) GetSanitizedContentHTML() template.HTML { + // mainly for type=4 CommentTypeCommitRef + // the content is a link like message title (from CreateRefComment) + return markup.Sanitize(c.Content) +} + // LoadLabel if comment.Type is CommentTypeLabel, then load Label func (c *Comment) LoadLabel(ctx context.Context) error { var label Label diff --git a/models/issues/dependency.go b/models/issues/dependency.go index 0eaa47e359..db8054e161 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 655cdebdfc..345b36a82f 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. @@ -74,17 +59,18 @@ type Issue struct { PosterID int64 `xorm:"INDEX"` Poster *user_model.User `xorm:"-"` OriginalAuthor string - OriginalAuthorID int64 `xorm:"index"` - Title string `xorm:"name"` - Content string `xorm:"LONGTEXT"` - RenderedContent template.HTML `xorm:"-"` - ContentVersion int `xorm:"NOT NULL DEFAULT 0"` - Labels []*Label `xorm:"-"` - isLabelsLoaded bool `xorm:"-"` - MilestoneID int64 `xorm:"INDEX"` - Milestone *Milestone `xorm:"-"` - isMilestoneLoaded bool `xorm:"-"` - Project *project_model.Project `xorm:"-"` + OriginalAuthorID int64 `xorm:"index"` + Title string `xorm:"name"` + Content string `xorm:"LONGTEXT"` + RenderedContent template.HTML `xorm:"-"` + ContentVersion int `xorm:"NOT NULL DEFAULT 0"` + Labels []*Label `xorm:"-"` + isLabelsLoaded bool `xorm:"-"` + MilestoneID int64 `xorm:"INDEX"` + Milestone *Milestone `xorm:"-"` + isMilestoneLoaded bool `xorm:"-"` + Projects []*project_model.Project `xorm:"-"` + isProjectsLoaded bool `xorm:"-"` Priority int AssigneeID int64 `xorm:"-"` Assignee *user_model.User `xorm:"-"` @@ -190,17 +176,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 } @@ -327,7 +306,7 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) { return err } - if err = issue.LoadProject(ctx); err != nil { + if err = issue.LoadProjects(ctx); err != nil { return err } @@ -377,6 +356,7 @@ func (issue *Issue) ResetAttributesLoaded() { issue.isMilestoneLoaded = false issue.isAttachmentsLoaded = false issue.isAssigneeLoaded = false + issue.isProjectsLoaded = false } // GetIsRead load the `IsRead` field of the issue @@ -592,6 +572,17 @@ func GetIssueByID(ctx context.Context, id int64) (*Issue, error) { return issue, nil } +func GetIssueByRepoID(ctx context.Context, repoID, issueID int64) (*Issue, error) { + issue := new(Issue) + has, err := db.GetEngine(ctx).ID(issueID).Where("repo_id=?", repoID).Get(issue) + if err != nil { + return nil, err + } else if !has { + return nil, ErrIssueNotExist{issueID, repoID, 0} + } + return issue, nil +} + // GetIssuesByIDs return issues with the given IDs. // If keepOrder is true, the order of the returned issues will be the same as the given IDs. func GetIssuesByIDs(ctx context.Context, issueIDs []int64, keepOrder ...bool) (IssueList, error) { diff --git a/models/issues/issue_label.go b/models/issues/issue_label.go index 151469a9b8..697448cf5e 100644 --- a/models/issues/issue_label.go +++ b/models/issues/issue_label.go @@ -356,7 +356,7 @@ func ClearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) return err } - perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) + perm, err := access_model.GetDoerRepoPermission(ctx, issue.Repo, doer) if err != nil { return err } diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 26b93189b8..da407094a7 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -185,7 +185,7 @@ func (issues IssueList) LoadMilestones(ctx context.Context) error { func (issues IssueList) LoadProjects(ctx context.Context) error { issueIDs := issues.getIssueIDs() - projectMaps := make(map[int64]*project_model.Project, len(issues)) + issueProjectMaps := make(map[int64][]*project_model.Project, len(issues)) left := len(issueIDs) type projectWithIssueID struct { @@ -202,19 +202,21 @@ func (issues IssueList) LoadProjects(ctx context.Context) error { Select("project.*, project_issue.issue_id"). Join("INNER", "project_issue", "project.id = project_issue.project_id"). In("project_issue.issue_id", issueIDs[:limit]). + OrderBy("project_issue.issue_id ASC, project.id ASC"). Find(&projects) if err != nil { return err } for _, project := range projects { - projectMaps[project.IssueID] = project.Project + issueProjectMaps[project.IssueID] = append(issueProjectMaps[project.IssueID], project.Project) } left -= limit issueIDs = issueIDs[limit:] } for _, issue := range issues { - issue.Project = projectMaps[issue.ID] + issue.Projects = issueProjectMaps[issue.ID] + issue.isProjectsLoaded = true } return nil } diff --git a/models/issues/issue_list_test.go b/models/issues/issue_list_test.go index e9dc412331..842249bad2 100644 --- a/models/issues/issue_list_test.go +++ b/models/issues/issue_list_test.go @@ -65,10 +65,10 @@ func TestIssueList_LoadAttributes(t *testing.T) { } if issue.ID == int64(1) { assert.Equal(t, int64(400), issue.TotalTrackedTime) - assert.NotNil(t, issue.Project) - assert.Equal(t, int64(1), issue.Project.ID) + assert.NotEmpty(t, issue.Projects) + assert.Equal(t, int64(1), issue.Projects[0].ID) } else { - assert.Nil(t, issue.Project) + assert.Empty(t, issue.Projects) } } } diff --git a/models/issues/issue_pin.go b/models/issues/issue_pin.go index ae6195b05d..753c96ed18 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_project.go b/models/issues/issue_project.go index 0185244783..18f0f91c38 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -12,41 +12,38 @@ import ( "code.gitea.io/gitea/modules/util" ) -// LoadProject load the project the issue was assigned to -func (issue *Issue) LoadProject(ctx context.Context) (err error) { - if issue.Project == nil { - var p project_model.Project - has, err := db.GetEngine(ctx).Table("project"). +// LoadProjects loads all projects the issue is assigned to +func (issue *Issue) LoadProjects(ctx context.Context) (err error) { + if !issue.isProjectsLoaded { + err = db.GetEngine(ctx).Table("project"). Join("INNER", "project_issue", "project.id=project_issue.project_id"). - Where("project_issue.issue_id = ?", issue.ID).Get(&p) - if err != nil { - return err - } else if has { - issue.Project = &p + Where("project_issue.issue_id = ?", issue.ID). + OrderBy("project.id ASC"). + Find(&issue.Projects) + if err == nil { + issue.isProjectsLoaded = true } } return err } -func (issue *Issue) projectID(ctx context.Context) int64 { - var ip project_model.ProjectIssue - has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) - if err != nil || !has { - return 0 - } - return ip.ProjectID +func (issue *Issue) projectIDs(ctx context.Context) (projectIDs []int64, _ error) { + err := db.GetEngine(ctx).Table("project_issue").Where("issue_id = ?", issue.ID).Cols("project_id").Find(&projectIDs) + return projectIDs, err } -// ProjectColumnID return project column id if issue was assigned to one -func (issue *Issue) ProjectColumnID(ctx context.Context) (int64, error) { - var ip project_model.ProjectIssue - has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) - if err != nil { - return 0, err - } else if !has { - return 0, nil +// ProjectColumnMap returns a map of project ID to column ID for this issue. +func (issue *Issue) ProjectColumnMap(ctx context.Context) (map[int64]int64, error) { + var projIssues []project_model.ProjectIssue + if err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Find(&projIssues); err != nil { + return nil, err } - return ip.ProjectColumnID, nil + + result := make(map[int64]int64, len(projIssues)) + for _, projIssue := range projIssues { + result[projIssue.ProjectID] = projIssue.ProjectColumnID + } + return result, nil } func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID int64) (map[int64]int64, error) { @@ -64,103 +61,91 @@ func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID i return result, nil } -// LoadIssuesFromColumn load issues assigned to this column -func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *IssuesOptions) (IssueList, error) { - issueList, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) { - o.ProjectColumnID = b.ID - o.ProjectID = b.ProjectID - o.SortType = "project-column-sorting" - })) - if err != nil { - return nil, err - } - - if b.Default { - issues, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) { - o.ProjectColumnID = db.NoConditionID - o.ProjectID = b.ProjectID - o.SortType = "project-column-sorting" - })) - if err != nil { - return nil, err - } - issueList = append(issueList, issues...) - } - - if err := issueList.LoadComments(ctx); err != nil { - return nil, err - } - - return issueList, nil -} - -// IssueAssignOrRemoveProject changes the project associated with an issue -// If newProjectID is 0, the issue is removed from the project -func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error { +// IssueAssignOrRemoveProject updates the projects associated with an issue. +// It adds projects that are in newProjectIDs but not currently assigned, +// and removes projects that are currently assigned but not in newProjectIDs. +// If newProjectIDs is empty, all projects are removed from the issue. +// When adding an issue to a project, it is placed in the project's default column. +func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectIDs []int64) error { return db.WithTx(ctx, func(ctx context.Context) error { - oldProjectID := issue.projectID(ctx) - if err := issue.LoadRepo(ctx); err != nil { return err } - // Only check if we add a new project and not remove it. - if newProjectID > 0 { - newProject, err := project_model.GetProjectByID(ctx, newProjectID) + oldProjectIDs, err := issue.projectIDs(ctx) + if err != nil { + return err + } + + projectsToAdd, projectsToRemove := util.DiffSlice(oldProjectIDs, newProjectIDs) + issue.isProjectsLoaded = false + issue.Projects = nil + + if len(projectsToRemove) > 0 { + if _, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).In("project_id", projectsToRemove).Delete(&project_model.ProjectIssue{}); err != nil { + return err + } + for _, projectID := range projectsToRemove { + if _, err := CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypeProject, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldProjectID: projectID, + ProjectID: 0, + }); err != nil { + return err + } + } + } + + if len(projectsToAdd) > 0 { + projectMap, err := project_model.GetProjectsMapByIDs(ctx, projectsToAdd) if err != nil { return err } - if !newProject.CanBeAccessedByOwnerRepo(issue.Repo.OwnerID, issue.Repo) { - return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID) - } - if newColumnID == 0 { - newDefaultColumn, err := newProject.MustDefaultColumn(ctx) + + for _, projectID := range projectsToAdd { + newProject, ok := projectMap[projectID] + if !ok { + return util.NewNotExistErrorf("project %d not found", projectID) + } + if !newProject.CanBeAccessedByOwnerRepo(issue.Repo.OwnerID, issue.Repo) { + return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID) + } + + defaultColumn, err := newProject.MustDefaultColumn(ctx) if err != nil { return err } - newColumnID = newDefaultColumn.ID + + newSorting, err := project_model.GetColumnIssueNextSorting(ctx, projectID, defaultColumn.ID) + if err != nil { + return err + } + + err = db.Insert(ctx, &project_model.ProjectIssue{ + IssueID: issue.ID, + ProjectID: projectID, + ProjectColumnID: defaultColumn.ID, + Sorting: newSorting, + }) + if err != nil { + return err + } + + if _, err := CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypeProject, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldProjectID: 0, + ProjectID: projectID, + }); err != nil { + return err + } } } - - if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { - return err - } - - if oldProjectID > 0 || newProjectID > 0 { - if _, err := CreateComment(ctx, &CreateCommentOptions{ - Type: CommentTypeProject, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldProjectID: oldProjectID, - ProjectID: newProjectID, - }); err != nil { - return err - } - } - if newProjectID == 0 { - return nil - } - if newColumnID == 0 { - panic("newColumnID must not be zero") // shouldn't happen - } - - res := struct { - MaxSorting int64 - IssueCount int64 - }{} - if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue"). - Where("project_id=?", newProjectID). - And("project_board_id=?", newColumnID). - Get(&res); err != nil { - return err - } - newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) - return db.Insert(ctx, &project_model.ProjectIssue{ - IssueID: issue.ID, - ProjectID: newProjectID, - ProjectColumnID: newColumnID, - Sorting: newSorting, - }) + return nil }) } diff --git a/models/issues/issue_project_multi_test.go b/models/issues/issue_project_multi_test.go new file mode 100644 index 0000000000..b004f994b1 --- /dev/null +++ b/models/issues/issue_project_multi_test.go @@ -0,0 +1,149 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues_test + +import ( + "fmt" + "testing" + + issues_model "code.gitea.io/gitea/models/issues" + project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIssueMultipleProjects(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("GeneralTest", func(t *testing.T) { + // Get test data + issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + project1 := unittest.AssertExistsAndLoadBean(t, &project_model.Project{ID: 1}) + + // Create a second project for the same repository + project2 := &project_model.Project{ + Title: "Test Project 2", + RepoID: issue1.RepoID, + Type: project_model.TypeRepository, + TemplateType: project_model.TemplateTypeBasicKanban, + } + require.NoError(t, project_model.NewProject(t.Context(), project2)) + defer func() { + _ = project_model.DeleteProjectByID(t.Context(), project2.ID) + }() + + err := issues_model.IssueAssignOrRemoveProject(t.Context(), issue1, user2, []int64{}) + require.NoError(t, err) + err = issue1.LoadProjects(t.Context()) + require.NoError(t, err) + require.Empty(t, issue1.Projects) + + // assign issue to both projects (each project uses its own default column) + err = issues_model.IssueAssignOrRemoveProject(t.Context(), issue1, user2, []int64{project1.ID}) + require.NoError(t, err) + assert.Nilf(t, issue1.Projects, "Issue's Projects should be nil after IssueAssignOrRemoveProject to ensure it reloads fresh data") + err = issue1.LoadProjects(t.Context()) + require.NoError(t, err) + require.Len(t, issue1.Projects, 1) + + err = issues_model.IssueAssignOrRemoveProject(t.Context(), issue1, user2, []int64{project1.ID, project2.ID}) + require.NoError(t, err) + assert.Nilf(t, issue1.Projects, "Issue's Projects should be nil after IssueAssignOrRemoveProject to ensure it reloads fresh data") + err = issue1.LoadProjects(t.Context()) + require.NoError(t, err) + require.Len(t, issue1.Projects, 2) + assert.ElementsMatch(t, []int64{project1.ID, project2.ID}, []int64{issue1.Projects[0].ID, issue1.Projects[1].ID}, "Issue should be in both projects") + + // test issue's project column map + projectColumnMap, err := issue1.ProjectColumnMap(t.Context()) + p1Col, _ := project1.MustDefaultColumn(t.Context()) + p2Col, _ := project2.MustDefaultColumn(t.Context()) + require.NoError(t, err) + assert.Equal(t, p1Col.ID, projectColumnMap[project1.ID]) + assert.Equal(t, p2Col.ID, projectColumnMap[project2.ID]) + + // only keep project2 + err = issues_model.IssueAssignOrRemoveProject(t.Context(), issue1, user2, []int64{project2.ID}) + require.NoError(t, err) + err = issue1.LoadProjects(t.Context()) + require.NoError(t, err) + require.Len(t, issue1.Projects, 1) + assert.Equal(t, project2.ID, issue1.Projects[0].ID) + + // also test ResetAttributesLoaded + issue1.Projects = nil + issue1.ResetAttributesLoaded() + err = issue1.LoadProjects(t.Context()) + require.NoError(t, err) + require.Len(t, issue1.Projects, 1) + assert.Equal(t, project2.ID, issue1.Projects[0].ID) + + // remove issue's projects + err = issues_model.IssueAssignOrRemoveProject(t.Context(), issue1, user2, []int64{}) + require.NoError(t, err) + err = issue1.LoadProjects(t.Context()) + require.NoError(t, err) + require.Empty(t, issue1.Projects) + }) + + t.Run("QueryByMultipleProjectIDs", func(t *testing.T) { + // Get test data + issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // Create three projects + var projects []*project_model.Project + for i := 1; i <= 3; i++ { + project := &project_model.Project{ + Title: fmt.Sprintf("Query Test Project %d", i), + RepoID: issue1.RepoID, + Type: project_model.TypeRepository, + TemplateType: project_model.TemplateTypeBasicKanban, + } + require.NoError(t, project_model.NewProject(t.Context(), project)) + projects = append(projects, project) + defer func(id int64) { + _ = project_model.DeleteProjectByID(t.Context(), id) + }(project.ID) + } + + // Assign issue1 to projects 1 and 2 + err := issues_model.IssueAssignOrRemoveProject(t.Context(), issue1, user2, []int64{projects[0].ID, projects[1].ID}) + require.NoError(t, err) + + // Assign issue2 to project 3 + err = issues_model.IssueAssignOrRemoveProject(t.Context(), issue2, user2, []int64{projects[2].ID}) + require.NoError(t, err) + + // Query for issues in project 3 only (should find issue2) + issues, err := issues_model.Issues(t.Context(), &issues_model.IssuesOptions{ + RepoIDs: []int64{issue1.RepoID}, + ProjectIDs: []int64{projects[2].ID}, + }) + require.NoError(t, err) + assert.NotEmpty(t, issues, "Should find issues in project 3") + + // Verify issue2 is in the results + foundIssue2 := false + for _, issue := range issues { + if issue.ID == issue2.ID { + foundIssue2 = true + break + } + } + assert.True(t, foundIssue2, "Issue 2 should be found when querying project 3") + + // FIXME: ISSUE-MULTIPLE-PROJECTS-FILTER: no multiple project filter support yet. Search logic is wrong. It should use "AND" but not "OR". + // Clean up + err = issues_model.IssueAssignOrRemoveProject(t.Context(), issue1, user2, []int64{}) + require.NoError(t, err) + err = issues_model.IssueAssignOrRemoveProject(t.Context(), issue2, user2, []int64{}) + require.NoError(t, err) + }) +} diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 049dcc7de8..f905e629e3 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -16,6 +16,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" "xorm.io/xorm" @@ -36,8 +37,7 @@ type IssuesOptions struct { //nolint:revive // export stutter ReviewedID int64 SubscriberID int64 MilestoneIDs []int64 - ProjectID int64 - ProjectColumnID int64 + ProjectIDs []int64 IsClosed optional.Option[bool] IsPull optional.Option[bool] LabelIDs []int64 @@ -198,26 +198,19 @@ func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) { } func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) { - if opts.ProjectID > 0 { // specific project - sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). - And("project_issue.project_id=?", opts.ProjectID) - } else if opts.ProjectID == db.NoConditionID { // show those that are in no project - sess.And(builder.NotIn("issue.id", builder.Select("issue_id").From("project_issue").And(builder.Neq{"project_id": 0}))) + projectIDs := util.SliceRemoveAll(opts.ProjectIDs, 0) + if len(projectIDs) == 1 && projectIDs[0] == db.NoConditionID { // show those that are in no project + sess.And(builder.NotIn("issue.id", builder.Select("issue_id").From("project_issue"))) + } else if len(projectIDs) == 1 && projectIDs[0] > 0 { // single specific project + sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id AND project_issue.project_id = ?", projectIDs[0]) + } else if len(projectIDs) > 1 { // multiple projects + // FIXME: ISSUE-MULTIPLE-PROJECTS-FILTER: this logic is not right, it should use "AND" but not "OR" + sess.And(builder.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.In("project_id", projectIDs)))) } - // opts.ProjectID == 0 means all projects, + // empty projectIDs means all projects, // do not need to apply any condition } -func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) { - // opts.ProjectColumnID == 0 means all project columns, - // do not need to apply any condition - if opts.ProjectColumnID > 0 { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectColumnID})) - } else if opts.ProjectColumnID == db.NoConditionID { - sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) - } -} - func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) { if len(opts.RepoIDs) == 1 { opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoIDs[0]} @@ -276,8 +269,6 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) { applyProjectCondition(sess, opts) - applyProjectColumnCondition(sess, opts) - if opts.IsPull.Has() { sess.And("issue.is_pull=?", opts.IsPull.Value()) } diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go index adedaa3d3a..ddf6613870 100644 --- a/models/issues/issue_stats.go +++ b/models/issues/issue_stats.go @@ -35,12 +35,10 @@ const ( FilterModeYourRepositories ) -const ( - // MaxQueryParameters represents the max query parameters - // When queries are broken down in parts because of the number - // of parameters, attempt to break by this amount - MaxQueryParameters = 300 -) +// MaxQueryParameters represents the max query parameters +// When queries are broken down in parts because of the number +// of parameters, attempt to break by this amount +var MaxQueryParameters = 300 // CountIssuesByRepo map from repoID to number of issues matching the options func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int64, error) { diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 55a90f50a1..99a09de0ec 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" "xorm.io/builder" @@ -297,14 +298,11 @@ func TestIssue_ResolveMentions(t *testing.T) { func TestResourceIndex(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - var wg sync.WaitGroup - for i := range 100 { - wg.Add(1) - go func(i int) { + for i := range 25 { + wg.Go(func() { testInsertIssue(t, fmt.Sprintf("issue %d", i+1), "my issue", 0) - wg.Done() - }(i) + }) } wg.Wait() } @@ -317,18 +315,12 @@ func TestCorrectIssueStats(t *testing.T) { // maxQueryParameters + 10 issues into the testDatabase. // Each new issues will have a constant description "Bugs are nasty" // Which will be used later on. - + defer test.MockVariableValue(&issues_model.MaxQueryParameters, 25)() issueAmount := issues_model.MaxQueryParameters + 10 - var wg sync.WaitGroup for i := range issueAmount { - wg.Add(1) - go func(i int) { - testInsertIssue(t, fmt.Sprintf("Issue %d", i+1), "Bugs are nasty", 0) - wg.Done() - }(i) + testInsertIssue(t, fmt.Sprintf("Issue %d", i+1), "Bugs are nasty", 0) } - wg.Wait() // Now we will get all issueID's that match the "Bugs are nasty" query. issues, err := issues_model.Issues(t.Context(), &issues_model.IssuesOptions{ @@ -424,10 +416,10 @@ func TestIssueLoadAttributes(t *testing.T) { } if issue.ID == int64(1) { assert.Equal(t, int64(400), issue.TotalTrackedTime) - assert.NotNil(t, issue.Project) - assert.Equal(t, int64(1), issue.Project.ID) + assert.NotEmpty(t, issue.Projects) + assert.Equal(t, int64(1), issue.Projects[0].ID) } else { - assert.Nil(t, issue.Project) + assert.Empty(t, issue.Projects) } } } diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 0a320ffc56..c58a2f319d 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) @@ -665,9 +659,9 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u continue } // Normal users must have read access to the referencing issue - perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user) + perm, err := access_model.GetIndividualUserRepoPermission(ctx, issue.Repo, user) if err != nil { - return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err) + return nil, fmt.Errorf("GetIndividualUserRepoPermission [%d]: %w", user.ID, err) } if !perm.CanReadIssuesOrPulls(issue.IsPull) { continue diff --git a/models/issues/issue_watch.go b/models/issues/issue_watch.go index 560be17eb6..f384e086e5 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/issue_xref.go b/models/issues/issue_xref.go index f8495929cf..41a867c892 100644 --- a/models/issues/issue_xref.go +++ b/models/issues/issue_xref.go @@ -207,7 +207,7 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe // Check doer permissions; set action to None if the doer can't change the destination if refIssue.RepoID != ctx.OrigIssue.RepoID || ref.Action != references.XRefActionNone { - perm, err := access_model.GetUserRepoPermission(stdCtx, refIssue.Repo, ctx.Doer) + perm, err := access_model.GetDoerRepoPermission(stdCtx, refIssue.Repo, ctx.Doer) if err != nil { return nil, references.XRefActionNone, err } diff --git a/models/issues/pull.go b/models/issues/pull.go index c07044f301..4c8d02a990 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) @@ -877,7 +877,12 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, warnings := make([]string, 0) - expr := fmt.Sprintf("^%s$", strings.TrimPrefix(tokens[0], "!")) + // Strip leading "!" for negative rules, then strip leading "/" since + // git returns relative paths (e.g. "docs/foo.md" not "/docs/foo.md") + // and the regex is already anchored with ^...$, so the "/" is redundant. + pattern := strings.TrimPrefix(tokens[0], "!") + pattern = strings.TrimPrefix(pattern, "/") + expr := fmt.Sprintf("^%s$", pattern) rule.Rule, err = regexp2.Compile(expr, regexp2.None) if err != nil { warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err)) diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index 19d727ecbd..6a6abca970 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -71,38 +71,69 @@ func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch } // CanMaintainerWriteToBranch check whether user is a maintainer and could write to the branch -func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission, branch string, user *user_model.User) bool { - if p.CanWrite(unit.TypeCode) { - return true +func CanMaintainerWriteToBranch(ctx context.Context, headPerm access_model.Permission, headBranch string, doer *user_model.User) bool { + can, err := canMaintainerWriteToBranch(ctx, headPerm, headBranch, doer) + if err != nil { + log.Error("CanMaintainerWriteToBranch: %v", err) + return false + } + return can +} + +func canMaintainerWriteToBranch(ctx context.Context, headPerm access_model.Permission, headBranch string, doer *user_model.User) (bool, error) { + if headPerm.CanWrite(unit.TypeCode) { + return true, nil } // the code below depends on units to get the repository ID, not ideal but just keep it for now - firstUnitRepoID := p.GetFirstUnitRepoID() + firstUnitRepoID := headPerm.GetFirstUnitRepoID() if firstUnitRepoID == 0 { - return false + return false, nil } - prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, branch) + prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, headBranch) if err != nil { - return false + return false, err + } + if _, err := prs.LoadIssues(ctx); err != nil { + return false, err } - for _, pr := range prs { - if pr.AllowMaintainerEdit { - err = pr.LoadBaseRepo(ctx) - if err != nil { - continue - } - prPerm, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, user) - if err != nil { - continue - } - if prPerm.CanWrite(unit.TypeCode) { - return true - } + if !pr.AllowMaintainerEdit { + continue + } + + // check the PR's poster's permissions + // If a "reader" poster created the PR in base repo from head repo, even if it is allowed to be edited by maintainers, + // the maintainers should not be allowed to write, because they don't really have "write" permission in the head repo + if err := pr.Issue.LoadPoster(ctx); err != nil { + return false, err + } + if err := pr.LoadHeadRepo(ctx); err != nil { + return false, err + } + posterHeadPerm, err := access_model.GetIndividualUserRepoPermission(ctx, pr.HeadRepo, pr.Issue.Poster) + if err != nil { + return false, err + } + if !posterHeadPerm.CanWrite(unit.TypeCode) { + continue + } + + // check the doer's permission + // Only allow the doer to edit the PR if they have write access to the base repository + if err := pr.LoadBaseRepo(ctx); err != nil { + return false, err + } + doerBasePerm, err := access_model.GetIndividualUserRepoPermission(ctx, pr.BaseRepo, doer) + if err != nil { + return false, err + } + if doerBasePerm.CanWrite(unit.TypeCode) { + return true, nil } } - return false + return false, nil } // HasUnmergedPullRequestsByHeadInfo checks if there are open and not merged pull request diff --git a/models/issues/pull_list_test.go b/models/issues/pull_list_test.go index 437830701c..302b2ca0ba 100644 --- a/models/issues/pull_list_test.go +++ b/models/issues/pull_list_test.go @@ -6,15 +6,28 @@ package issues_test import ( "testing" + "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "xorm.io/builder" ) -func TestPullRequestList_LoadAttributes(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func TestPullRequestList(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + t.Run("LoadAttributes", testPullRequestListLoadAttributes) + t.Run("LoadReviewCommentsCounts", testPullRequestListLoadReviewCommentsCounts) + t.Run("LoadReviews", testPullRequestListLoadReviews) + t.Run("CanMaintainerWriteToBranch", testCanMaintainerWriteToBranch) +} +func testPullRequestListLoadAttributes(t *testing.T) { prs := issues_model.PullRequestList{ unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), @@ -28,9 +41,7 @@ func TestPullRequestList_LoadAttributes(t *testing.T) { assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes(t.Context())) } -func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testPullRequestListLoadReviewCommentsCounts(t *testing.T) { prs := issues_model.PullRequestList{ unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), @@ -43,9 +54,7 @@ func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) { } } -func TestPullRequestList_LoadReviews(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testPullRequestListLoadReviews(t *testing.T) { prs := issues_model.PullRequestList{ unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), @@ -61,3 +70,73 @@ func TestPullRequestList_LoadReviews(t *testing.T) { assert.EqualValues(t, 10, reviewList[4].ID) assert.EqualValues(t, 22, reviewList[5].ID) } + +func testCanMaintainerWriteToBranch(t *testing.T) { + ctx := t.Context() + baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}) + headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11}) + + _ = baseRepo.LoadOwner(ctx) + _ = headRepo.LoadOwner(ctx) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // a PR from header's owner + headOwnerPR := &issues_model.PullRequest{ + Issue: &issues_model.Issue{ + RepoID: baseRepo.ID, + PosterID: headRepo.OwnerID, + }, + HeadRepoID: headRepo.ID, + BaseRepoID: baseRepo.ID, + HeadBranch: "pr-from-head-owner", + BaseBranch: "master", + } + require.NoError(t, issues_model.NewPullRequest(ctx, baseRepo, headOwnerPR.Issue, nil, nil, headOwnerPR)) + + // a PR from a user, they might have or not have "write" permission in the target repo + anyUserPR := &issues_model.PullRequest{ + Issue: &issues_model.Issue{ + RepoID: baseRepo.ID, + PosterID: user.ID, + }, + HeadRepoID: headRepo.ID, + BaseRepoID: baseRepo.ID, + HeadBranch: "pr-from-head-user", + BaseBranch: "master", + } + require.NoError(t, issues_model.NewPullRequest(ctx, baseRepo, anyUserPR.Issue, nil, nil, anyUserPR)) + + doerCanWrite := func(doer *user_model.User, pr *issues_model.PullRequest) bool { + headPerm, _ := access.GetIndividualUserRepoPermission(ctx, headRepo, doer) + return issues_model.CanMaintainerWriteToBranch(ctx, headPerm, pr.HeadBranch, doer) + } + + t.Run("NoAllowMaintainerEdit", func(t *testing.T) { + assert.True(t, doerCanWrite(headRepo.Owner, headOwnerPR)) + assert.False(t, doerCanWrite(baseRepo.Owner, headOwnerPR)) + assert.False(t, doerCanWrite(baseRepo.Owner, anyUserPR)) + assert.False(t, doerCanWrite(user, anyUserPR)) + }) + + t.Run("WithAllowMaintainerEdit-HeadPosterReader", func(t *testing.T) { + _, err := db.GetEngine(ctx).Where(builder.In("id", []int64{headOwnerPR.ID, anyUserPR.ID})). + Cols("allow_maintainer_edit"). + Update(&issues_model.PullRequest{AllowMaintainerEdit: true}) + require.NoError(t, err) + assert.True(t, doerCanWrite(baseRepo.Owner, headOwnerPR)) + assert.False(t, doerCanWrite(baseRepo.Owner, anyUserPR)) // poster doesn't have write permission, so maintainer can't write either + }) + + t.Run("WithAllowMaintainerEdit-HeadPosterWriter", func(t *testing.T) { + _, err := db.GetEngine(ctx).Where(builder.In("id", []int64{headOwnerPR.ID, anyUserPR.ID})). + Cols("allow_maintainer_edit"). + Update(&issues_model.PullRequest{AllowMaintainerEdit: true}) + require.NoError(t, err) + err = db.Insert(ctx, &repo_model.Collaboration{RepoID: headRepo.ID, UserID: user.ID, Mode: perm.AccessModeWrite}) + require.NoError(t, err) + err = db.Insert(ctx, &access.Access{RepoID: headRepo.ID, UserID: user.ID, Mode: perm.AccessModeWrite}) + require.NoError(t, err) + assert.True(t, doerCanWrite(baseRepo.Owner, headOwnerPR)) + assert.True(t, doerCanWrite(baseRepo.Owner, anyUserPR)) // now the poster has the write permission + }) +} diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 25b27cbe9c..79d1f8aa9b 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -17,16 +17,43 @@ import ( "github.com/stretchr/testify/require" ) -func TestPullRequest_LoadAttributes(t *testing.T) { +func TestPullRequest(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("LoadAttributes", testPullRequestLoadAttributes) + t.Run("LoadIssue", testPullRequestLoadIssue) + t.Run("LoadBaseRepo", testPullRequestLoadBaseRepo) + t.Run("LoadHeadRepo", testPullRequestLoadHeadRepo) + t.Run("PullRequestsNewest", testPullRequestsNewest) + t.Run("PullRequestsOldest", testPullRequestsOldest) + t.Run("GetUnmergedPullRequest", testGetUnmergedPullRequest) + t.Run("HasUnmergedPullRequestsByHeadInfo", testHasUnmergedPullRequestsByHeadInfo) + t.Run("GetUnmergedPullRequestsByHeadInfo", testGetUnmergedPullRequestsByHeadInfo) + t.Run("GetUnmergedPullRequestsByBaseInfo", testGetUnmergedPullRequestsByBaseInfo) + t.Run("GetPullRequestByIndex", testGetPullRequestByIndex) + t.Run("GetPullRequestByID", testGetPullRequestByID) + t.Run("GetPullRequestByIssueID", testGetPullRequestByIssueID) + t.Run("PullRequest_UpdateCols", testPullRequestUpdateCols) + t.Run("PullRequest_IsWorkInProgress", testPullRequestIsWorkInProgress) + t.Run("PullRequest_GetWorkInProgressPrefixWorkInProgress", testPullRequestGetWorkInProgressPrefixWorkInProgress) + t.Run("DeleteOrphanedObjects", testDeleteOrphanedObjects) + t.Run("ParseCodeOwnersLine", testParseCodeOwnersLine) + t.Run("CodeOwnerAbsolutePathPatterns", testCodeOwnerAbsolutePathPatterns) + t.Run("GetApprovers", testGetApprovers) + t.Run("GetPullRequestByMergedCommit", testGetPullRequestByMergedCommit) + t.Run("Migrate_InsertPullRequests", testMigrateInsertPullRequests) + t.Run("PullRequestsClosedRecentSortType", testPullRequestsClosedRecentSortType) + t.Run("LoadRequestedReviewers", testLoadRequestedReviewers) +} + +func testPullRequestLoadAttributes(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) assert.NoError(t, pr.LoadAttributes(t.Context())) assert.NotNil(t, pr.Merger) assert.Equal(t, pr.MergerID, pr.Merger.ID) } -func TestPullRequest_LoadIssue(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testPullRequestLoadIssue(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) assert.NoError(t, pr.LoadIssue(t.Context())) assert.NotNil(t, pr.Issue) @@ -36,8 +63,7 @@ func TestPullRequest_LoadIssue(t *testing.T) { assert.Equal(t, int64(2), pr.Issue.ID) } -func TestPullRequest_LoadBaseRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testPullRequestLoadBaseRepo(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) assert.NoError(t, pr.LoadBaseRepo(t.Context())) assert.NotNil(t, pr.BaseRepo) @@ -47,8 +73,7 @@ func TestPullRequest_LoadBaseRepo(t *testing.T) { assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID) } -func TestPullRequest_LoadHeadRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testPullRequestLoadHeadRepo(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) assert.NoError(t, pr.LoadHeadRepo(t.Context())) assert.NotNil(t, pr.HeadRepo) @@ -59,8 +84,7 @@ func TestPullRequest_LoadHeadRepo(t *testing.T) { // TODO TestNewPullRequest -func TestPullRequestsNewest(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testPullRequestsNewest(t *testing.T) { prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{ ListOptions: db.ListOptions{ Page: 1, @@ -77,7 +101,7 @@ func TestPullRequestsNewest(t *testing.T) { } } -func TestPullRequests_Closed_RecentSortType(t *testing.T) { +func testPullRequestsClosedRecentSortType(t *testing.T) { // Issue ID | Closed At. | Updated At // 2 | 1707270001 | 1707270001 // 3 | 1707271000 | 1707279999 @@ -90,7 +114,6 @@ func TestPullRequests_Closed_RecentSortType(t *testing.T) { {"recentclose", []int64{11, 3, 2}}, } - assert.NoError(t, unittest.PrepareTestDatabase()) _, err := db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2") require.NoError(t, err) _, err = db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3") @@ -118,9 +141,7 @@ func TestPullRequests_Closed_RecentSortType(t *testing.T) { } } -func TestLoadRequestedReviewers(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testLoadRequestedReviewers(t *testing.T) { pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) assert.NoError(t, pull.LoadIssue(t.Context())) issue := pull.Issue @@ -146,8 +167,7 @@ func TestLoadRequestedReviewers(t *testing.T) { assert.Empty(t, pull.RequestedReviewers) } -func TestPullRequestsOldest(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testPullRequestsOldest(t *testing.T) { prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{ ListOptions: db.ListOptions{ Page: 1, @@ -164,8 +184,7 @@ func TestPullRequestsOldest(t *testing.T) { } } -func TestGetUnmergedPullRequest(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetUnmergedPullRequest(t *testing.T) { pr, err := issues_model.GetUnmergedPullRequest(t.Context(), 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub) assert.NoError(t, err) assert.Equal(t, int64(2), pr.ID) @@ -175,9 +194,7 @@ func TestGetUnmergedPullRequest(t *testing.T) { assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } -func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testHasUnmergedPullRequestsByHeadInfo(t *testing.T) { exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2") assert.NoError(t, err) assert.True(t, exist) @@ -187,8 +204,7 @@ func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) { assert.False(t, exist) } -func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetUnmergedPullRequestsByHeadInfo(t *testing.T) { prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2") assert.NoError(t, err) assert.Len(t, prs, 1) @@ -198,8 +214,7 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) { } } -func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetUnmergedPullRequestsByBaseInfo(t *testing.T) { prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(t.Context(), 1, "master") assert.NoError(t, err) assert.Len(t, prs, 1) @@ -209,8 +224,7 @@ func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { assert.Equal(t, "master", pr.BaseBranch) } -func TestGetPullRequestByIndex(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetPullRequestByIndex(t *testing.T) { pr, err := issues_model.GetPullRequestByIndex(t.Context(), 1, 2) assert.NoError(t, err) assert.Equal(t, int64(1), pr.BaseRepoID) @@ -225,8 +239,7 @@ func TestGetPullRequestByIndex(t *testing.T) { assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } -func TestGetPullRequestByID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetPullRequestByID(t *testing.T) { pr, err := issues_model.GetPullRequestByID(t.Context(), 1) assert.NoError(t, err) assert.Equal(t, int64(1), pr.ID) @@ -237,8 +250,7 @@ func TestGetPullRequestByID(t *testing.T) { assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } -func TestGetPullRequestByIssueID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetPullRequestByIssueID(t *testing.T) { pr, err := issues_model.GetPullRequestByIssueID(t.Context(), 2) assert.NoError(t, err) assert.Equal(t, int64(2), pr.IssueID) @@ -248,8 +260,7 @@ func TestGetPullRequestByIssueID(t *testing.T) { assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } -func TestPullRequest_UpdateCols(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testPullRequestUpdateCols(t *testing.T) { pr := &issues_model.PullRequest{ ID: 1, BaseBranch: "baseBranch", @@ -265,9 +276,7 @@ func TestPullRequest_UpdateCols(t *testing.T) { // TODO TestAddTestPullRequestTask -func TestPullRequest_IsWorkInProgress(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testPullRequestIsWorkInProgress(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) pr.LoadIssue(t.Context()) @@ -280,9 +289,7 @@ func TestPullRequest_IsWorkInProgress(t *testing.T) { assert.True(t, pr.IsWorkInProgress(t.Context())) } -func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testPullRequestGetWorkInProgressPrefixWorkInProgress(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) pr.LoadIssue(t.Context()) @@ -296,9 +303,7 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) { assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix(t.Context())) } -func TestDeleteOrphanedObjects(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testDeleteOrphanedObjects(t *testing.T) { countBefore, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{}) assert.NoError(t, err) @@ -317,7 +322,7 @@ func TestDeleteOrphanedObjects(t *testing.T) { assert.Equal(t, countBefore, countAfter) } -func TestParseCodeOwnersLine(t *testing.T) { +func testParseCodeOwnersLine(t *testing.T) { type CodeOwnerTest struct { Line string Tokens []string @@ -331,6 +336,8 @@ func TestParseCodeOwnersLine(t *testing.T) { {Line: `docs/(aws|google|azure)/[^/]*\\.(md|txt) @org3 @org2/team2`, Tokens: []string{`docs/(aws|google|azure)/[^/]*\.(md|txt)`, "@org3", "@org2/team2"}}, {Line: `\#path @org3`, Tokens: []string{`#path`, "@org3"}}, {Line: `path\ with\ spaces/ @org3`, Tokens: []string{`path with spaces/`, "@org3"}}, + {Line: `/docs/.*\\.md @user1`, Tokens: []string{`/docs/.*\.md`, "@user1"}}, + {Line: `!/assets/.*\\.(bin|exe|msi) @user1`, Tokens: []string{`!/assets/.*\.(bin|exe|msi)`, "@user1"}}, } for _, g := range given { @@ -339,8 +346,37 @@ func TestParseCodeOwnersLine(t *testing.T) { } } -func TestGetApprovers(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testCodeOwnerAbsolutePathPatterns(t *testing.T) { + type testCase struct { + content string + file string + expected bool + } + + cases := []testCase{ + // Absolute path pattern should match (leading "/" stripped) + {content: "/README.md @user5\n", file: "README.md", expected: true}, + // Absolute path pattern in subdirectory + {content: "/docs/.* @user5\n", file: "docs/foo.md", expected: true}, + // Absolute path should not match nested paths it shouldn't + {content: "/docs/.* @user5\n", file: "other/docs/foo.md", expected: false}, + // Relative path still works + {content: "README.md @user5\n", file: "README.md", expected: true}, + // Negated absolute path pattern + {content: "!/.* @user5\n", file: "README.md", expected: false}, + } + + for _, c := range cases { + rules, _ := issues_model.GetCodeOwnersFromContent(t.Context(), c.content) + require.NotEmpty(t, rules) + rule := rules[0] + regexpMatched, _ := rule.Rule.MatchString(c.file) + ruleMatched := regexpMatched == !rule.Negative + assert.Equal(t, c.expected, ruleMatched, "pattern %q against file %q", c.content, c.file) + } +} + +func testGetApprovers(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5}) // Official reviews are already deduplicated. Allow unofficial reviews // to assert that there are no duplicated approvers. @@ -350,8 +386,7 @@ func TestGetApprovers(t *testing.T) { assert.Equal(t, expected, approvers) } -func TestGetPullRequestByMergedCommit(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetPullRequestByMergedCommit(t *testing.T) { pr, err := issues_model.GetPullRequestByMergedCommit(t.Context(), 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3") assert.NoError(t, err) assert.EqualValues(t, 1, pr.ID) @@ -362,8 +397,7 @@ func TestGetPullRequestByMergedCommit(t *testing.T) { assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{}) } -func TestMigrate_InsertPullRequests(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testMigrateInsertPullRequests(t *testing.T) { reponame := "repo1" repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) diff --git a/models/issues/review.go b/models/issues/review.go index 22e2e186d9..78ef0d20c2 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 { @@ -919,7 +911,7 @@ func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.Use return false, nil } if doer.ID != issue.PosterID { - p, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) + p, err := access_model.GetDoerRepoPermission(ctx, issue.Repo, doer) if err != nil { return false, err } diff --git a/models/migrations/base/db_test.go b/models/migrations/base/db_test.go index 00635ca72e..ce6e1169d8 100644 --- a/models/migrations/base/db_test.go +++ b/models/migrations/base/db_test.go @@ -6,22 +6,23 @@ package base import ( "testing" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "xorm.io/xorm/names" ) func TestMain(m *testing.M) { - MainTest(m) + migrationtest.MainTest(m) } func Test_DropTableColumns(t *testing.T) { - x, deferable := PrepareTestEnv(t, 0) - if x == nil || t.Failed() { - defer deferable() - return - } + x, deferable := migrationtest.PrepareTestEnv(t, 0) defer deferable() + // FIXME: this logic seems wrong. Need to add an assertion here in the future, but it seems causing failure. + if x == nil || t.Failed() { + t.Skip("PrepareTestEnv did not yield a usable engine") + } type DropTest struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go deleted file mode 100644 index 17ea951b5a..0000000000 --- a/models/migrations/base/tests.go +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package base - -import ( - "database/sql" - "fmt" - "os" - "path" - "path/filepath" - "testing" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/tempdir" - "code.gitea.io/gitea/modules/testlogger" - "code.gitea.io/gitea/modules/util" - - "github.com/stretchr/testify/require" - "xorm.io/xorm" - "xorm.io/xorm/schemas" -) - -// FIXME: this file shouldn't be in a normal package, it should only be compiled for tests - -func newXORMEngine(t *testing.T) (*xorm.Engine, error) { - if err := db.InitEngine(t.Context()); err != nil { - return nil, err - } - x := unittest.GetXORMEngine() - return x, nil -} - -func deleteDB() error { - switch { - case setting.Database.Type.IsSQLite3(): - if err := util.Remove(setting.Database.Path); err != nil { - return err - } - return os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm) - - case setting.Database.Type.IsMySQL(): - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/", - setting.Database.User, setting.Database.Passwd, setting.Database.Host)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { - return err - } - - if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name); err != nil { - return err - } - return nil - case setting.Database.Type.IsPostgreSQL(): - db, err := sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { - return err - } - - if _, err = db.Exec("CREATE DATABASE " + setting.Database.Name); err != nil { - return err - } - db.Close() - - // Check if we need to setup 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)) - if err != nil { - return err - } - defer db.Close() - - schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema)) - if err != nil { - return err - } - defer schrows.Close() - - if !schrows.Next() { - // Create and setup a DB schema - _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema) - if err != nil { - return err - } - } - - // Make the user's default search path the created schema; this will affect new connections - _, err = db.Exec(fmt.Sprintf(`ALTER USER "%s" SET search_path = %s`, setting.Database.User, setting.Database.Schema)) - if err != nil { - return err - } - return nil - } - case setting.Database.Type.IsMSSQL(): - host, port := setting.ParseMSSQLHostPort(setting.Database.Host) - db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, "master", setting.Database.User, setting.Database.Passwd)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS [%s]", setting.Database.Name)); err != nil { - return err - } - if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE [%s]", setting.Database.Name)); err != nil { - return err - } - } - - return nil -} - -// PrepareTestEnv prepares the test environment and reset the database. The skip parameter should usually be 0. -// Provide models to be sync'd with the database - in particular any models you expect fixtures to be loaded from. -// -// fixtures in `models/migrations/fixtures/` will be loaded automatically -func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, func()) { - t.Helper() - 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)) - - if err := deleteDB(); err != nil { - t.Fatalf("unable to reset database: %v", err) - return nil, deferFn - } - - x, err := newXORMEngine(t) - require.NoError(t, err) - if x != nil { - oldDefer := deferFn - deferFn = func() { - oldDefer() - if err := x.Close(); err != nil { - t.Errorf("error during close: %v", err) - } - if err := deleteDB(); err != nil { - t.Errorf("unable to reset database: %v", err) - } - } - } - if err != nil { - return x, deferFn - } - - if len(syncModels) > 0 { - if err := x.Sync(syncModels...); err != nil { - t.Errorf("error during sync: %v", err) - return x, deferFn - } - } - - fixturesDir := filepath.Join(filepath.Dir(setting.AppPath), "models", "migrations", "fixtures", t.Name()) - - if _, err := os.Stat(fixturesDir); err == nil { - t.Logf("initializing fixtures from: %s", fixturesDir) - if err := unittest.InitFixtures( - unittest.FixturesOptions{ - Dir: fixturesDir, - }, x); err != nil { - t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err) - return x, deferFn - } - if err := unittest.LoadFixtures(); err != nil { - t.Errorf("error whilst loading fixtures from %s: %v", fixturesDir, err) - return x, deferFn - } - } else if !os.IsNotExist(err) { - t.Errorf("unexpected error whilst checking for existence of fixtures: %v", err) - } else { - t.Logf("no fixtures found in: %s", fixturesDir) - } - - return x, deferFn -} - -func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table { - tables, err := x.DBMetas() - require.NoError(t, err) - tableMap := make(map[string]*schemas.Table) - for _, table := range tables { - tableMap[table.Name] = table - } - return tableMap -} - -func mainTest(m *testing.M) int { - testlogger.Init() - - tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data") - if err != nil { - testlogger.Panicf("Unable to create temporary data path %v\n", err) - } - defer cleanup() - - setting.AppDataPath = tmpDataPath - - unittest.InitSettingsForTesting() - if err = git.InitFull(); err != nil { - testlogger.Panicf("Unable to InitFull: %v\n", err) - } - setting.LoadDBSetting() - setting.InitLoggersForTest() - return m.Run() -} - -func MainTest(m *testing.M) { - os.Exit(mainTest(m)) -} diff --git a/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run.yml b/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run.yml index 342adb2a04..49f71ce3fc 100644 --- a/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run.yml +++ b/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run.yml @@ -1,9 +1,29 @@ # type ActionRun struct { -# ID int64 `xorm:"pk autoincr"` -# RepoID int64 `xorm:"index"` -# Index int64 +# ID int64 `xorm:"pk autoincr"` +# RepoID int64 `xorm:"index"` +# Index int64 +# CommitSHA string `xorm:"commit_sha"` +# Event string +# TriggerEvent string +# EventPayload string `xorm:"LONGTEXT"` # } - - id: 106 - repo_id: 1 + id: 990 + repo_id: 100 index: 7 + commit_sha: merge-sha + event: pull_request + event_payload: '{"pull_request":{"head":{"sha":"sha-shared"}}}' +- + id: 991 + repo_id: 100 + index: 8 + commit_sha: sha-shared + event: push + event_payload: '{"head_commit":{"id":"sha-shared"}}' +- + id: 1991 + repo_id: 100 + index: 9 + commit_sha: sha-other + event: release diff --git a/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run_job.yml b/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run_job.yml index 4f90a4495c..addf5e0682 100644 --- a/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run_job.yml +++ b/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run_job.yml @@ -3,8 +3,14 @@ # RunID int64 `xorm:"index"` # } - - id: 530 - run_id: 106 + id: 997 + run_id: 990 - - id: 531 - run_id: 106 + id: 998 + run_id: 990 +- + id: 1997 + run_id: 991 +- + id: 1998 + run_id: 1991 diff --git a/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status.yml b/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status.yml index ceff4c9993..6be1c7ca48 100644 --- a/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status.yml +++ b/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status.yml @@ -1,29 +1,51 @@ # type CommitStatus struct { # ID int64 `xorm:"pk autoincr"` # RepoID int64 `xorm:"index"` +# SHA string # TargetURL string # } - - id: 10 - repo_id: 1 + id: 10010 + repo_id: 100 + sha: sha-shared target_url: /testuser/repo1/actions/runs/7/jobs/0 - - id: 11 - repo_id: 1 + id: 10011 + repo_id: 100 + sha: sha-shared target_url: /testuser/repo1/actions/runs/7/jobs/1 - - id: 12 - repo_id: 1 + id: 10012 + repo_id: 100 + sha: sha-shared + target_url: /testuser/repo1/actions/runs/8/jobs/0 +- + id: 10013 + repo_id: 100 + sha: sha-other + target_url: /testuser/repo1/actions/runs/9/jobs/0 +- + id: 10014 + repo_id: 100 + sha: sha-shared target_url: /otheruser/badrepo/actions/runs/7/jobs/0 - - id: 13 - repo_id: 1 + id: 10015 + repo_id: 100 + sha: sha-shared target_url: /testuser/repo1/actions/runs/10/jobs/0 - - id: 14 - repo_id: 1 + id: 10016 + repo_id: 100 + sha: sha-shared target_url: /testuser/repo1/actions/runs/7/jobs/3 - - id: 15 - repo_id: 1 + id: 10017 + repo_id: 100 + sha: sha-shared target_url: https://ci.example.com/build/123 +- + id: 10018 + repo_id: 100 + sha: sha-shared + target_url: /testuser/repo1/actions/runs/990/jobs/997 diff --git a/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status_summary.yml b/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status_summary.yml index 580b2a4f04..3dd846bb36 100644 --- a/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status_summary.yml +++ b/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status_summary.yml @@ -6,14 +6,14 @@ # TargetURL string # } - - id: 20 - repo_id: 1 - sha: "012345" - state: success + id: 10020 + repo_id: 100 + sha: sha-shared + state: pending target_url: /testuser/repo1/actions/runs/7/jobs/0 - - id: 21 - repo_id: 1 - sha: "678901" - state: success - target_url: https://ci.example.com/build/123 + id: 10021 + repo_id: 100 + sha: sha-other + state: pending + target_url: /testuser/repo1/actions/runs/9/jobs/0 diff --git a/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/repository.yml b/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/repository.yml index 86cfb926e4..46162e7803 100644 --- a/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/repository.yml +++ b/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/repository.yml @@ -4,6 +4,6 @@ # Name string # } - - id: 1 + id: 100 owner_name: testuser name: repo1 diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index bb8dad5ec6..3689f448d8 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" @@ -400,10 +401,16 @@ func prepareMigrationTasks() []*migration { newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency), newMigration(324, "Fix closed milestone completeness for milestones with no issues", v1_26.FixClosedMilestoneCompleteness), newMigration(325, "Fix missed repo_id when migrate attachments", v1_26.FixMissedRepoIDWhenMigrateAttachments), - newMigration(326, "Migrate commit status target URL to use run ID and job ID", v1_26.FixCommitStatusTargetURLToUseRunAndJobID), + newMigration(326, "Partially migrate commit status target URL to use run ID and job ID", v1_26.FixCommitStatusTargetURLToUseRunAndJobID), newMigration(327, "Add disabled state to action runners", v1_26.AddDisabledToActionRunner), 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), + newMigration(332, "Add last_sync_unix to mirror", v1_27.AddLastSyncUnixToMirror), + newMigration(333, "Add bypass allowlist to branch protection", v1_27.AddBranchProtectionBypassAllowlist), } return preparedMigrations } diff --git a/models/migrations/migrationtest/tests.go b/models/migrations/migrationtest/tests.go new file mode 100644 index 0000000000..e0f7d04bb0 --- /dev/null +++ b/models/migrations/migrationtest/tests.go @@ -0,0 +1,121 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migrationtest + +import ( + "os" + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/testlogger" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +// PrepareTestEnv prepares the test environment and reset the database. The skip parameter should usually be 0. +// Provide models to be sync'd with the database - in particular any models you expect fixtures to be loaded from. +// +// fixtures in `models/migrations/fixtures/` will be loaded automatically +func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, func()) { + t.Helper() + ourSkip := 2 + ourSkip += skip + deferFn := testlogger.PrintCurrentTest(t, ourSkip) + giteaRoot := setting.GetGiteaTestSourceRoot() + require.NoError(t, unittest.SyncDirs(filepath.Join(giteaRoot, "tests/gitea-repositories-meta"), setting.RepoRootPath)) + + cleanup, err := unittest.ResetTestDatabase() + if err != nil { + t.Fatalf("unable to reset database: %v", err) + return nil, deferFn + } + { + oldDefer := deferFn + deferFn = func() { + cleanup() + oldDefer() + } + } + + err = db.InitEngine(t.Context()) + if !assert.NoError(t, err) { + return nil, deferFn + } + x := unittest.GetXORMEngine() + { + oldDefer := deferFn + deferFn = func() { + _ = x.Close() + oldDefer() + } + } + + if len(syncModels) > 0 { + if err := x.Sync(syncModels...); err != nil { + t.Errorf("error during sync: %v", err) + return x, deferFn + } + } + + fixturesDir := filepath.Join(giteaRoot, "models", "migrations", "fixtures", t.Name()) + + if _, err := os.Stat(fixturesDir); err == nil { + t.Logf("initializing fixtures from: %s", fixturesDir) + if err := unittest.InitFixtures( + unittest.FixturesOptions{ + Dir: fixturesDir, + }); err != nil { + t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err) + return x, deferFn + } + if err := unittest.LoadFixtures(); err != nil { + t.Errorf("error whilst loading fixtures from %s: %v", fixturesDir, err) + return x, deferFn + } + } else if !os.IsNotExist(err) { + t.Errorf("unexpected error whilst checking for existence of fixtures: %v", err) + } else { + t.Logf("no fixtures found in: %s", fixturesDir) + } + + return x, deferFn +} + +func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table { + tables, err := x.DBMetas() + require.NoError(t, err) + tableMap := make(map[string]*schemas.Table) + for _, table := range tables { + tableMap[table.Name] = table + } + return tableMap +} + +func mainTest(m *testing.M) int { + testlogger.Init() + err := setting.PrepareIntegrationTestConfig() + if err != nil { + return testlogger.MainErrorf("Unable to prepare integration test config: %v", err) + } + setting.SetupGiteaTestEnv() + + if err = git.InitFull(); err != nil { + return testlogger.MainErrorf("Unable to InitFull: %v", err) + } + setting.Database.SlowQueryThreshold = 0 + setting.LoadDBSetting() + setting.InitLoggersForTest() + return m.Run() +} + +func MainTest(m *testing.M) { + os.Exit(mainTest(m)) +} diff --git a/models/migrations/v1_14/main_test.go b/models/migrations/v1_14/main_test.go index 978f88577c..6ed240c407 100644 --- a/models/migrations/v1_14/main_test.go +++ b/models/migrations/v1_14/main_test.go @@ -6,9 +6,9 @@ package v1_14 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_14/v176_test.go b/models/migrations/v1_14/v176_test.go index 5c1db4db71..aa57b5ad1d 100644 --- a/models/migrations/v1_14/v176_test.go +++ b/models/migrations/v1_14/v176_test.go @@ -6,7 +6,7 @@ package v1_14 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -47,7 +47,7 @@ func Test_RemoveInvalidLabels(t *testing.T) { } // load and prepare the test database - x, deferable := base.PrepareTestEnv(t, 0, new(Comment), new(Issue), new(Repository), new(IssueLabel), new(Label)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Comment), new(Issue), new(Repository), new(IssueLabel), new(Label)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_14/v177_test.go b/models/migrations/v1_14/v177_test.go index 263f69f338..a86fb98830 100644 --- a/models/migrations/v1_14/v177_test.go +++ b/models/migrations/v1_14/v177_test.go @@ -6,7 +6,7 @@ package v1_14 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" @@ -34,7 +34,7 @@ func Test_DeleteOrphanedIssueLabels(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(IssueLabel), new(Label)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(IssueLabel), new(Label)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_15/main_test.go b/models/migrations/v1_15/main_test.go index d01585e997..768bbd310b 100644 --- a/models/migrations/v1_15/main_test.go +++ b/models/migrations/v1_15/main_test.go @@ -6,9 +6,9 @@ package v1_15 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_15/v181_test.go b/models/migrations/v1_15/v181_test.go index 73b5c1f3d6..e230c684ea 100644 --- a/models/migrations/v1_15/v181_test.go +++ b/models/migrations/v1_15/v181_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -20,7 +20,7 @@ func Test_AddPrimaryEmail2EmailAddress(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(User)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(User)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_15/v182_test.go b/models/migrations/v1_15/v182_test.go index 5fc6a0c467..c0a1378534 100644 --- a/models/migrations/v1_15/v182_test.go +++ b/models/migrations/v1_15/v182_test.go @@ -6,7 +6,7 @@ package v1_15 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -20,7 +20,7 @@ func Test_AddIssueResourceIndexTable(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(Issue)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Issue)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_16/main_test.go b/models/migrations/v1_16/main_test.go index 7f93d6e9e5..c54424788d 100644 --- a/models/migrations/v1_16/main_test.go +++ b/models/migrations/v1_16/main_test.go @@ -6,9 +6,9 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_16/v189_test.go b/models/migrations/v1_16/v189_test.go index fb56ac8e11..44424dd369 100644 --- a/models/migrations/v1_16/v189_test.go +++ b/models/migrations/v1_16/v189_test.go @@ -6,7 +6,7 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/json" "github.com/stretchr/testify/assert" @@ -27,7 +27,7 @@ func (ls *LoginSourceOriginalV189) TableName() string { func Test_UnwrapLDAPSourceCfg(t *testing.T) { // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(LoginSourceOriginalV189)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(LoginSourceOriginalV189)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go index 2e827f0550..f68dd6d92d 100644 --- a/models/migrations/v1_16/v193_test.go +++ b/models/migrations/v1_16/v193_test.go @@ -6,7 +6,7 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -31,7 +31,7 @@ func Test_AddRepoIDForAttachment(t *testing.T) { } // Prepare and load the testing database - x, deferrable := base.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) + x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) defer deferrable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_16/v195_test.go b/models/migrations/v1_16/v195_test.go index 946e06e399..bbfa5e162a 100644 --- a/models/migrations/v1_16/v195_test.go +++ b/models/migrations/v1_16/v195_test.go @@ -6,7 +6,7 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -21,7 +21,7 @@ func Test_AddTableCommitStatusIndex(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(CommitStatus)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(CommitStatus)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_16/v210_test.go b/models/migrations/v1_16/v210_test.go index 3b4ac7aa4b..7bff2572e1 100644 --- a/models/migrations/v1_16/v210_test.go +++ b/models/migrations/v1_16/v210_test.go @@ -6,7 +6,7 @@ package v1_16 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" @@ -44,7 +44,7 @@ func Test_RemigrateU2FCredentials(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(WebauthnCredential), new(U2fRegistration), new(ExpectedWebauthnCredential)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(WebauthnCredential), new(U2fRegistration), new(ExpectedWebauthnCredential)) if x == nil || t.Failed() { defer deferable() return diff --git a/models/migrations/v1_17/main_test.go b/models/migrations/v1_17/main_test.go index 571a4f55a3..8652201871 100644 --- a/models/migrations/v1_17/main_test.go +++ b/models/migrations/v1_17/main_test.go @@ -6,9 +6,9 @@ package v1_17 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_17/v221_test.go b/models/migrations/v1_17/v221_test.go index a2dc0fae55..6fda9b9980 100644 --- a/models/migrations/v1_17/v221_test.go +++ b/models/migrations/v1_17/v221_test.go @@ -7,7 +7,7 @@ import ( "encoding/base32" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -38,7 +38,7 @@ func Test_StoreWebauthnCredentialIDAsBytes(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(WebauthnCredential), new(ExpectedWebauthnCredential)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(WebauthnCredential), new(ExpectedWebauthnCredential)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_18/main_test.go b/models/migrations/v1_18/main_test.go index ebcfb45a94..b8641526f3 100644 --- a/models/migrations/v1_18/main_test.go +++ b/models/migrations/v1_18/main_test.go @@ -6,9 +6,9 @@ package v1_18 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_18/v229_test.go b/models/migrations/v1_18/v229_test.go index 5722dd3557..638983ad0b 100644 --- a/models/migrations/v1_18/v229_test.go +++ b/models/migrations/v1_18/v229_test.go @@ -7,7 +7,7 @@ import ( "testing" "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -16,7 +16,7 @@ func Test_UpdateOpenMilestoneCounts(t *testing.T) { type ExpectedMilestone issues.Milestone // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(issues.Milestone), new(ExpectedMilestone), new(issues.Issue)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(issues.Milestone), new(ExpectedMilestone), new(issues.Issue)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_18/v230_test.go b/models/migrations/v1_18/v230_test.go index 25b2f6525d..e5e28ea63f 100644 --- a/models/migrations/v1_18/v230_test.go +++ b/models/migrations/v1_18/v230_test.go @@ -6,7 +6,7 @@ package v1_18 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -18,7 +18,7 @@ func Test_AddConfidentialClientColumnToOAuth2ApplicationTable(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(oauth2Application)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(oauth2Application)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_19/main_test.go b/models/migrations/v1_19/main_test.go index 87e807be6e..784ca0e46e 100644 --- a/models/migrations/v1_19/main_test.go +++ b/models/migrations/v1_19/main_test.go @@ -6,9 +6,9 @@ package v1_19 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_19/v233.go b/models/migrations/v1_19/v233.go index 9eb6d40509..44ced874b3 100644 --- a/models/migrations/v1_19/v233.go +++ b/models/migrations/v1_19/v233.go @@ -9,7 +9,6 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" "xorm.io/builder" "xorm.io/xorm" @@ -129,11 +128,11 @@ func AddHeaderAuthorizationEncryptedColWebhook(x *xorm.Engine) error { } type MatrixPayloadSafe struct { - Body string `json:"body"` - MsgType string `json:"msgtype"` - Format string `json:"format"` - FormattedBody string `json:"formatted_body"` - Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"` + Body string `json:"body"` + MsgType string `json:"msgtype"` + Format string `json:"format"` + FormattedBody string `json:"formatted_body"` + Commits json.Value `json:"io.gitea.commits,omitempty"` } type MatrixPayloadUnsafe struct { MatrixPayloadSafe diff --git a/models/migrations/v1_19/v233_test.go b/models/migrations/v1_19/v233_test.go index 7436ff7483..3f7900c58f 100644 --- a/models/migrations/v1_19/v233_test.go +++ b/models/migrations/v1_19/v233_test.go @@ -6,7 +6,7 @@ package v1_19 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" @@ -39,7 +39,7 @@ func Test_AddHeaderAuthorizationEncryptedColWebhook(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(Webhook), new(ExpectedWebhook), new(HookTask)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Webhook), new(ExpectedWebhook), new(HookTask)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_20/main_test.go b/models/migrations/v1_20/main_test.go index 2fd63a7118..3ceb9a3c66 100644 --- a/models/migrations/v1_20/main_test.go +++ b/models/migrations/v1_20/main_test.go @@ -6,9 +6,9 @@ package v1_20 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_20/v259_test.go b/models/migrations/v1_20/v259_test.go index 0bf63719e5..3864eecb78 100644 --- a/models/migrations/v1_20/v259_test.go +++ b/models/migrations/v1_20/v259_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -66,7 +66,7 @@ func Test_ConvertScopedAccessTokens(t *testing.T) { }) } - x, deferable := base.PrepareTestEnv(t, 0, new(AccessToken)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(AccessToken)) defer deferable() if x == nil || t.Failed() { t.Skip() diff --git a/models/migrations/v1_21/main_test.go b/models/migrations/v1_21/main_test.go index 536a7ade08..daf98d40f4 100644 --- a/models/migrations/v1_21/main_test.go +++ b/models/migrations/v1_21/main_test.go @@ -6,9 +6,9 @@ package v1_21 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_22/main_test.go b/models/migrations/v1_22/main_test.go index ac8facd6aa..e02c8a5328 100644 --- a/models/migrations/v1_22/main_test.go +++ b/models/migrations/v1_22/main_test.go @@ -6,9 +6,9 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_22/v283_test.go b/models/migrations/v1_22/v283_test.go index 743f860466..8e4c9410bd 100644 --- a/models/migrations/v1_22/v283_test.go +++ b/models/migrations/v1_22/v283_test.go @@ -6,7 +6,7 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -21,7 +21,7 @@ func Test_AddCombinedIndexToIssueUser(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(IssueUser)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(IssueUser)) defer deferable() assert.NoError(t, AddCombinedIndexToIssueUser(x)) diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go index b4a50f6fcb..1bd7fac2f1 100644 --- a/models/migrations/v1_22/v286_test.go +++ b/models/migrations/v1_22/v286_test.go @@ -6,7 +6,7 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" "xorm.io/xorm" @@ -64,7 +64,7 @@ func PrepareOldRepository(t *testing.T) (*xorm.Engine, func()) { } // Prepare and load the testing database - return base.PrepareTestEnv(t, 0, + return migrationtest.PrepareTestEnv(t, 0, new(Repository), new(CommitStatus), new(RepoArchiver), diff --git a/models/migrations/v1_22/v287_test.go b/models/migrations/v1_22/v287_test.go index 2b42a33c38..21946a662a 100644 --- a/models/migrations/v1_22/v287_test.go +++ b/models/migrations/v1_22/v287_test.go @@ -7,7 +7,7 @@ import ( "strconv" "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -20,7 +20,7 @@ func Test_UpdateBadgeColName(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(Badge)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Badge)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_22/v293_test.go b/models/migrations/v1_22/v293_test.go index c7b643c7e0..bc3a33055c 100644 --- a/models/migrations/v1_22/v293_test.go +++ b/models/migrations/v1_22/v293_test.go @@ -6,7 +6,7 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/models/project" "github.com/stretchr/testify/assert" @@ -14,7 +14,7 @@ import ( func Test_CheckProjectColumnsConsistency(t *testing.T) { // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Column)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(project.Project), new(project.Column)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_22/v294_test.go b/models/migrations/v1_22/v294_test.go index 1cf03d6120..a711b5ec5f 100644 --- a/models/migrations/v1_22/v294_test.go +++ b/models/migrations/v1_22/v294_test.go @@ -6,7 +6,7 @@ package v1_22 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" "xorm.io/xorm/schemas" @@ -20,7 +20,7 @@ func Test_AddUniqueIndexForProjectIssue(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(ProjectIssue)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ProjectIssue)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_23/main_test.go b/models/migrations/v1_23/main_test.go index f7b2caed83..ffccac0fd3 100644 --- a/models/migrations/v1_23/main_test.go +++ b/models/migrations/v1_23/main_test.go @@ -6,9 +6,9 @@ package v1_23 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_23/v302_test.go b/models/migrations/v1_23/v302_test.go index b008b6fc03..1832adf39a 100644 --- a/models/migrations/v1_23/v302_test.go +++ b/models/migrations/v1_23/v302_test.go @@ -6,7 +6,7 @@ package v1_23 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" @@ -44,7 +44,7 @@ func Test_AddIndexToActionTaskStoppedLogExpired(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(ActionTask)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ActionTask)) defer deferable() assert.NoError(t, AddIndexToActionTaskStoppedLogExpired(x)) diff --git a/models/migrations/v1_23/v304_test.go b/models/migrations/v1_23/v304_test.go index c3dfa5e7e7..9af84cd257 100644 --- a/models/migrations/v1_23/v304_test.go +++ b/models/migrations/v1_23/v304_test.go @@ -6,7 +6,7 @@ package v1_23 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" @@ -33,7 +33,7 @@ func Test_AddIndexForReleaseSha1(t *testing.T) { } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(Release)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Release)) defer deferable() assert.NoError(t, AddIndexForReleaseSha1(x)) diff --git a/models/migrations/v1_25/main_test.go b/models/migrations/v1_25/main_test.go index d2c4a4105d..33c981edb9 100644 --- a/models/migrations/v1_25/main_test.go +++ b/models/migrations/v1_25/main_test.go @@ -6,9 +6,9 @@ package v1_25 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_25/v321_test.go b/models/migrations/v1_25/v321_test.go index 3ef2c68aa3..0749a20e20 100644 --- a/models/migrations/v1_25/v321_test.go +++ b/models/migrations/v1_25/v321_test.go @@ -6,7 +6,7 @@ package v1_25 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -44,12 +44,12 @@ func Test_UseLongTextInSomeColumnsAndFixBugs(t *testing.T) { } // Prepare and load the testing database - x, deferrable := base.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice)) + x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice)) defer deferrable() require.NoError(t, UseLongTextInSomeColumnsAndFixBugs(x)) - tables := base.LoadTableSchemasMap(t, x) + tables := migrationtest.LoadTableSchemasMap(t, x) table := tables["review_state"] column := table.GetColumn("updated_files") assert.Equal(t, "LONGTEXT", column.SQLType.Name) diff --git a/models/migrations/v1_25/v322_test.go b/models/migrations/v1_25/v322_test.go index 78d890704c..1964614035 100644 --- a/models/migrations/v1_25/v322_test.go +++ b/models/migrations/v1_25/v322_test.go @@ -6,7 +6,7 @@ package v1_25 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -23,11 +23,11 @@ func Test_ExtendCommentTreePathLength(t *testing.T) { TreePath string `xorm:"VARCHAR(255)"` } - x, deferrable := base.PrepareTestEnv(t, 0, new(Comment)) + x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(Comment)) defer deferrable() require.NoError(t, ExtendCommentTreePathLength(x)) - table := base.LoadTableSchemasMap(t, x)["comment"] + table := migrationtest.LoadTableSchemasMap(t, x)["comment"] column := table.GetColumn("tree_path") assert.Contains(t, []string{"NVARCHAR", "VARCHAR"}, column.SQLType.Name) assert.EqualValues(t, 4000, column.Length) diff --git a/models/migrations/v1_26/main_test.go b/models/migrations/v1_26/main_test.go index 5aa12d553c..0b271b9bbc 100644 --- a/models/migrations/v1_26/main_test.go +++ b/models/migrations/v1_26/main_test.go @@ -6,9 +6,9 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" ) func TestMain(m *testing.M) { - base.MainTest(m) + migrationtest.MainTest(m) } diff --git a/models/migrations/v1_26/v325_test.go b/models/migrations/v1_26/v325_test.go index d4a66fee81..3fd658e01b 100644 --- a/models/migrations/v1_26/v325_test.go +++ b/models/migrations/v1_26/v325_test.go @@ -6,7 +6,7 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/require" @@ -38,7 +38,7 @@ func Test_FixMissedRepoIDWhenMigrateAttachments(t *testing.T) { } // Prepare and load the testing database - x, deferrable := base.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) + x, deferrable := migrationtest.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) defer deferrable() require.NoError(t, FixMissedRepoIDWhenMigrateAttachments(x)) diff --git a/models/migrations/v1_26/v326.go b/models/migrations/v1_26/v326.go index 1ec0af76a0..dcf548bec0 100644 --- a/models/migrations/v1_26/v326.go +++ b/models/migrations/v1_26/v326.go @@ -4,18 +4,32 @@ package v1_26 import ( + "errors" "fmt" "net/url" "strconv" "strings" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + webhook_module "code.gitea.io/gitea/modules/webhook" "xorm.io/xorm" ) -const actionsRunPath = "/actions/runs/" +const ( + actionsRunPath = "/actions/runs/" + + // Only commit status target URLs whose resolved run ID is smaller than this threshold are rewritten by this partial migration. + // The fixed value 1000 is a conservative cutoff chosen to cover the smaller legacy run indexes that are most likely to be confused with ID-based URLs at runtime. + // Larger legacy {run} or {job} numbers are usually easier to disambiguate. For example: + // * /actions/runs/1200/jobs/1420 is most likely an ID-based URL, because a run should not contain more than 256 jobs. + // * /actions/runs/1500/jobs/3 is most likely an index-based URL, because a job ID cannot be smaller than its run ID. + // But URLs with small numbers, such as /actions/runs/5/jobs/6, are much harder to distinguish reliably. + // This migration therefore prioritizes rewriting target URLs for runs in that lower range. + legacyURLIDThreshold int64 = 1000 +) type migrationRepository struct { ID int64 @@ -24,9 +38,13 @@ type migrationRepository struct { } type migrationActionRun struct { - ID int64 - RepoID int64 - Index int64 + ID int64 + RepoID int64 + Index int64 + CommitSHA string `xorm:"commit_sha"` + Event webhook_module.HookEventType + TriggerEvent string + EventPayload string } type migrationActionRunJob struct { @@ -40,93 +58,156 @@ type migrationCommitStatus struct { TargetURL string } -func FixCommitStatusTargetURLToUseRunAndJobID(x *xorm.Engine) error { - runByIndexCache := make(map[int64]map[int64]*migrationActionRun) - jobsByRunIDCache := make(map[int64][]int64) - repoLinkCache := make(map[int64]string) - - if err := migrateCommitStatusTargetURL(x, "commit_status", runByIndexCache, jobsByRunIDCache, repoLinkCache); err != nil { - return err - } - return migrateCommitStatusTargetURL(x, "commit_status_summary", runByIndexCache, jobsByRunIDCache, repoLinkCache) +// Frozen subsets of modules/structs payload types, decoded from stored +// action_run.event_payload values. Inlined so the migration is insulated +// from future field changes in modules/structs. +type migrationPayloadCommit struct { + ID string `json:"id"` } -func migrateCommitStatusTargetURL( +type migrationPushPayload struct { + HeadCommit *migrationPayloadCommit `json:"head_commit"` +} + +type migrationPRBranchInfo struct { + Sha string `json:"sha"` +} + +type migrationPullRequest struct { + Head *migrationPRBranchInfo `json:"head"` +} + +type migrationPullRequestPayload struct { + PullRequest *migrationPullRequest `json:"pull_request"` +} + +type commitSHAAndRuns struct { + commitSHA string + runs map[int64]*migrationActionRun +} + +// FixCommitStatusTargetURLToUseRunAndJobID partially migrates legacy Actions +// commit status target URLs to the new run/job ID-based form. +// +// Only rows whose resolved run ID is below legacyURLIDThreshold are rewritten. +// This is because smaller legacy run indexes are more likely to collide with run ID URLs during runtime resolution, +// so this migration prioritizes that lower range and leaves the remaining legacy target URLs to the web compatibility logic. +func FixCommitStatusTargetURLToUseRunAndJobID(x *xorm.Engine) error { + jobsByRunIDCache := make(map[int64][]int64) + repoLinkCache := make(map[int64]string) + groups, err := loadLegacyMigrationRunGroups(x) + if err != nil { + return err + } + + for repoID, groupsBySHA := range groups { + for _, group := range groupsBySHA { + if err := migrateCommitStatusTargetURLForGroup(x, "commit_status", repoID, group.commitSHA, group.runs, jobsByRunIDCache, repoLinkCache); err != nil { + return err + } + if err := migrateCommitStatusTargetURLForGroup(x, "commit_status_summary", repoID, group.commitSHA, group.runs, jobsByRunIDCache, repoLinkCache); err != nil { + return err + } + } + } + return nil +} + +func loadLegacyMigrationRunGroups(x *xorm.Engine) (map[int64]map[string]*commitSHAAndRuns, error) { + var runs []migrationActionRun + if err := x.Table("action_run"). + Where("id < ?", legacyURLIDThreshold). + Cols("id", "repo_id", "`index`", "commit_sha", "event", "trigger_event", "event_payload"). + Find(&runs); err != nil { + return nil, fmt.Errorf("query action_run: %w", err) + } + + groups := make(map[int64]map[string]*commitSHAAndRuns) + for i := range runs { + run := runs[i] + commitID, err := getCommitStatusCommitID(&run) + if err != nil { + log.Warn("skip action_run id=%d when resolving commit status commit SHA: %v", run.ID, err) + continue + } + if commitID == "" { + // empty commitID means the run didn't create any commit status records, just skip + continue + } + if groups[run.RepoID] == nil { + groups[run.RepoID] = make(map[string]*commitSHAAndRuns) + } + if groups[run.RepoID][commitID] == nil { + groups[run.RepoID][commitID] = &commitSHAAndRuns{ + commitSHA: commitID, + runs: make(map[int64]*migrationActionRun), + } + } + groups[run.RepoID][commitID].runs[run.Index] = &run + } + return groups, nil +} + +func migrateCommitStatusTargetURLForGroup( x *xorm.Engine, table string, - runByIndexCache map[int64]map[int64]*migrationActionRun, + repoID int64, + sha string, + runs map[int64]*migrationActionRun, jobsByRunIDCache map[int64][]int64, repoLinkCache map[int64]string, ) error { - const batchSize = 500 - var lastID int64 + var rows []migrationCommitStatus + if err := x.Table(table). + Where("repo_id = ?", repoID). + And("sha = ?", sha). + Cols("id", "repo_id", "target_url"). + Find(&rows); err != nil { + return fmt.Errorf("query %s for repo_id=%d sha=%s: %w", table, repoID, sha, err) + } - for { - var rows []migrationCommitStatus - sess := x.Table(table). - Where("target_url LIKE ?", "%"+actionsRunPath+"%"). - And("id > ?", lastID). - Asc("id"). - Limit(batchSize) - if err := sess.Find(&rows); err != nil { - return fmt.Errorf("query %s: %w", table, err) - } - if len(rows) == 0 { - return nil + for _, row := range rows { + repoLink, err := getRepoLinkCached(x, repoLinkCache, row.RepoID) + if err != nil || repoLink == "" { + if err != nil { + log.Warn("convert %s id=%d getRepoLinkCached: %v", table, row.ID, err) + } else { + log.Warn("convert %s id=%d: repo=%d not found", table, row.ID, row.RepoID) + } + continue } - for _, row := range rows { - lastID = row.ID - if row.TargetURL == "" { - continue - } + runNum, jobNum, ok := parseTargetURL(row.TargetURL, repoLink) + if !ok { + continue + } - repoLink, err := getRepoLinkCached(x, repoLinkCache, row.RepoID) - if err != nil || repoLink == "" { - if err != nil { - log.Warn("convert %s id=%d getRepoLinkCached: %v", table, row.ID, err) - } else { - log.Warn("convert %s id=%d: repo=%d not found", table, row.ID, row.RepoID) - } - continue - } + run, ok := runs[runNum] + if !ok { + continue + } - runNum, jobNum, ok := parseTargetURL(row.TargetURL, repoLink) - if !ok { - continue + jobID, ok, err := getJobIDByIndexCached(x, jobsByRunIDCache, run.ID, jobNum) + if err != nil || !ok { + if err != nil { + log.Warn("convert %s id=%d getJobIDByIndexCached: %v", table, row.ID, err) + } else { + log.Warn("convert %s id=%d: job not found for run_id=%d job_index=%d", table, row.ID, run.ID, jobNum) } + continue + } - run, err := getRunByIndexCached(x, runByIndexCache, row.RepoID, runNum) - if err != nil || run == nil { - if err != nil { - log.Warn("convert %s id=%d getRunByIndexCached: %v", table, row.ID, err) - } else { - log.Warn("convert %s id=%d: run not found for repo_id=%d run_index=%d", table, row.ID, row.RepoID, runNum) - } - continue - } + oldURL := row.TargetURL + newURL := fmt.Sprintf("%s%s%d/jobs/%d", repoLink, actionsRunPath, run.ID, jobID) + if oldURL == newURL { + continue + } - jobID, ok, err := getJobIDByIndexCached(x, jobsByRunIDCache, run.ID, jobNum) - if err != nil || !ok { - if err != nil { - log.Warn("convert %s id=%d getJobIDByIndexCached: %v", table, row.ID, err) - } else { - log.Warn("convert %s id=%d: job not found for run_id=%d job_index=%d", table, row.ID, run.ID, jobNum) - } - continue - } - - oldURL := row.TargetURL - newURL := fmt.Sprintf("%s%s%d/jobs/%d", repoLink, actionsRunPath, run.ID, jobID) // expect: {repo_link}/actions/runs/{run_id}/jobs/{job_id} - if oldURL == newURL { - continue - } - - if _, err := x.Table(table).ID(row.ID).Cols("target_url").Update(&migrationCommitStatus{TargetURL: newURL}); err != nil { - return fmt.Errorf("update %s id=%d target_url from %s to %s: %w", table, row.ID, oldURL, newURL, err) - } + if _, err := x.Table(table).ID(row.ID).Cols("target_url").Update(&migrationCommitStatus{TargetURL: newURL}); err != nil { + return fmt.Errorf("update %s id=%d target_url from %s to %s: %w", table, row.ID, oldURL, newURL, err) } } + return nil } func getRepoLinkCached(x *xorm.Engine, cache map[int64]string, repoID int64) (string, error) { @@ -147,35 +228,6 @@ func getRepoLinkCached(x *xorm.Engine, cache map[int64]string, repoID int64) (st return link, nil } -func getRunByIndexCached(x *xorm.Engine, cache map[int64]map[int64]*migrationActionRun, repoID, runIndex int64) (*migrationActionRun, error) { - if repoCache, ok := cache[repoID]; ok { - if run, ok := repoCache[runIndex]; ok { - if run == nil { - return nil, fmt.Errorf("run repo_id=%d run_index=%d not found", repoID, runIndex) - } - return run, nil - } - } - - var run migrationActionRun - has, err := x.Table("action_run").Where("repo_id=? AND `index`=?", repoID, runIndex).Get(&run) - if err != nil { - return nil, err - } - if !has { - if cache[repoID] == nil { - cache[repoID] = make(map[int64]*migrationActionRun) - } - cache[repoID][runIndex] = nil - return nil, fmt.Errorf("run repo_id=%d run_index=%d not found", repoID, runIndex) - } - if cache[repoID] == nil { - cache[repoID] = make(map[int64]*migrationActionRun) - } - cache[repoID][runIndex] = &run - return &run, nil -} - func getJobIDByIndexCached(x *xorm.Engine, cache map[int64][]int64, runID, jobIndex int64) (int64, bool, error) { jobIDs, ok := cache[runID] if !ok { @@ -202,7 +254,7 @@ func parseTargetURL(targetURL, repoLink string) (runNum, jobNum int64, ok bool) } rest := targetURL[len(prefix):] - parts := strings.Split(rest, "/") // expect: {run_num}/jobs/{job_num} + parts := strings.Split(rest, "/") if len(parts) == 3 && parts[1] == "jobs" { runNum, err1 := strconv.ParseInt(parts[0], 10, 64) jobNum, err2 := strconv.ParseInt(parts[2], 10, 64) @@ -214,3 +266,72 @@ func parseTargetURL(targetURL, repoLink string) (runNum, jobNum int64, ok bool) return 0, 0, false } + +func getCommitStatusCommitID(run *migrationActionRun) (string, error) { + switch run.Event { + case webhook_module.HookEventPush: + payload, err := getPushEventPayload(run) + if err != nil { + return "", fmt.Errorf("getPushEventPayload: %w", err) + } + if payload.HeadCommit == nil { + return "", errors.New("head commit is missing in event payload") + } + return payload.HeadCommit.ID, nil + case webhook_module.HookEventPullRequest, + webhook_module.HookEventPullRequestSync, + webhook_module.HookEventPullRequestAssign, + webhook_module.HookEventPullRequestLabel, + webhook_module.HookEventPullRequestReviewRequest, + webhook_module.HookEventPullRequestMilestone: + payload, err := getPullRequestEventPayload(run) + if err != nil { + return "", fmt.Errorf("getPullRequestEventPayload: %w", err) + } + if payload.PullRequest == nil { + return "", errors.New("pull request is missing in event payload") + } else if payload.PullRequest.Head == nil { + return "", errors.New("head of pull request is missing in event payload") + } + return payload.PullRequest.Head.Sha, nil + case webhook_module.HookEventPullRequestReviewApproved, + webhook_module.HookEventPullRequestReviewRejected, + webhook_module.HookEventPullRequestReviewComment: + payload, err := getPullRequestEventPayload(run) + if err != nil { + return "", fmt.Errorf("getPullRequestEventPayload: %w", err) + } + if payload.PullRequest == nil { + return "", errors.New("pull request is missing in event payload") + } else if payload.PullRequest.Head == nil { + return "", errors.New("head of pull request is missing in event payload") + } + return payload.PullRequest.Head.Sha, nil + case webhook_module.HookEventRelease: + return run.CommitSHA, nil + default: + return "", nil + } +} + +func getPushEventPayload(run *migrationActionRun) (*migrationPushPayload, error) { + if run.Event != webhook_module.HookEventPush { + return nil, fmt.Errorf("event %s is not a push event", run.Event) + } + var payload migrationPushPayload + if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { + return nil, err + } + return &payload, nil +} + +func getPullRequestEventPayload(run *migrationActionRun) (*migrationPullRequestPayload, error) { + if !run.Event.IsPullRequest() && !run.Event.IsPullRequestReview() { + return nil, fmt.Errorf("event %s is not a pull request event", run.Event) + } + var payload migrationPullRequestPayload + if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { + return nil, err + } + return &payload, nil +} diff --git a/models/migrations/v1_26/v326_test.go b/models/migrations/v1_26/v326_test.go index ddc2640160..a0225eb774 100644 --- a/models/migrations/v1_26/v326_test.go +++ b/models/migrations/v1_26/v326_test.go @@ -6,7 +6,7 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" @@ -28,9 +28,13 @@ func Test_FixCommitStatusTargetURLToUseRunAndJobID(t *testing.T) { } type ActionRun struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"index"` - Index int64 + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"index"` + Index int64 + CommitSHA string `xorm:"commit_sha"` + Event string + TriggerEvent string + EventPayload string `xorm:"LONGTEXT"` } type ActionRunJob struct { @@ -41,6 +45,7 @@ func Test_FixCommitStatusTargetURLToUseRunAndJobID(t *testing.T) { type CommitStatus struct { ID int64 `xorm:"pk autoincr"` RepoID int64 `xorm:"index"` + SHA string TargetURL string } @@ -52,7 +57,7 @@ func Test_FixCommitStatusTargetURLToUseRunAndJobID(t *testing.T) { TargetURL string } - x, deferable := base.PrepareTestEnv(t, 0, + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Repository), new(ActionRun), new(ActionRunJob), @@ -61,14 +66,6 @@ func Test_FixCommitStatusTargetURLToUseRunAndJobID(t *testing.T) { ) defer deferable() - newURL1 := "/testuser/repo1/actions/runs/106/jobs/530" - newURL2 := "/testuser/repo1/actions/runs/106/jobs/531" - - invalidWrongRepo := "/otheruser/badrepo/actions/runs/7/jobs/0" - invalidNonexistentRun := "/testuser/repo1/actions/runs/10/jobs/0" - invalidNonexistentJob := "/testuser/repo1/actions/runs/7/jobs/3" - externalTargetURL := "https://ci.example.com/build/123" - require.NoError(t, FixCommitStatusTargetURLToUseRunAndJobID(x)) cases := []struct { @@ -76,14 +73,26 @@ func Test_FixCommitStatusTargetURLToUseRunAndJobID(t *testing.T) { id int64 want string }{ - {table: "commit_status", id: 10, want: newURL1}, - {table: "commit_status", id: 11, want: newURL2}, - {table: "commit_status", id: 12, want: invalidWrongRepo}, - {table: "commit_status", id: 13, want: invalidNonexistentRun}, - {table: "commit_status", id: 14, want: invalidNonexistentJob}, - {table: "commit_status", id: 15, want: externalTargetURL}, - {table: "commit_status_summary", id: 20, want: newURL1}, - {table: "commit_status_summary", id: 21, want: externalTargetURL}, + // Legacy URLs for runs whose resolved run IDs are below the threshold should be rewritten. + {table: "commit_status", id: 10010, want: "/testuser/repo1/actions/runs/990/jobs/997"}, + {table: "commit_status", id: 10011, want: "/testuser/repo1/actions/runs/990/jobs/998"}, + {table: "commit_status", id: 10012, want: "/testuser/repo1/actions/runs/991/jobs/1997"}, + + // Runs whose resolved IDs are above the threshold are intentionally left unchanged. + {table: "commit_status", id: 10013, want: "/testuser/repo1/actions/runs/9/jobs/0"}, + + // URLs that do not resolve cleanly as legacy Actions URLs should remain untouched. + {table: "commit_status", id: 10014, want: "/otheruser/badrepo/actions/runs/7/jobs/0"}, + {table: "commit_status", id: 10015, want: "/testuser/repo1/actions/runs/10/jobs/0"}, + {table: "commit_status", id: 10016, want: "/testuser/repo1/actions/runs/7/jobs/3"}, + {table: "commit_status", id: 10017, want: "https://ci.example.com/build/123"}, + + // Already ID-based URLs are valid inputs and should not be rewritten again. + {table: "commit_status", id: 10018, want: "/testuser/repo1/actions/runs/990/jobs/997"}, + + // The same rewrite rules apply to commit_status_summary rows. + {table: "commit_status_summary", id: 10020, want: "/testuser/repo1/actions/runs/990/jobs/997"}, + {table: "commit_status_summary", id: 10021, want: "/testuser/repo1/actions/runs/9/jobs/0"}, } for _, tc := range cases { diff --git a/models/migrations/v1_26/v327_test.go b/models/migrations/v1_26/v327_test.go index 971707be4f..98e948cf05 100644 --- a/models/migrations/v1_26/v327_test.go +++ b/models/migrations/v1_26/v327_test.go @@ -6,7 +6,7 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/require" ) @@ -17,7 +17,7 @@ func Test_AddDisabledToActionRunner(t *testing.T) { Name string } - x, deferable := base.PrepareTestEnv(t, 0, new(ActionRunner)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ActionRunner)) defer deferable() _, err := x.Insert(&ActionRunner{Name: "runner"}) diff --git a/models/migrations/v1_26/v329_test.go b/models/migrations/v1_26/v329_test.go index cab8e79906..e4bebfb71d 100644 --- a/models/migrations/v1_26/v329_test.go +++ b/models/migrations/v1_26/v329_test.go @@ -6,7 +6,7 @@ package v1_26 import ( "testing" - "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/migrations/migrationtest" "github.com/stretchr/testify/assert" ) @@ -22,7 +22,7 @@ func (UserBadgeBefore) TableName() string { } func Test_AddUniqueIndexForUserBadge(t *testing.T) { - x, deferable := base.PrepareTestEnv(t, 0, new(UserBadgeBefore)) + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(UserBadgeBefore)) defer deferable() if x == nil || t.Failed() { return diff --git a/models/migrations/v1_26/v330.go b/models/migrations/v1_26/v330.go new file mode 100644 index 0000000000..9f77331090 --- /dev/null +++ b/models/migrations/v1_26/v330.go @@ -0,0 +1,16 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "xorm.io/xorm" +) + +func AddNameToWebhook(x *xorm.Engine) error { + type Webhook struct { + Name string `xorm:"VARCHAR(255) NOT NULL DEFAULT ''"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(Webhook)) + return err +} diff --git a/models/migrations/v1_27/main_test.go b/models/migrations/v1_27/main_test.go new file mode 100644 index 0000000000..0c6a6a2440 --- /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/migrationtest" +) + +func TestMain(m *testing.M) { + migrationtest.MainTest(m) +} diff --git a/models/migrations/v1_27/v331.go b/models/migrations/v1_27/v331.go new file mode 100644 index 0000000000..204b7b661e --- /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 0000000000..2302fee024 --- /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/migrationtest" + + "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 := migrationtest.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 := migrationtest.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/migrations/v1_27/v332.go b/models/migrations/v1_27/v332.go new file mode 100644 index 0000000000..f7e023a0cd --- /dev/null +++ b/models/migrations/v1_27/v332.go @@ -0,0 +1,21 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import "xorm.io/xorm" + +type mirrorWithLastSyncUnix struct { + LastSyncUnix int64 `xorm:"INDEX"` +} + +func (mirrorWithLastSyncUnix) TableName() string { + return "mirror" +} + +func AddLastSyncUnixToMirror(x *xorm.Engine) error { + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(mirrorWithLastSyncUnix)) + return err +} diff --git a/models/migrations/v1_27/v333.go b/models/migrations/v1_27/v333.go new file mode 100644 index 0000000000..0ffab0a1f2 --- /dev/null +++ b/models/migrations/v1_27/v333.go @@ -0,0 +1,20 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import "xorm.io/xorm" + +func AddBranchProtectionBypassAllowlist(x *xorm.Engine) error { + type ProtectedBranch struct { + EnableBypassAllowlist bool `xorm:"NOT NULL DEFAULT false"` + BypassAllowlistUserIDs []int64 `xorm:"JSON TEXT"` + BypassAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` + } + + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(ProtectedBranch)) + return err +} diff --git a/models/migrations/v1_27/v333_test.go b/models/migrations/v1_27/v333_test.go new file mode 100644 index 0000000000..768fa10261 --- /dev/null +++ b/models/migrations/v1_27/v333_test.go @@ -0,0 +1,60 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/migrationtest" + + "github.com/stretchr/testify/require" +) + +func Test_AddBranchProtectionBypassAllowlist(t *testing.T) { + type ProtectedBranch struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + BranchName string `xorm:"INDEX"` + EnableBypassAllowlist bool `xorm:"NOT NULL DEFAULT false"` + BypassAllowlistUserIDs []int64 `xorm:"JSON TEXT"` + BypassAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` + } + + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(ProtectedBranch)) + defer deferable() + + // Test with default values + _, err := x.Insert(&ProtectedBranch{RepoID: 1, BranchName: "main"}) + require.NoError(t, err) + + // Test with populated allowlist + _, err = x.Insert(&ProtectedBranch{ + RepoID: 1, + BranchName: "develop", + EnableBypassAllowlist: true, + BypassAllowlistUserIDs: []int64{1, 2, 3}, + BypassAllowlistTeamIDs: []int64{10, 20}, + }) + require.NoError(t, err) + + require.NoError(t, AddBranchProtectionBypassAllowlist(x)) + + // Verify the default values record + var pb ProtectedBranch + has, err := x.Where("repo_id = ? AND branch_name = ?", 1, "main").Get(&pb) + require.NoError(t, err) + require.True(t, has) + require.False(t, pb.EnableBypassAllowlist) + require.Nil(t, pb.BypassAllowlistUserIDs) + require.Nil(t, pb.BypassAllowlistTeamIDs) + + // Verify the populated allowlist record + var pb2 ProtectedBranch + has, err = x.Where("repo_id = ? AND branch_name = ?", 1, "develop").Get(&pb2) + require.NoError(t, err) + require.True(t, has) + require.True(t, pb2.EnableBypassAllowlist) + require.Equal(t, []int64{1, 2, 3}, pb2.BypassAllowlistUserIDs) + require.Equal(t, []int64{10, 20}, pb2.BypassAllowlistTeamIDs) +} diff --git a/models/migrations/v1_6/v71.go b/models/migrations/v1_6/v71.go index 2b11f57c92..b4dcd87eba 100644 --- a/models/migrations/v1_6/v71.go +++ b/models/migrations/v1_6/v71.go @@ -51,10 +51,7 @@ func AddScratchHash(x *xorm.Engine) error { for _, tfa := range tfas { // generate salt - salt, err := util.CryptoRandomString(10) - if err != nil { - return err - } + salt := util.CryptoRandomString(10) tfa.ScratchSalt = salt tfa.ScratchHash = base.HashToken(tfa.ScratchToken, salt) diff --git a/models/migrations/v1_9/v85.go b/models/migrations/v1_9/v85.go index 48e1cd5dc4..0e95a71f92 100644 --- a/models/migrations/v1_9/v85.go +++ b/models/migrations/v1_9/v85.go @@ -65,10 +65,7 @@ func HashAppToken(x *xorm.Engine) error { for _, token := range tokens { // generate salt - salt, err := util.CryptoRandomString(10) - if err != nil { - return err - } + salt := util.CryptoRandomString(10) token.TokenSalt = salt token.TokenHash = base.HashToken(token.Sha1, salt) if len(token.Sha1) < 8 { diff --git a/models/organization/org_test.go b/models/organization/org_test.go index 7a74c5f5fc..be27dbb96c 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -456,6 +456,22 @@ func TestGetUsersWhoCanCreateOrgRepo(t *testing.T) { assert.NotNil(t, users[5]) } +func TestCanCreateOrgRepoByOwnerTeamWithoutFlag(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + ownerTeam, err := org.GetOwnerTeam(t.Context()) + require.NoError(t, err) + + ownerTeam.CanCreateOrgRepo = false + _, err = db.GetEngine(t.Context()).ID(ownerTeam.ID).Cols("can_create_org_repo").Update(ownerTeam) + require.NoError(t, err) + + ok, err := organization.CanCreateOrgRepo(t.Context(), org.ID, 2) + require.NoError(t, err) + assert.True(t, ok) +} + func TestUser_RemoveOrgRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) diff --git a/models/organization/org_user.go b/models/organization/org_user.go index 69cd960944..7d0476606c 100644 --- a/models/organization/org_user.go +++ b/models/organization/org_user.go @@ -9,9 +9,7 @@ import ( "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" @@ -107,7 +105,7 @@ func IsPublicMembership(ctx context.Context, orgID, uid int64) (bool, error) { // CanCreateOrgRepo returns true if user can create repo in organization func CanCreateOrgRepo(ctx context.Context, orgID, uid int64) (bool, error) { return db.GetEngine(ctx). - Where(builder.Eq{"team.can_create_org_repo": true}). + Where(builder.Eq{"team.can_create_org_repo": true}.Or(builder.Eq{"team.authorize": perm.AccessModeOwner})). Join("INNER", "team_user", "team_user.team_id = team.id"). And("team_user.uid = ?", uid). And("team_user.org_id = ?", orgID). @@ -129,49 +127,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_invite.go b/models/organization/team_invite.go index 17f6c59610..186ae5f6e8 100644 --- a/models/organization/team_invite.go +++ b/models/organization/team_invite.go @@ -116,10 +116,7 @@ func CreateTeamInvite(ctx context.Context, doer *user_model.User, team *Team, em } } - token, err := util.CryptoRandomString(25) - if err != nil { - return nil, err - } + token := util.CryptoRandomString(25) invite := &TeamInvite{ Token: token, diff --git a/models/organization/team_list.go b/models/organization/team_list.go index 0274f9c5ba..5629cec366 100644 --- a/models/organization/team_list.go +++ b/models/organization/team_list.go @@ -88,7 +88,7 @@ func SearchTeam(ctx context.Context, opts *SearchTeamOptions) (TeamList, int64, sess = db.SetSessionPagination(sess, opts) teams := make([]*Team, 0, opts.PageSize) - count, err := sess.Where(cond).OrderBy("lower_name").FindAndCount(&teams) + count, err := sess.Where(cond).OrderBy("CASE WHEN name=? THEN '' ELSE lower_name END", OwnerTeamName).FindAndCount(&teams) if err != nil { return nil, 0, err } diff --git a/models/organization/team_unit.go b/models/organization/team_unit.go index c6ec6b39b2..b5237c2c58 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 d6d0a5054d..d24a4c5126 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/packages/descriptor.go b/models/packages/descriptor.go index ea0e0d5e73..2ef27051ee 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -9,6 +9,7 @@ import ( "fmt" "net/url" + "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/cache" @@ -53,8 +54,11 @@ func (l PackagePropertyList) GetByName(name string) string { // PackageDescriptor describes a package type PackageDescriptor struct { - Package *Package - Owner *user_model.User + // basic package info + Package *Package + Owner *user_model.User + + // package version info Repository *repo_model.Repository Version *PackageVersion SemVer *version.Version @@ -77,6 +81,11 @@ func (pd *PackageDescriptor) PackageWebLink() string { return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) } +// PackageSettingsLink returns the relative package settings link +func (pd *PackageDescriptor) PackageSettingsLink() string { + return fmt.Sprintf("%s/-/packages/settings/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) +} + // VersionWebLink returns the relative package version web link func (pd *PackageDescriptor) VersionWebLink() string { return fmt.Sprintf("%s/%s", pd.PackageWebLink(), url.PathEscape(pd.Version.LowerVersion)) @@ -203,6 +212,8 @@ func GetPackageDescriptorWithCache(ctx context.Context, pv *PackageVersion, c *c metadata = &rubygems.Metadata{} case TypeSwift: metadata = &swift.Metadata{} + case TypeTerraformState: + // terraform packages have no metadata case TypeVagrant: metadata = &vagrant.Metadata{} default: @@ -267,6 +278,15 @@ func GetPackageDescriptors(ctx context.Context, pvs []*PackageVersion) ([]*Packa return getPackageDescriptors(ctx, pvs, cache.NewEphemeralCache()) } +// GetAllPackageDescriptors gets all package descriptors for a package +func GetAllPackageDescriptors(ctx context.Context, p *Package) ([]*PackageDescriptor, error) { + pvs := make([]*PackageVersion, 0, 10) + if err := db.GetEngine(ctx).Where("package_id = ?", p.ID).Find(&pvs); err != nil { + return nil, err + } + return getPackageDescriptors(ctx, pvs, cache.NewEphemeralCache()) +} + func getPackageDescriptors(ctx context.Context, pvs []*PackageVersion, c *cache.EphemeralCache) ([]*PackageDescriptor, error) { pds := make([]*PackageDescriptor, 0, len(pvs)) for _, pv := range pvs { diff --git a/models/packages/package.go b/models/packages/package.go index 38d1cdcf66..17e5d4eee3 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -30,28 +30,29 @@ type Type string // List of supported packages const ( - TypeAlpine Type = "alpine" - TypeArch Type = "arch" - TypeCargo Type = "cargo" - TypeChef Type = "chef" - TypeComposer Type = "composer" - TypeConan Type = "conan" - TypeConda Type = "conda" - TypeContainer Type = "container" - TypeCran Type = "cran" - TypeDebian Type = "debian" - TypeGeneric Type = "generic" - TypeGo Type = "go" - TypeHelm Type = "helm" - TypeMaven Type = "maven" - TypeNpm Type = "npm" - TypeNuGet Type = "nuget" - TypePub Type = "pub" - TypePyPI Type = "pypi" - TypeRpm Type = "rpm" - TypeRubyGems Type = "rubygems" - TypeSwift Type = "swift" - TypeVagrant Type = "vagrant" + TypeAlpine Type = "alpine" + TypeArch Type = "arch" + TypeCargo Type = "cargo" + TypeChef Type = "chef" + TypeComposer Type = "composer" + TypeConan Type = "conan" + TypeConda Type = "conda" + TypeContainer Type = "container" + TypeCran Type = "cran" + TypeDebian Type = "debian" + TypeGeneric Type = "generic" + TypeGo Type = "go" + TypeHelm Type = "helm" + TypeMaven Type = "maven" + TypeNpm Type = "npm" + TypeNuGet Type = "nuget" + TypePub Type = "pub" + TypePyPI Type = "pypi" + TypeRpm Type = "rpm" + TypeRubyGems Type = "rubygems" + TypeSwift Type = "swift" + TypeTerraformState Type = "terraform" + TypeVagrant Type = "vagrant" ) var TypeList = []Type{ @@ -76,6 +77,7 @@ var TypeList = []Type{ TypeRpm, TypeRubyGems, TypeSwift, + TypeTerraformState, TypeVagrant, } @@ -124,6 +126,8 @@ func (pt Type) Name() string { return "RubyGems" case TypeSwift: return "Swift" + case TypeTerraformState: + return "Terraform State" case TypeVagrant: return "Vagrant" } @@ -175,6 +179,8 @@ func (pt Type) SVGName() string { return "gitea-rubygems" case TypeSwift: return "gitea-swift" + case TypeTerraformState: + return "gitea-terraform" case TypeVagrant: return "gitea-vagrant" } diff --git a/models/packages/package_blob_upload.go b/models/packages/package_blob_upload.go index 4b0e789221..60a55805a8 100644 --- a/models/packages/package_blob_upload.go +++ b/models/packages/package_blob_upload.go @@ -31,16 +31,13 @@ type PackageBlobUpload struct { // CreateBlobUpload inserts a blob upload func CreateBlobUpload(ctx context.Context) (*PackageBlobUpload, error) { - id, err := util.CryptoRandomString(25) - if err != nil { - return nil, err - } + id := util.CryptoRandomString(25) pbu := &PackageBlobUpload{ ID: strings.ToLower(id), } - _, err = db.GetEngine(ctx).Insert(pbu) + _, err := db.GetEngine(ctx).Insert(pbu) return pbu, err } diff --git a/models/packages/package_file.go b/models/packages/package_file.go index bf877485d6..64bd08f0b2 100644 --- a/models/packages/package_file.go +++ b/models/packages/package_file.go @@ -68,7 +68,7 @@ func TryInsertFile(ctx context.Context, pf *PackageFile) (*PackageFile, error) { // GetFilesByVersionID gets all files of a version func GetFilesByVersionID(ctx context.Context, versionID int64) ([]*PackageFile, error) { pfs := make([]*PackageFile, 0, 10) - return pfs, db.GetEngine(ctx).Where("version_id = ?", versionID).Find(&pfs) + return pfs, db.GetEngine(ctx).Where("version_id = ?", versionID).OrderBy("lower_name, created_unix, id").Find(&pfs) } // GetFileForVersionByID gets a file of a version by id @@ -115,6 +115,20 @@ func DeleteFileByID(ctx context.Context, fileID int64) error { return err } +// DeleteFilesByPackageID deletes all files of a specific package +// Versions must not be deleted prior to this call +func DeleteFilesByPackageID(ctx context.Context, packageID int64) error { + deleteStmt := builder.Delete(builder.In("version_id", builder.Select("package_version.id").From("package_version").Where(builder.Eq{"package_id": packageID}))).From("package_file") + _, err := db.GetEngine(ctx).Exec(deleteStmt) + return err +} + +// DeleteFilesByVersionID deletes all files of a specific version +func DeleteFilesByVersionID(ctx context.Context, versionID int64) error { + _, err := db.GetEngine(ctx).Where("version_id = ?", versionID).Delete(&PackageFile{}) + return err +} + func UpdateFile(ctx context.Context, pf *PackageFile, cols []string) error { _, err := db.GetEngine(ctx).ID(pf.ID).Cols(cols...).Update(pf) return err diff --git a/models/packages/package_property.go b/models/packages/package_property.go index acc05d8d5a..30794ad73c 100644 --- a/models/packages/package_property.go +++ b/models/packages/package_property.go @@ -5,6 +5,7 @@ package packages import ( "context" + "errors" "code.gitea.io/gitea/models/db" @@ -51,13 +52,13 @@ func InsertProperty(ctx context.Context, refType PropertyType, refID int64, name // GetProperties gets all properties func GetProperties(ctx context.Context, refType PropertyType, refID int64) ([]*PackageProperty, error) { pps := make([]*PackageProperty, 0, 10) - return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).Find(&pps) + return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).OrderBy("id").Find(&pps) } // GetPropertiesByName gets all properties with a specific name func GetPropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) ([]*PackageProperty, error) { pps := make([]*PackageProperty, 0, 10) - return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Find(&pps) + return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).OrderBy("id").Find(&pps) } // UpdateProperty updates a property @@ -86,6 +87,46 @@ func DeleteAllProperties(ctx context.Context, refType PropertyType, refID int64) return err } +// DeletePropertiesByPackageID deletes properties of a typed linked to the package +// Use to avoid for loops in mass deletion of properties +func DeletePropertiesByPackageID(ctx context.Context, refType PropertyType, packageID int64) error { + var deleteStmt *builder.Builder + + switch refType { + case PropertyTypeFile: + deleteStmt = builder.Delete( + // Delete all properties that are attached to a file and are in ids from a subquery + // which returns ids from the package_file table joined on package_version to link it with package id + builder.Eq{"ref_type": PropertyTypeFile}, builder.In("ref_id", + builder.Select("package_file.id").From("package_file"). + LeftJoin("package_version", "package_file.version_id = package_version.id"). + Where(builder.Eq{"package_version.package_id": packageID}))).From("package_property") + case PropertyTypeVersion: + // Delete all properties that are attached to a version and are in ids from subquery to the package_version filtered by package id + deleteStmt = builder.Delete( + builder.Eq{"ref_type": PropertyTypeVersion}, builder.In("ref_id", + builder.Select("package_version.id").From("package_version"). + Where(builder.Eq{"package_version.package_id": packageID}))).From("package_property") + case PropertyTypePackage: + // Delete all properties that are attached to a package and their reference links to the given package ID + deleteStmt = builder.Delete( + builder.Eq{"ref_type": PropertyTypePackage}, builder.Eq{"ref_id": packageID}). + From("package_property") + default: + return errors.New("invalid ref type") + } + + _, err := db.GetEngine(ctx).Exec(deleteStmt) + return err +} + +// DeleteFilePropertiesByVersionID deletes all file properties linked to specific version +func DeleteFilePropertiesByVersionID(ctx context.Context, versionID int64) error { + deleteStmt := builder.Delete(builder.Eq{"ref_type": PropertyTypeFile}, builder.In("ref_id", builder.Select("id").From("package_file").Where(builder.Eq{"version_id": versionID}))).From("package_property") + _, err := db.GetEngine(ctx).Exec(deleteStmt) + return err +} + // DeletePropertyByID deletes a property func DeletePropertyByID(ctx context.Context, propertyID int64) error { _, err := db.GetEngine(ctx).ID(propertyID).Delete(&PackageProperty{}) diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 0a478c0323..3e0e1899ea 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -157,6 +157,12 @@ func DeleteVersionByID(ctx context.Context, versionID int64) error { return err } +// DeleteVersionsByPackageID deletes all versions of a specific package +func DeleteVersionsByPackageID(ctx context.Context, packageID int64) error { + _, err := db.GetEngine(ctx).Where(builder.Eq{"package_id": packageID}).Delete(&PackageVersion{}) + return err +} + // HasVersionFileReferences checks if there are associated files func HasVersionFileReferences(ctx context.Context, versionID int64) (bool, error) { return db.GetEngine(ctx).Get(&PackageFile{ diff --git a/models/perm/access/access.go b/models/perm/access/access.go index 6433c4675c..acc34c434e 100644 --- a/models/perm/access/access.go +++ b/models/perm/access/access.go @@ -91,43 +91,80 @@ func updateUserAccess(accessMap map[int64]*userAccess, user *user_model.User, mo } } -// FIXME: do cross-comparison so reduce deletions and additions to the minimum? +// refreshAccesses updates the repository's access records in the database by comparing the provided accessMap +// with existing records. It minimizes DB operations by performing selective inserts, updates, and deletes +// instead of removing all existing records and re-adding them. func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap map[int64]*userAccess) (err error) { - minMode := perm.AccessModeRead + minModeToKeep := perm.AccessModeRead if err := repo.LoadOwner(ctx); err != nil { return fmt.Errorf("LoadOwner: %w", err) } - // If the repo isn't private and isn't owned by a organization, + // If the repo isn't private and isn't owned by an organization, // increase the minMode to Write. if !repo.IsPrivate && !repo.Owner.IsOrganization() { - minMode = perm.AccessModeWrite + minModeToKeep = perm.AccessModeWrite } - newAccesses := make([]Access, 0, len(accessMap)) + // Query existing accesses for cross-comparison + var existingAccesses []Access + if err := db.GetEngine(ctx).Where(builder.Eq{"repo_id": repo.ID}).Find(&existingAccesses); err != nil { + return fmt.Errorf("find existing accesses: %w", err) + } + existingMap := make(map[int64]perm.AccessMode, len(existingAccesses)) + for _, a := range existingAccesses { + existingMap[a.UserID] = a.Mode + } + + var toDelete []int64 + var toInsert, toUpdate []Access + + // Determine changes for userID, ua := range accessMap { - if ua.Mode < minMode && !ua.User.IsRestricted { - continue + if ua.Mode < minModeToKeep && !ua.User.IsRestricted { + // No explicit access record needed (handled by default permissions, e.g., public repo access) + if _, exists := existingMap[userID]; exists { + toDelete = append(toDelete, userID) + } + } else { + desiredMode := ua.Mode + if existingMode, exists := existingMap[userID]; exists { + if existingMode != desiredMode { + toUpdate = append(toUpdate, Access{UserID: userID, RepoID: repo.ID, Mode: desiredMode}) + } + } else { + toInsert = append(toInsert, Access{UserID: userID, RepoID: repo.ID, Mode: desiredMode}) + } } - - newAccesses = append(newAccesses, Access{ - UserID: userID, - RepoID: repo.ID, - Mode: ua.Mode, - }) + delete(existingMap, userID) } - // Delete old accesses and insert new ones for repository. - if _, err = db.DeleteByBean(ctx, &Access{RepoID: repo.ID}); err != nil { - return fmt.Errorf("delete old accesses: %w", err) - } - if len(newAccesses) == 0 { - return nil + // Remaining in existingMap should be deleted + for userID := range existingMap { + toDelete = append(toDelete, userID) } - if err = db.Insert(ctx, newAccesses); err != nil { - return fmt.Errorf("insert new accesses: %w", err) + // Execute deletions + if len(toDelete) > 0 { + if _, err = db.GetEngine(ctx).In("user_id", toDelete).And("repo_id = ?", repo.ID).Delete(&Access{}); err != nil { + return fmt.Errorf("delete accesses: %w", err) + } } + + // Execute updates + for _, u := range toUpdate { + if _, err = db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", u.UserID, repo.ID).Cols("mode").Update(&Access{Mode: u.Mode}); err != nil { + return fmt.Errorf("update access for user %d: %w", u.UserID, err) + } + } + + // Execute insertions + if len(toInsert) > 0 { + if err = db.Insert(ctx, toInsert); err != nil { + return fmt.Errorf("insert new accesses: %w", err) + } + } + return nil } diff --git a/models/perm/access/access_test.go b/models/perm/access/access_test.go index 15d18b368c..148a02efa3 100644 --- a/models/perm/access/access_test.go +++ b/models/perm/access/access_test.go @@ -15,11 +15,20 @@ import ( "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestAccessLevel(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func TestAccess(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + t.Run("AccessLevel", testAccessLevel) + t.Run("HasAccess", testHasAccess) + t.Run("RecalculateAccesses", testRecalculateAccesses) + t.Run("RecalculateAccesses2", testRecalculateAccesses2) + t.Run("RecalculateAccessesUpdateMode", testRecalculateAccessesUpdateMode) + t.Run("RecalculateAccessesRemoveAccess", testRecalculateAccessesRemoveAccess) +} +func testAccessLevel(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) @@ -75,9 +84,7 @@ func TestAccessLevel(t *testing.T) { assert.Equal(t, perm_model.AccessModeRead, level) } -func TestHasAccess(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testHasAccess(t *testing.T) { user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // A public repository owned by User 2 @@ -101,9 +108,8 @@ func TestHasAccess(t *testing.T) { assert.NoError(t, err) } -func TestRepository_RecalculateAccesses(t *testing.T) { +func testRecalculateAccesses(t *testing.T) { // test with organization repo - assert.NoError(t, unittest.PrepareTestDatabase()) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) assert.NoError(t, repo1.LoadOwner(t.Context())) @@ -118,9 +124,8 @@ func TestRepository_RecalculateAccesses(t *testing.T) { assert.Equal(t, perm_model.AccessModeOwner, access.Mode) } -func TestRepository_RecalculateAccesses2(t *testing.T) { +func testRecalculateAccesses2(t *testing.T) { // test with non-organization repo - assert.NoError(t, unittest.PrepareTestDatabase()) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) assert.NoError(t, repo1.LoadOwner(t.Context())) @@ -132,3 +137,67 @@ func TestRepository_RecalculateAccesses2(t *testing.T) { assert.NoError(t, err) assert.False(t, has) } + +func testRecalculateAccessesUpdateMode(t *testing.T) { + // Test the update path in refreshAccesses optimization + // Scenario: User's access mode changes from Read to Write + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + assert.NoError(t, repo.LoadOwner(t.Context())) + + // Verify initial access mode + _ = db.Insert(t.Context(), &repo_model.Collaboration{UserID: 4, RepoID: 4, Mode: perm_model.AccessModeWrite}) + _ = db.Insert(t.Context(), &access_model.Access{UserID: 4, RepoID: 4, Mode: perm_model.AccessModeWrite}) + initialAccess := &access_model.Access{UserID: 4, RepoID: 4} + has, err := db.GetEngine(t.Context()).Get(initialAccess) + assert.NoError(t, err) + assert.True(t, has) + initialMode := initialAccess.Mode + + // Change collaboration mode to trigger update path + newMode := perm_model.AccessModeAdmin + assert.NotEqual(t, initialMode, newMode, "New mode should differ from initial mode") + + _, err = db.GetEngine(t.Context()). + Where("user_id = ? AND repo_id = ?", 4, 4). + Cols("mode"). + Update(&repo_model.Collaboration{Mode: newMode}) + assert.NoError(t, err) + + // Recalculate accesses - should UPDATE existing access, not delete+insert + assert.NoError(t, access_model.RecalculateAccesses(t.Context(), repo)) + + // Verify access was updated, not deleted and re-inserted + updatedAccess := &access_model.Access{UserID: 4, RepoID: 4} + has, err = db.GetEngine(t.Context()).Get(updatedAccess) + assert.NoError(t, err) + assert.True(t, has, "Access should still exist") + assert.Equal(t, newMode, updatedAccess.Mode, "Access mode should be updated to new collaboration mode") +} + +func testRecalculateAccessesRemoveAccess(t *testing.T) { + // Test the delete path in refreshAccesses optimization + // Scenario: Remove a user's collaboration, access should be deleted + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + assert.NoError(t, repo.LoadOwner(t.Context())) + + // Verify initial access exists + initialAccess := &access_model.Access{UserID: 4, RepoID: 4} + has, err := db.GetEngine(t.Context()).Get(initialAccess) + assert.NoError(t, err) + assert.True(t, has, "Access should exist initially") + + // Remove the collaboration to trigger delete path + _, err = db.GetEngine(t.Context()). + Where("user_id = ? AND repo_id = ?", 4, 4). + Delete(&repo_model.Collaboration{}) + assert.NoError(t, err) + + // Recalculate accesses - should DELETE the access record + assert.NoError(t, access_model.RecalculateAccesses(t.Context(), repo)) + + // Verify access was deleted + removedAccess := &access_model.Access{UserID: 4, RepoID: 4} + has, err = db.GetEngine(t.Context()).Get(removedAccess) + assert.NoError(t, err) + assert.False(t, has, "Access should be deleted after removing collaboration") +} diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 622fa5d99a..21821f1746 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -331,7 +331,7 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito // Check permission like simple user but limit to read-only (PR #36095) // Enhanced to also grant read-only access if isSameRepo is true and target repository is public - botPerm, err := GetUserRepoPermission(ctx, repo, user_model.NewActionsUser()) + botPerm, err := GetIndividualUserRepoPermission(ctx, repo, user_model.NewActionsUser()) if err != nil { return perm, err } @@ -379,8 +379,19 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito return perm, nil } -// GetUserRepoPermission returns the user permissions to the repository -func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { +// GetDoerRepoPermission returns the repository permission for the current actor, +// dispatching to GetActionsUserRepoPermission when the actor is an Actions token user. +func GetDoerRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (Permission, error) { + if taskID, ok := user_model.GetActionsUserTaskID(user); ok { + return GetActionsUserRepoPermission(ctx, repo, user, taskID) + } + return GetIndividualUserRepoPermission(ctx, repo, user) +} + +// GetIndividualUserRepoPermission returns the permissions for an explicit user identity. +// In most request paths, callers should use GetDoerRepoPermission instead. +// Unlike GetDoerRepoPermission, this helper does not resolve Actions task users. +func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { defer func() { if err == nil { finalProcessRepoUnitPermission(user, &perm) @@ -539,8 +550,9 @@ func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re // AccessLevelUnit returns the Access a user has to a repository's. Will return NoneAccess if the // user does not have access. +// This helper only supports explicit user identities and does not resolve Actions task users. func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint:revive // export stutter - perm, err := GetUserRepoPermission(ctx, repo, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo, user) if err != nil { return perm_model.AccessModeNone, err } @@ -559,7 +571,7 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model. if user.IsOrganization() { return false, fmt.Errorf("organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID) } - perm, err := GetUserRepoPermission(ctx, repo, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo, user) if err != nil { return false, err } @@ -568,6 +580,7 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model. } // HasAnyUnitAccess see the comment of "perm.HasAnyUnitAccess" +// This helper only supports explicit user identities and does not resolve Actions task users. func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Repository) (bool, error) { var user *user_model.User var err error @@ -577,7 +590,7 @@ func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Reposi return false, err } } - perm, err := GetUserRepoPermission(ctx, repo, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo, user) if err != nil { return false, err } @@ -625,13 +638,14 @@ func GetUserIDsWithUnitAccess(ctx context.Context, repo *repo_model.Repository, } // CheckRepoUnitUser check whether user could visit the unit of this repository +// This helper only supports explicit user identities and does not resolve Actions task users. func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *user_model.User, unitType unit.Type) bool { if user != nil && user.IsAdmin { return true } - perm, err := GetUserRepoPermission(ctx, repo, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo, user) if err != nil { - log.Error("GetUserRepoPermission: %w", err) + log.Error("GetIndividualUserRepoPermission: %w", err) return false } diff --git a/models/perm/access/repo_permission_test.go b/models/perm/access/repo_permission_test.go index a36be213ec..29a8dac71d 100644 --- a/models/perm/access/repo_permission_test.go +++ b/models/perm/access/repo_permission_test.go @@ -6,6 +6,7 @@ package access import ( "testing" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" perm_model "code.gitea.io/gitea/models/perm" @@ -157,8 +158,13 @@ func TestUnitAccessMode(t *testing.T) { assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map") } -func TestGetUserRepoPermission(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func TestGetRepoPermission(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + t.Run("GetIndividualUserRepoPermission", testGetIndividualUserRepoPermission) + t.Run("GetDoerRepoPermission", testGetDoerRepoPermission) +} + +func testGetIndividualUserRepoPermission(t *testing.T) { ctx := t.Context() repo32 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) // org public repo require.NoError(t, repo32.LoadOwner(ctx)) @@ -172,7 +178,7 @@ func TestGetUserRepoPermission(t *testing.T) { require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID})) t.Run("DoerInTeamWithNoRepo", func(t *testing.T) { - perm, err := GetUserRepoPermission(ctx, repo32, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo32, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) assert.Nil(t, perm.unitsMode) // doer in the team, but has no access to the repo @@ -181,7 +187,7 @@ func TestGetUserRepoPermission(t *testing.T) { require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo32.ID})) require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone})) t.Run("DoerWithTeamUnitAccessNone", func(t *testing.T) { - perm, err := GetUserRepoPermission(ctx, repo32, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo32, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeCode]) @@ -191,7 +197,7 @@ func TestGetUserRepoPermission(t *testing.T) { require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{})) require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeWrite})) t.Run("DoerWithTeamUnitAccessWrite", func(t *testing.T) { - perm, err := GetUserRepoPermission(ctx, repo32, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo32, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode]) @@ -204,7 +210,7 @@ func TestGetUserRepoPermission(t *testing.T) { require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}, &Access{})) // The user has access set of that repo, remove it, it is useless for our test require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo3.ID})) t.Run("DoerWithNoopTeamOnPrivateRepo", func(t *testing.T) { - perm, err := GetUserRepoPermission(ctx, repo3, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo3, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode) assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode]) @@ -214,7 +220,7 @@ func TestGetUserRepoPermission(t *testing.T) { require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone})) require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeIssues, AccessMode: perm_model.AccessModeRead})) t.Run("DoerWithReadIssueTeamOnPrivateRepo", func(t *testing.T) { - perm, err := GetUserRepoPermission(ctx, repo3, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo3, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode) assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode]) @@ -233,7 +239,7 @@ func TestGetUserRepoPermission(t *testing.T) { require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite})) require.NoError(t, db.Insert(ctx, Access{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite})) t.Run("DoerWithReadIssueTeamAndWriteCollaboratorOnPrivateRepo", func(t *testing.T) { - perm, err := GetUserRepoPermission(ctx, repo3, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo3, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode) assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode]) @@ -245,3 +251,25 @@ func TestGetUserRepoPermission(t *testing.T) { assert.Equal(t, user.ID, users[0].ID) }) } + +func testGetDoerRepoPermission(t *testing.T) { + ctx := t.Context() + + repo4 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + task47 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47}) + actionsDoer := user_model.NewActionsUserWithTaskID(task47.ID) + regularUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + actionsPerm, err := GetDoerRepoPermission(ctx, repo4, actionsDoer) + require.NoError(t, err) + directPerm, err := GetActionsUserRepoPermission(ctx, repo4, actionsDoer, task47.ID) + require.NoError(t, err) + assert.Equal(t, directPerm, actionsPerm) + + doerPerm, err := GetDoerRepoPermission(ctx, repo1, regularUser) + require.NoError(t, err) + individualPerm, err := GetIndividualUserRepoPermission(ctx, repo1, regularUser) + require.NoError(t, err) + assert.Equal(t, individualPerm, doerPerm) +} diff --git a/models/project/column.go b/models/project/column.go index 79f6dfe911..9c9abb4599 100644 --- a/models/project/column.go +++ b/models/project/column.go @@ -185,7 +185,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error { return err } - if err = column.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil { + if err = moveIssuesToAnotherColumn(ctx, column, defaultColumn); err != nil { return err } @@ -257,9 +257,12 @@ func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) { return columns, nil } -// getDefaultColumn return default column and ensure only one exists -func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) { +// getDefaultColumnWithFallback return default column if one exists +// otherwise return the first column by sorting and set it as default column +func (p *Project) getDefaultColumnWithFallback(ctx context.Context) (*Column, error) { var column Column + + // try to find a column "default=true" has, err := db.GetEngine(ctx). Where("project_id=? AND `default` = ?", p.ID, true). Desc("id").Get(&column) @@ -270,23 +273,9 @@ func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) { if has { return &column, nil } - return nil, ErrProjectColumnNotExist{ColumnID: 0} -} -// MustDefaultColumn returns the default column for a project. -// If one exists, it is returned -// If none exists, the first column will be elevated to the default column of this project -func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) { - c, err := p.getDefaultColumn(ctx) - if err != nil && !IsErrProjectColumnNotExist(err) { - return nil, err - } - if c != nil { - return c, nil - } - - var column Column - has, err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column) + // try to find the first column by sorting + has, err = db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column) if err != nil { return nil, err } @@ -298,8 +287,24 @@ func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) { return &column, nil } + return nil, ErrProjectColumnNotExist{ColumnID: 0} +} + +// MustDefaultColumn returns the default column for a project. +// If one exists, it is returned +// If none exists, the first column will be elevated to the default column of this project +// If there is no column, it creates a default column and returns it +func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) { + c, err := p.getDefaultColumnWithFallback(ctx) + if err != nil && !IsErrProjectColumnNotExist(err) { + return nil, err + } + if c != nil { + return c, nil + } + // create a default column if none is found - column = Column{ + column := Column{ ProjectID: p.ID, Default: true, Title: "Uncategorized", @@ -332,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/project/column_test.go b/models/project/column_test.go index 948e012c62..6437a764ed 100644 --- a/models/project/column_test.go +++ b/models/project/column_test.go @@ -59,7 +59,7 @@ func Test_moveIssuesToAnotherColumn(t *testing.T) { assert.Len(t, issues, 1) assert.EqualValues(t, 3, issues[0].ID) - err = column1.moveIssuesToAnotherColumn(t.Context(), column2) + err = moveIssuesToAnotherColumn(t.Context(), column1, column2) assert.NoError(t, err) issues, err = column1.GetIssues(t.Context()) diff --git a/models/project/issue.go b/models/project/issue.go index 47d1537ec7..c89f524305 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -33,38 +33,45 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error return err } -func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error { - if c.ProjectID != newColumn.ProjectID { - return errors.New("columns have to be in the same project") - } - - if c.ID == newColumn.ID { - return nil - } - +// GetColumnIssueNextSorting returns the sorting value to append an issue at the end of the column. +func GetColumnIssueNextSorting(ctx context.Context, projectID, columnID int64) (int64, error) { res := struct { MaxSorting int64 IssueCount int64 }{} - if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count"). + if _, err := db.GetEngine(ctx).Select("max(sorting) AS max_sorting, count(*) AS issue_count"). Table("project_issue"). - Where("project_id=?", newColumn.ProjectID). - And("project_board_id=?", newColumn.ID). + Where("project_id=?", projectID). + And("project_board_id=?", columnID). Get(&res); err != nil { - return err + return 0, err + } + return util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0), nil +} + +func moveIssuesToAnotherColumn(ctx context.Context, oldColumn, newColumn *Column) error { + if oldColumn.ProjectID != newColumn.ProjectID { + return errors.New("columns have to be in the same project") } - issues, err := c.GetIssues(ctx) - if err != nil { - return err - } - if len(issues) == 0 { + if oldColumn.ID == newColumn.ID { return nil } - nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) + movedIssues, err := oldColumn.GetIssues(ctx) + if err != nil { + return err + } + if len(movedIssues) == 0 { + return nil + } + + nextSorting, err := GetColumnIssueNextSorting(ctx, newColumn.ProjectID, newColumn.ID) + if err != nil { + return err + } return db.WithTx(ctx, func(ctx context.Context) error { - for i, issue := range issues { + for i, issue := range movedIssues { issue.ProjectColumnID = newColumn.ID issue.Sorting = nextSorting + int64(i) if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil { diff --git a/models/project/project.go b/models/project/project.go index 7646c3dd71..7fcef430f2 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -302,6 +302,15 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) { return p, nil } +// GetProjectsMapByIDs returns projects by a list of IDs. +func GetProjectsMapByIDs(ctx context.Context, ids []int64) (map[int64]*Project, error) { + projects := make(map[int64]*Project, len(ids)) + if len(ids) == 0 { + return projects, nil + } + return projects, db.GetEngine(ctx).In("id", ids).Find(&projects) +} + func GetProjectByIDAndOwner(ctx context.Context, id, ownerID int64) (*Project, error) { p := new(Project) has, err := db.GetEngine(ctx).ID(id).And("owner_id = ?", ownerID).Get(p) diff --git a/models/pull/automerge.go b/models/pull/automerge.go index 7f940a9849..d32dc847d2 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/renderhelper/repo_comment.go b/models/renderhelper/repo_comment.go index ae0fbf0abd..d1c587671b 100644 --- a/models/renderhelper/repo_comment.go +++ b/models/renderhelper/repo_comment.go @@ -34,7 +34,7 @@ func (r *RepoComment) ResolveLink(link, preferLinkType string) string { case markup.LinkTypeRoot: return r.ctx.ResolveLinkRoot(link) default: - return r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link) + return r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefSubURL, link) } } @@ -43,7 +43,7 @@ var _ markup.RenderHelper = (*RepoComment)(nil) type RepoCommentOptions struct { DeprecatedRepoName string // it is only a patch for the non-standard "markup" api DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api - CurrentRefPath string // eg: "branch/main" or "commit/11223344" + CurrentRefSubURL string // eg: "branch/main" or "commit/11223344" FootnoteContextID string // the extra context ID for footnotes, used to avoid conflicts with other footnotes in the same page } diff --git a/models/renderhelper/repo_comment_test.go b/models/renderhelper/repo_comment_test.go index 3b13bff73c..1443f8b3c0 100644 --- a/models/renderhelper/repo_comment_test.go +++ b/models/renderhelper/repo_comment_test.go @@ -54,8 +54,8 @@ func TestRepoComment(t *testing.T) { `, rendered) }) - t.Run("WithCurrentRefPath", func(t *testing.T) { - rctx := NewRenderContextRepoComment(t.Context(), repo1, RepoCommentOptions{CurrentRefPath: "/commit/1234"}). + t.Run("WithCurrentRefSubURL", func(t *testing.T) { + rctx := NewRenderContextRepoComment(t.Context(), repo1, RepoCommentOptions{CurrentRefSubURL: "/commit/1234"}). WithMarkupType(markdown.MarkupName) // the ref path is only used to render commit message: a commit message is rendered at the commit page with its commit ID path diff --git a/models/renderhelper/repo_file.go b/models/renderhelper/repo_file.go index f1df8e89e0..d9aa71b727 100644 --- a/models/renderhelper/repo_file.go +++ b/models/renderhelper/repo_file.go @@ -35,11 +35,11 @@ func (r *RepoFile) ResolveLink(link, preferLinkType string) (finalLink string) { case markup.LinkTypeRoot: finalLink = r.ctx.ResolveLinkRoot(link) case markup.LinkTypeRaw: - finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) + finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefSubURL), r.opts.CurrentTreePath, link) case markup.LinkTypeMedia: - finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) + finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefSubURL), r.opts.CurrentTreePath, link) default: - finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) + finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefSubURL), r.opts.CurrentTreePath, link) } return finalLink } @@ -50,8 +50,8 @@ type RepoFileOptions struct { DeprecatedRepoName string // it is only a patch for the non-standard "markup" api DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api - CurrentRefPath string // eg: "branch/main" - CurrentTreePath string // eg: "path/to/file" in the repo + CurrentRefSubURL string // eg: "branch/main" or "branch/my%20branch", it is a sub URL path escaped by callers + CurrentTreePath string // eg: "path/to/file" in the repo, it is the tree path without URL path escaping } func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, opts ...RepoFileOptions) *markup.RenderContext { @@ -70,6 +70,9 @@ func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, "repo": helper.opts.DeprecatedRepoName, }) } + // External render's iframe needs this to generate correct links + // TODO: maybe need to make it access "CurrentRefSubURL" directly (but impossible at the moment due to cycle-import) + rctx.RenderOptions.Metas["RefTypeNameSubURL"] = helper.opts.CurrentRefSubURL rctx = rctx.WithHelper(helper).WithEnableHeadingIDGeneration(true) return rctx } diff --git a/models/renderhelper/repo_file_test.go b/models/renderhelper/repo_file_test.go index 3b48efba3a..72d98efc66 100644 --- a/models/renderhelper/repo_file_test.go +++ b/models/renderhelper/repo_file_test.go @@ -36,7 +36,7 @@ func TestRepoFile(t *testing.T) { }) t.Run("AbsoluteAndRelative", func(t *testing.T) { - rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefPath: "branch/main"}). + rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefSubURL: "branch/main"}). WithMarkupType(markdown.MarkupName) rendered, err := markup.RenderString(rctx, ` [/test](/test) @@ -53,8 +53,8 @@ func TestRepoFile(t *testing.T) { `, rendered) }) - t.Run("WithCurrentRefPath", func(t *testing.T) { - rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefPath: "/commit/1234"}). + t.Run("WithCurrentRefSubURL", func(t *testing.T) { + rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefSubURL: "/commit/1234"}). WithMarkupType(markdown.MarkupName) rendered, err := markup.RenderString(rctx, ` [/test](/test) @@ -66,10 +66,10 @@ func TestRepoFile(t *testing.T) { `, rendered) }) - t.Run("WithCurrentRefPathByTag", func(t *testing.T) { + t.Run("WithCurrentRefSubURLByTag", func(t *testing.T) { rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{ - CurrentRefPath: "/commit/1234", - CurrentTreePath: "my-dir", + CurrentRefSubURL: "/commit/1234", + CurrentTreePath: "my-dir", }). WithMarkupType(markdown.MarkupName) rendered, err := markup.RenderString(rctx, ` @@ -89,8 +89,8 @@ func TestRepoFileOrgMode(t *testing.T) { t.Run("Links", func(t *testing.T) { rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{ - CurrentRefPath: "/commit/1234", - CurrentTreePath: "my-dir", + CurrentRefSubURL: "/commit/1234", + CurrentTreePath: "my-dir", }).WithRelativePath("my-dir/a.org") rendered, err := markup.RenderString(rctx, ` diff --git a/models/renderhelper/repo_wiki.go b/models/renderhelper/repo_wiki.go index 218b1e4a67..61e2b570e5 100644 --- a/models/renderhelper/repo_wiki.go +++ b/models/renderhelper/repo_wiki.go @@ -36,9 +36,9 @@ func (r *RepoWiki) ResolveLink(link, preferLinkType string) (finalLink string) { case markup.LinkTypeRoot: finalLink = r.ctx.ResolveLinkRoot(link) case markup.LinkTypeMedia, markup.LinkTypeRaw: - finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki/raw", r.opts.currentRefPath), r.opts.currentTreePath, link) + finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki/raw", r.opts.currentRefSubURL), r.opts.currentTreePath, link) default: - finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link) + finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefSubURL), r.opts.currentTreePath, link) } return finalLink } @@ -50,8 +50,8 @@ type RepoWikiOptions struct { DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api // these options are not used at the moment because Wiki doesn't support sub-path, nor branch - currentRefPath string // eg: "branch/main" - currentTreePath string // eg: "path/to/file" in the repo + currentRefSubURL string // eg: "branch/main" + currentTreePath string // eg: "path/to/file" in the repo } func NewRenderContextRepoWiki(ctx context.Context, repo *repo_model.Repository, opts ...RepoWikiOptions) *markup.RenderContext { diff --git a/models/repo.go b/models/repo.go index 522debb9fe..34e7b10803 100644 --- a/models/repo.go +++ b/models/repo.go @@ -7,8 +7,7 @@ package models import ( "context" "strconv" - - _ "image/jpeg" // Needed for jpeg support + "strings" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" @@ -17,6 +16,8 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + _ "image/jpeg" // Needed for jpeg support + "xorm.io/builder" ) @@ -86,11 +87,20 @@ func labelStatsCorrectNumClosedIssuesRepo(ctx context.Context, id int64) error { return err } -var milestoneStatsQueryNumIssues = "SELECT `milestone`.id FROM `milestone` WHERE `milestone`.num_closed_issues!=(SELECT COUNT(*) FROM `issue` WHERE `issue`.milestone_id=`milestone`.id AND `issue`.is_closed=?) OR `milestone`.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE `issue`.milestone_id=`milestone`.id)" +func milestoneStatsQueryNumIssuesSQL() string { + sql := ` +SELECT "milestone".id FROM "milestone" +WHERE ( + "milestone".num_closed_issues != (SELECT COUNT(*) FROM "issue" WHERE "issue".milestone_id="milestone".id AND "issue".is_closed=?) + OR "milestone".num_issues != (SELECT COUNT(*) FROM "issue" WHERE "issue".milestone_id="milestone".id) +) +` + return strings.TrimSpace(strings.ReplaceAll(sql, "\"", "`")) +} func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { e := db.GetEngine(ctx) - results, err := e.Query(milestoneStatsQueryNumIssues+" AND `milestone`.repo_id = ?", true, id) + results, err := e.Query(milestoneStatsQueryNumIssuesSQL()+" AND `milestone`.repo_id = ?", true, id) if err != nil { return err } @@ -192,7 +202,7 @@ func CheckRepoStats(ctx context.Context) error { }, // Milestone.Num{,Closed}Issues { - statsQuery(milestoneStatsQueryNumIssues, true), + statsQuery(milestoneStatsQueryNumIssuesSQL(), true), issues_model.UpdateMilestoneCounters, "milestone count 'num_closed_issues' and 'num_issues'", }, diff --git a/models/repo/git.go b/models/repo/git.go index 388bf86522..b2e434a076 100644 --- a/models/repo/git.go +++ b/models/repo/git.go @@ -29,6 +29,16 @@ const ( MergeStyleRebaseUpdate MergeStyle = "rebase-update-only" ) +// UpdateStyle is a pull request branch update style +type UpdateStyle string + +const ( + // UpdateStyleMerge merges the base branch into the pull request branch + UpdateStyleMerge UpdateStyle = "merge" + // UpdateStyleRebase rebases the pull request branch onto the base branch + UpdateStyleRebase UpdateStyle = "rebase" +) + // UpdateDefaultBranch updates the default branch func UpdateDefaultBranch(ctx context.Context, repo *Repository) error { _, err := db.GetEngine(ctx).ID(repo.ID).Cols("default_branch").Update(repo) diff --git a/models/repo/mirror.go b/models/repo/mirror.go index 8fd0cd169e..f43a931715 100644 --- a/models/repo/mirror.go +++ b/models/repo/mirror.go @@ -28,6 +28,7 @@ type Mirror struct { UpdatedUnix timeutil.TimeStamp `xorm:"INDEX"` NextUpdateUnix timeutil.TimeStamp `xorm:"INDEX"` + LastSyncUnix timeutil.TimeStamp `xorm:"INDEX"` LFS bool `xorm:"lfs_enabled NOT NULL DEFAULT false"` LFSEndpoint string `xorm:"lfs_endpoint TEXT"` diff --git a/models/repo/org_repo.go b/models/repo/org_repo.go index 96f21ba2ac..d8c2c91fec 100644 --- a/models/repo/org_repo.go +++ b/models/repo/org_repo.go @@ -18,7 +18,14 @@ import ( // GetOrgRepositories get repos belonging to the given organization func GetOrgRepositories(ctx context.Context, orgID int64) (RepositoryList, error) { var orgRepos []*Repository - return orgRepos, db.GetEngine(ctx).Where("owner_id = ?", orgID).Find(&orgRepos) + err := db.GetEngine(ctx).Where("owner_id = ?", orgID).Find(&orgRepos) + return orgRepos, err +} + +// GetOrgRepositoryIDs get repo IDs belonging to the given organization +func GetOrgRepositoryIDs(ctx context.Context, orgID int64) (repoIDs []int64, _ error) { + err := db.GetEngine(ctx).Table("repository").Where("owner_id = ?", orgID).Cols("id").Find(&repoIDs) + return repoIDs, err } type SearchTeamRepoOptions struct { @@ -26,7 +33,7 @@ type SearchTeamRepoOptions struct { TeamID int64 } -// GetRepositories returns paginated repositories in team of organization. +// GetTeamRepositories returns paginated repositories in team of organization. func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (RepositoryList, error) { sess := db.GetEngine(ctx) if opts.TeamID > 0 { diff --git a/models/repo/pull_request_default_test.go b/models/repo/pull_request_default_test.go index b1653f2f1a..9fad9eaad3 100644 --- a/models/repo/pull_request_default_test.go +++ b/models/repo/pull_request_default_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) @@ -30,3 +31,82 @@ func TestDefaultTargetBranchSelection(t *testing.T) { repo.Units = nil assert.Equal(t, "branch2", repo.GetPullRequestTargetBranch(ctx)) } + +func TestPullRequestConfigFromDB(t *testing.T) { + cases := []struct { + // name describes the row shape under test; the comments capture why each row matters. + name string + json string + wantMergeUpdate bool + wantRebaseUpdate bool + wantDefaultStyle UpdateStyle + wantValidatesPass bool + }{ + { + // Empty object exercises the all-defaults path (e.g. fresh repos created via low-level paths). + name: "defaults", json: "{}", + wantMergeUpdate: true, wantRebaseUpdate: true, + wantDefaultStyle: UpdateStyleMerge, wantValidatesPass: true, + }, + { + // Realistic upgrade case: pre-PR JSON lacks the new fields and has AllowRebaseUpdate=false. + // Historical setting must be preserved while new fields take safe defaults. + name: "legacy without new fields", + json: `{"AllowMerge":true,"AllowRebase":true,"AllowRebaseMerge":true,"AllowSquash":true,"AllowRebaseUpdate":false}`, + wantMergeUpdate: true, wantRebaseUpdate: false, + wantDefaultStyle: UpdateStyleMerge, wantValidatesPass: true, + }, + { + // Partially-migrated row with explicit empty string must normalize so ValidateUpdateSettings passes. + name: "empty default style", json: `{"DefaultUpdateStyle":""}`, + wantMergeUpdate: true, wantRebaseUpdate: true, + wantDefaultStyle: UpdateStyleMerge, wantValidatesPass: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cfg := new(PullRequestsConfig) + assert.NoError(t, cfg.FromDB([]byte(tc.json))) + assert.Equal(t, tc.wantMergeUpdate, cfg.AllowMergeUpdate) + assert.Equal(t, tc.wantRebaseUpdate, cfg.AllowRebaseUpdate) + assert.Equal(t, tc.wantDefaultStyle, cfg.DefaultUpdateStyle) + if tc.wantValidatesPass { + assert.NoError(t, cfg.ValidateUpdateSettings()) + } + }) + } +} + +func TestPullRequestConfigValidateUpdateSettingsInvalidArgument(t *testing.T) { + cases := []struct { + name string + cfg PullRequestsConfig + }{ + { + name: "invalid default style", + cfg: PullRequestsConfig{ + AllowMergeUpdate: true, + AllowRebaseUpdate: true, + DefaultUpdateStyle: "invalid", + }, + }, + { + name: "no update style enabled", + cfg: PullRequestsConfig{ + DefaultUpdateStyle: UpdateStyleMerge, + }, + }, + { + name: "default update style disabled", + cfg: PullRequestsConfig{ + AllowRebaseUpdate: true, + DefaultUpdateStyle: UpdateStyleMerge, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert.ErrorIs(t, tc.cfg.ValidateUpdateSettings(), util.ErrInvalidArgument) + }) + } +} diff --git a/models/repo/repo.go b/models/repo/repo.go index 25207cc28b..7814bb4876 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -376,8 +376,9 @@ func (repo *Repository) CommitLink(commitID string) (result string) { } // APIURL returns the repository API URL -func (repo *Repository) APIURL() string { - return setting.AppURL + "api/v1/repos/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) +func (repo *Repository) APIURL(ctxOpt ...context.Context) string { + ctx := util.OptionalArg(ctxOpt, context.TODO()) + return httplib.MakeAbsoluteURL(ctx, setting.AppSubURL+"/api/v1/repos/"+url.PathEscape(repo.OwnerName)+"/"+url.PathEscape(repo.Name)) } // GetCommitsCountCacheKey returns cache key used for commits count caching. diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index c32adbbcd4..e7204923a2 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -125,7 +125,9 @@ type PullRequestsConfig struct { AllowFastForwardOnly bool AllowManualMerge bool AutodetectManualMerge bool + AllowMergeUpdate bool AllowRebaseUpdate bool + DefaultUpdateStyle UpdateStyle DefaultDeleteBranchAfterMerge bool DefaultMergeStyle MergeStyle DefaultAllowMaintainerEdit bool @@ -139,7 +141,9 @@ func DefaultPullRequestsConfig() *PullRequestsConfig { AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true, + AllowMergeUpdate: true, AllowRebaseUpdate: true, + DefaultUpdateStyle: UpdateStyleMerge, DefaultAllowMaintainerEdit: true, } cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge @@ -152,7 +156,9 @@ func DefaultPullRequestsConfig() *PullRequestsConfig { func (cfg *PullRequestsConfig) FromDB(bs []byte) error { // set default values for existing PullRequestConfig in DB *cfg = *DefaultPullRequestsConfig() - return json.UnmarshalHandleDoubleEncode(bs, &cfg) + _ = json.UnmarshalHandleDoubleEncode(bs, &cfg) // don't let corrupted database value cause unnecessary 500 error + cfg.DefaultUpdateStyle = util.IfZero(cfg.DefaultUpdateStyle, UpdateStyleMerge) + return nil } // ToDB exports a PullRequestsConfig to a serialized format. @@ -170,6 +176,32 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool { mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge } +// IsUpdateStyleAllowed returns if a pull request branch update style is allowed +func (cfg *PullRequestsConfig) IsUpdateStyleAllowed(updateStyle UpdateStyle) bool { + switch updateStyle { + case UpdateStyleMerge: + return cfg.AllowMergeUpdate + case UpdateStyleRebase: + return cfg.AllowRebaseUpdate + default: + return false + } +} + +// ValidateUpdateSettings checks that the AllowMerge/RebaseUpdate flags and DefaultUpdateStyle are mutually consistent. +func (cfg *PullRequestsConfig) ValidateUpdateSettings() error { + if cfg.DefaultUpdateStyle != UpdateStyleMerge && cfg.DefaultUpdateStyle != UpdateStyleRebase { + return util.NewInvalidArgumentErrorf("default update style must be merge or rebase") + } + if !cfg.AllowMergeUpdate && !cfg.AllowRebaseUpdate { + return util.NewInvalidArgumentErrorf("at least one pull request branch update style must be enabled") + } + if !cfg.IsUpdateStyleAllowed(cfg.DefaultUpdateStyle) { + return util.NewInvalidArgumentErrorf("default update style must be enabled") + } + return nil +} + func DefaultPullRequestsUnit(repoID int64) RepoUnit { return RepoUnit{RepoID: repoID, Type: unit.TypePullRequests, Config: DefaultPullRequestsConfig()} } diff --git a/models/repo/star.go b/models/repo/star.go index bc865f8373..e1672623c8 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 6d5209d821..14017da2d0 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 e15a64b01e..28ae83a095 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 cd8a0f1a1f..cd45db61d0 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 f39188f8fb..4b919dffc9 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/fixtures.go b/models/unittest/fixtures.go index a9a01a3227..872bdffc6d 100644 --- a/models/unittest/fixtures.go +++ b/models/unittest/fixtures.go @@ -4,7 +4,10 @@ package unittest import ( + "context" "fmt" + "strings" + "unicode" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/auth/password/hash" @@ -12,11 +15,13 @@ import ( "code.gitea.io/gitea/modules/util" "xorm.io/xorm" + "xorm.io/xorm/contexts" "xorm.io/xorm/schemas" ) type FixturesLoader interface { Load() error + MarkTableChanged(tableName string) } var fixturesLoader FixturesLoader @@ -57,15 +62,101 @@ func loadFixtureResetSeqPgsql(e *xorm.Engine) error { return nil } +type fixturesHookStruct struct{} + +func cutSpaceForSQL(s string) (string, string, bool) { + s = strings.TrimSpace(s) + pos := strings.IndexFunc(s, unicode.IsSpace) + if pos == -1 { + return s, "", false + } + return s[:pos], strings.TrimSpace(s[pos+1:]), true +} + +func trimTableNameQuotes(s string) string { + pos := strings.IndexByte(s, '.') + if pos != -1 { + s = s[pos+1:] + } + return strings.Trim(s, "\"`[]") +} + +func (f fixturesHookStruct) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { + if c.Ctx.Value(db.ContextKeyTestFixtures) != nil { + return c.Ctx, nil + } + ctx, sql := c.Ctx, c.SQL + cmdPart, cmdRemaining, ok := cutSpaceForSQL(sql) + if !ok { + return ctx, nil + } + + // ignore the SQLs which don't change data + if util.AsciiEqualFold(cmdPart, "SELECT") || + util.AsciiEqualFold(cmdPart, "SHOW") || + util.AsciiEqualFold(cmdPart, "PRAGMA") || + util.AsciiEqualFold(cmdPart, "ALTER") || + util.AsciiEqualFold(cmdPart, "CREATE") || + util.AsciiEqualFold(cmdPart, "DROP") || + util.AsciiEqualFold(cmdPart, "IF") || + util.AsciiEqualFold(cmdPart, "SET") || + util.AsciiEqualFold(cmdPart, "sp_rename") || + util.AsciiEqualFold(cmdPart, "BEGIN") || + util.AsciiEqualFold(cmdPart, "ROLLBACK") || + util.AsciiEqualFold(cmdPart, "COMMIT") { + return ctx, nil + } + + switch { + case util.AsciiEqualFold(cmdPart, "INSERT"): + cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining) + if util.AsciiEqualFold(cmdPart, "INTO") { + cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining) + } + fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart)) + case util.AsciiEqualFold(cmdPart, "MERGE"): + cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining) + if util.AsciiEqualFold(cmdPart, "INTO") { + cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining) + } + fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart)) + case util.AsciiEqualFold(cmdPart, "UPDATE"): + cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining) + fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart)) + case util.AsciiEqualFold(cmdPart, "DELETE"): + cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining) + if util.AsciiEqualFold(cmdPart, "FROM") { + cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining) + } + fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart)) + case util.AsciiEqualFold(cmdPart, "TRUNCATE"): + cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining) + if util.AsciiEqualFold(cmdPart, "TABLE") { + cmdPart, cmdRemaining, _ = cutSpaceForSQL(cmdRemaining) + } + fixturesLoader.MarkTableChanged(trimTableNameQuotes(cmdPart)) + default: + // should either parse the table name if it changes data, or ignore it + panic("unrecognized sql: " + sql) + } + _ = cmdRemaining + return ctx, nil +} + +func (f fixturesHookStruct) AfterProcess(c *contexts.ContextHook) error { + return nil +} + // InitFixtures initialize test fixtures for a test database -func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { - xormEngine := util.IfZero(util.OptionalArg(engine), GetXORMEngine()) +func InitFixtures(opts FixturesOptions) (err error) { + xormEngine := GetXORMEngine() fixturesLoader, err = NewFixturesLoader(xormEngine, opts) // fixturesLoader = NewFixturesLoaderVendor(xormEngine, opts) // register the dummy hash algorithm function used in the test fixtures _ = hash.Register("dummy", hash.NewDummyHasher) setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") + xormEngine.AddHook(&fixturesHookStruct{}) return err } diff --git a/models/unittest/fixtures_loader.go b/models/unittest/fixtures_loader.go index 5b79cb5643..e2f2f5846f 100644 --- a/models/unittest/fixtures_loader.go +++ b/models/unittest/fixtures_loader.go @@ -4,6 +4,7 @@ package unittest import ( + "context" "database/sql" "encoding/hex" "fmt" @@ -11,6 +12,7 @@ import ( "path/filepath" "slices" "strings" + "sync" "code.gitea.io/gitea/models/db" @@ -32,7 +34,7 @@ type FixtureItem struct { type fixturesLoaderInternal struct { xormEngine *xorm.Engine - xormTableNames map[string]bool + tableSyncMap sync.Map db *sql.DB dbType schemas.DBType fixtures map[string]*FixtureItem @@ -148,25 +150,36 @@ func (f *fixturesLoaderInternal) Load() error { } defer func() { _ = tx.Rollback() }() + ctx := context.WithValue(context.Background(), db.ContextKeyTestFixtures, true) + for _, fixture := range f.fixtures { - if !f.xormTableNames[fixture.tableName] { + synced, existing := f.tableSyncMap.Load(fixture.tableName) + if synced == true || !existing { continue } if err := f.loadFixtures(tx, fixture); err != nil { return fmt.Errorf("failed to load fixtures from %s: %w", fixture.fileFullPath, err) } + f.tableSyncMap.Store(fixture.tableName, true) } if err = tx.Commit(); err != nil { return err } - for xormTableName := range f.xormTableNames { - if f.fixtures[xormTableName] == nil { - _, _ = f.xormEngine.Exec("DELETE FROM `" + xormTableName + "`") + f.tableSyncMap.Range(func(k, v any) bool { + tableName, synced := k.(string), v.(bool) + if !synced && f.fixtures[tableName] == nil { + _, _ = f.xormEngine.Context(ctx).Exec("DELETE FROM `" + tableName + "`") } - } + f.tableSyncMap.Store(tableName, true) + return true + }) return nil } +func (f *fixturesLoaderInternal) MarkTableChanged(tableName string) { + f.tableSyncMap.Store(tableName, false) +} + func FixturesFileFullPaths(dir string, files []string) (map[string]*FixtureItem, error) { if files != nil && len(files) == 0 { return nil, nil //nolint:nilnil // load nothing @@ -215,11 +228,12 @@ func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, er f.paramPlaceholder = func(idx int) string { return "?" } } + // If a model is not imported in a package (no bean is registered), the table won't exist in database. + // So only use tables of registered models (beans). xormBeans, _ := db.NamesToBean() - f.xormTableNames = map[string]bool{} for _, bean := range xormBeans { - f.xormTableNames[x.TableName(bean)] = true + beanTableName := x.TableName(bean) + f.tableSyncMap.Store(trimTableNameQuotes(beanTableName), false) } - return f, nil } diff --git a/models/unittest/fixtures_test.go b/models/unittest/fixtures_test.go index 72944ec0db..d45ef33847 100644 --- a/models/unittest/fixtures_test.go +++ b/models/unittest/fixtures_test.go @@ -70,7 +70,7 @@ func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions { opts := unittest.FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: []string{ "user.yml", }} - require.NoError(t, unittest.CreateTestEngine(opts)) + require.NoError(t, unittest.CreateTestEngine(filepath.Join(t.TempDir(), "sqlite-test.db"), opts)) return opts } @@ -95,7 +95,7 @@ func TestFixturesLoader(t *testing.T) { func BenchmarkFixturesLoader(b *testing.B) { opts := prepareTestFixturesLoaders(b) - require.NoError(b, unittest.CreateTestEngine(opts)) + require.NoError(b, unittest.CreateTestEngine(filepath.Join(b.TempDir(), "sqlite-test.db"), opts)) loaderInternal, err := unittest.NewFixturesLoader(unittest.GetXORMEngine(), opts) require.NoError(b, err) loaderVendor, err := NewFixturesLoaderVendor(unittest.GetXORMEngine(), opts) diff --git a/models/unittest/mock_http.go b/models/unittest/mock_http.go new file mode 100644 index 0000000000..dd05b263ed --- /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/reflection.go b/models/unittest/reflection.go index bc96a05973..47145d6c03 100644 --- a/models/unittest/reflection.go +++ b/models/unittest/reflection.go @@ -9,7 +9,7 @@ import ( ) func fieldByName(v reflect.Value, field string) reflect.Value { - if v.Kind() == reflect.Ptr { + if v.Kind() == reflect.Pointer { v = v.Elem() } f := v.FieldByName(field) diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 63c9a3a999..2a87a02fcd 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -5,6 +5,8 @@ package unittest import ( "context" + "database/sql" + "errors" "fmt" "os" "path/filepath" @@ -13,10 +15,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 +29,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 +44,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) + if err := CreateTestEngine(filepath.Join(tempWorkPath, "sqlite-test.db"), fixturesOpts); err != nil { + return testlogger.MainErrorf("Error creating test database engine: %v", err) } setting.AppURL = "https://try.gitea.io/" @@ -91,59 +69,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,25 +98,121 @@ 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 } +func ResetTestDatabase() (cleanup func(), err error) { + defer func() { + if cleanup == nil { + cleanup = func() {} + } + }() + + connOpts := db.GlobalConnOptions() + driverDefault, connStrDefault, err := db.ConnStrDefaultDatabase(connOpts) + if err != nil { + return nil, err + } + driverDatabase, connStrDatabase, err := db.ConnStr(connOpts) + if err != nil { + return nil, err + } + + if connOpts.Type.IsSQLite3() { + if !strings.HasSuffix(connOpts.SQLitePath, "-test.db") { + return nil, errors.New(`testing database file for sqlite3 must end in "-test.db"`) + } + _ = os.Remove(connOpts.SQLitePath) + err = os.MkdirAll(filepath.Dir(connOpts.SQLitePath), os.ModePerm) + if err != nil { + return nil, err + } + cleanup = func() { + _ = os.Remove(connOpts.SQLitePath) + _ = os.Remove(filepath.Dir(connOpts.SQLitePath)) + } + return cleanup, nil + } + + if !strings.Contains(connOpts.Database, "test") { + return nil, fmt.Errorf(`testing database name for %s must contain "test"`, connOpts.Database) + } + + quotedDbName := connOpts.Database + if connOpts.Type.IsMSSQL() { + quotedDbName = `[` + connOpts.Database + `]` + } + + sqlExec := func(sqlDB *sql.DB, sql string) error { + _, err := sqlDB.Exec(sql) + if err != nil { + return fmt.Errorf("failed to execute SQL %q: %w", sql, err) + } + return nil + } + + createDatabase := func() error { + sqlDB, err := sql.Open(driverDefault, connStrDefault) + if err != nil { + return err + } + defer sqlDB.Close() + if err = sqlExec(sqlDB, "DROP DATABASE IF EXISTS "+quotedDbName); err != nil { + return err + } + return sqlExec(sqlDB, "CREATE DATABASE "+quotedDbName) + } + if err = createDatabase(); err != nil { + return nil, err + } + + cleanup = func() { + sqlDB, err := sql.Open(driverDefault, connStrDefault) + if err != nil { + return + } + defer sqlDB.Close() + _, _ = sqlDB.Exec("DROP DATABASE IF EXISTS " + quotedDbName) + } + + createDatabaseSchema := func() error { + if !connOpts.Type.IsPostgreSQL() { + return nil + } + if connOpts.Schema == "" { + return nil + } + sqlDB, err := sql.Open(driverDatabase, connStrDatabase) + if err != nil { + return err + } + defer sqlDB.Close() + if err = sqlExec(sqlDB, "DROP SCHEMA IF EXISTS "+connOpts.Schema); err != nil { + return err + } + return sqlExec(sqlDB, "CREATE SCHEMA "+connOpts.Schema) + } + + return cleanup, createDatabaseSchema() +} + // FixturesOptions fixtures needs to be loaded options type FixturesOptions struct { Dir string Files []string } -// CreateTestEngine creates a memory database and loads the fixture data from fixturesDir -func CreateTestEngine(opts FixturesOptions) error { - x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate") +// CreateTestEngine creates a test database and loads the fixture data from fixturesDir +func CreateTestEngine(testSQLiteFile string, opts FixturesOptions) error { + driver, connStr, err := db.ConnStr(db.ConnOptions{Type: setting.DatabaseTypeSQLite3, SQLitePath: testSQLiteFile, SQLiteBusyTimeout: 5000}) + if err != nil { + return err + } + x, err := xorm.NewEngine(driver, connStr) if err != nil { - if strings.Contains(err.Error(), "unknown driver") { - return fmt.Errorf("sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n%w", err) - } return err } x.SetMapper(names.GonicMapper{}) diff --git a/models/user/badge.go b/models/user/badge.go index fbba865926..a4a465a9d5 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 f4afd47d0f..03f984a8fd 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 aa483d5f00..670d417f9e 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 cbf19998d1..5a956a2afe 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 0e764efb9f..636a2007ee 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 a16fc86e55..67b45208fc 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 a74662bb12..9d9551e28d 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" @@ -20,8 +21,6 @@ import ( "time" "unicode" - _ "image/jpeg" // Needed for jpeg support - "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/auth/openid" @@ -39,6 +38,8 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" + _ "image/jpeg" // Needed for jpeg support + "golang.org/x/text/runes" "golang.org/x/text/transform" "golang.org/x/text/unicode/norm" @@ -306,6 +307,13 @@ func (u *User) DashboardLink() string { return setting.AppSubURL + "/" } +func (u *User) SettingsLink() string { + if u.IsOrganization() { + return u.OrganisationLink() + "/settings" + } + return setting.AppSubURL + "/user/settings" +} + // HomeLink returns the user or organization home page link. func (u *User) HomeLink() string { return setting.AppSubURL + "/" + url.PathEscape(u.Name) @@ -524,10 +532,7 @@ const SaltByteLength = 16 // GetUserSalt returns a random user salt token. func GetUserSalt() (string, error) { - rBytes, err := util.CryptoRandomBytes(SaltByteLength) - if err != nil { - return "", err - } + rBytes := util.CryptoRandomBytes(SaltByteLength) // Returns a 32-byte long string. return hex.EncodeToString(rBytes), nil } @@ -617,6 +622,7 @@ var ( "sitemap.xml", // search engine sitemap "ssh_info", // agit info "swagger.v1.json", + "openapi3.v1.json", "ghost", // reserved name for deleted users (id: -1) "gitea-actions", // gitea builtin user (id: -2) @@ -1016,17 +1022,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 @@ -1047,13 +1058,13 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) { return users, nil } -// GetUserByName returns user by given name. -func GetUserByName(ctx context.Context, name string) (*User, error) { - if len(name) == 0 { - return nil, ErrUserNotExist{Name: name} +func getUserByNameWithTypes(ctx context.Context, name string, types ...UserType) (*User, error) { + u := &User{} + sess := db.GetEngine(ctx).Where(builder.Eq{"lower_name": strings.ToLower(name)}) + if len(types) > 0 { + sess.In("`type`", types) } - u := &User{LowerName: strings.ToLower(name), Type: UserTypeIndividual} - has, err := db.GetEngine(ctx).Get(u) + has, err := sess.Get(u) if err != nil { return nil, err } else if !has { @@ -1062,6 +1073,15 @@ func GetUserByName(ctx context.Context, name string) (*User, error) { return u, nil } +// GetUserByName returns the user object by given name, any user type. +func GetUserByName(ctx context.Context, name string) (*User, error) { + return getUserByNameWithTypes(ctx, name) +} + +func GetIndividualUserByName(ctx context.Context, name string) (*User, error) { + return getUserByNameWithTypes(ctx, name, UserTypeIndividual) +} + // GetUserEmailsByNames returns a list of e-mails corresponds to names of users // that have their email notifications set to enabled or onmention. func GetUserEmailsByNames(ctx context.Context, names []string) []string { @@ -1104,19 +1124,6 @@ func GetMailableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([] Find(&ous) } -// GetUserNameByID returns username for the id -func GetUserNameByID(ctx context.Context, id int64) (string, error) { - var name string - has, err := db.GetEngine(ctx).Table("user").Where("id = ?", id).Cols("name").Get(&name) - if err != nil { - return "", err - } - if has { - return name, nil - } - return "", nil -} - // GetUserIDsByNames returns a slice of ids corresponds to names. func GetUserIDsByNames(ctx context.Context, names []string, ignoreNonExistent bool) ([]int64, error) { ids := make([]int64, 0, len(names)) @@ -1317,15 +1324,19 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) { if id != 0 { return GetUserByID(ctx, id) } - return GetUserByName(ctx, name) + return GetIndividualUserByName(ctx, name) } return nil, ErrUserNotExist{Name: email} } -// GetUser checks if a user already exists -func GetUser(ctx context.Context, user *User) (bool, error) { - return db.GetEngine(ctx).Get(user) +func GetIndividualUser(ctx context.Context, user *User) (bool, error) { + // FIXME: the design is wrong, empty User fields won't apply, this function should be removed in the future + has, err := db.GetEngine(ctx).Get(user) + if has && user.Type != UserTypeIndividual { + has = false + } + return has, err } // GetUserByOpenID returns the user object by given OpenID if exists. @@ -1459,16 +1470,6 @@ func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool { return false } -// CountWrongUserType count OrgUser who have wrong type -func CountWrongUserType(ctx context.Context) (int64, error) { - return db.GetEngine(ctx).Where(builder.Eq{"type": 0}.And(builder.Neq{"num_teams": 0})).Count(new(User)) -} - -// FixWrongUserType fix OrgUser who have wrong type -func FixWrongUserType(ctx context.Context) (int64, error) { - return db.GetEngine(ctx).Where(builder.Eq{"type": 0}.And(builder.Neq{"num_teams": 0})).Cols("type").NoAutoTime().Update(&User{Type: 1}) -} - func GetOrderByName() string { if setting.UI.DefaultShowFullName { return "full_name, name" @@ -1495,27 +1496,3 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] { } return &setting.Admin.UserDisabledFeatures } - -// GetUserOrOrgIDByName returns the id for a user or an org by name -func GetUserOrOrgIDByName(ctx context.Context, name string) (int64, error) { - var id int64 - has, err := db.GetEngine(ctx).Table("user").Where("name = ?", name).Cols("id").Get(&id) - if err != nil { - return 0, err - } else if !has { - return 0, fmt.Errorf("user or org with name %s: %w", name, util.ErrNotExist) - } - return id, nil -} - -// GetUserOrOrgByName returns the user or org by name -func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) { - var u User - has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u) - if err != nil { - return nil, err - } else if !has { - return nil, ErrUserNotExist{Name: name} - } - return &u, nil -} diff --git a/models/user/user_system_test.go b/models/user/user_system_test.go index 70a900378f..3ae9c6e366 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 f19465d505..5f2d5081a1 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.go b/models/webhook/webhook.go index 7d4b2e2237..5144c0eca6 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -126,6 +126,7 @@ type Webhook struct { OwnerID int64 `xorm:"INDEX"` IsSystemWebhook bool URL string `xorm:"url TEXT"` + Name string `xorm:"VARCHAR(255) NOT NULL DEFAULT ''"` HTTPMethod string `xorm:"http_method"` ContentType HookContentType Secret string `xorm:"TEXT"` diff --git a/models/webhook/webhook_system_test.go b/models/webhook/webhook_system_test.go index d0013c6873..9e954d1e37 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 71f50017c5..073af91de2 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/artifacts.go b/modules/actions/artifacts.go deleted file mode 100644 index e8bf70ec31..0000000000 --- a/modules/actions/artifacts.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package actions - -import ( - "net/http" - - actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/storage" - "code.gitea.io/gitea/services/context" -) - -// Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend -// The v4 backend ensures ContentEncoding is set to "application/zip", which is not the case for the old backend -func IsArtifactV4(art *actions_model.ActionArtifact) bool { - return art.ArtifactName+".zip" == art.ArtifactPath && art.ContentEncoding == "application/zip" -} - -func DownloadArtifactV4ServeDirectOnly(ctx *context.Base, art *actions_model.ActionArtifact) (bool, error) { - if setting.Actions.ArtifactStorage.ServeDirect() { - u, err := storage.ActionsArtifacts.ServeDirectURL(art.StoragePath, art.ArtifactPath, ctx.Req.Method, nil) - if u != nil && err == nil { - ctx.Redirect(u.String(), http.StatusFound) - return true, nil - } - } - return false, nil -} - -func DownloadArtifactV4Fallback(ctx *context.Base, art *actions_model.ActionArtifact) error { - f, err := storage.ActionsArtifacts.Open(art.StoragePath) - if err != nil { - return err - } - defer f.Close() - http.ServeContent(ctx.Resp, ctx.Req, art.ArtifactName+".zip", art.CreatedUnix.AsLocalTime(), f) - return nil -} - -func DownloadArtifactV4(ctx *context.Base, art *actions_model.ActionArtifact) error { - ok, err := DownloadArtifactV4ServeDirectOnly(ctx, art) - if ok || err != nil { - return err - } - return DownloadArtifactV4Fallback(ctx, art) -} diff --git a/modules/actions/commit_status_info.go b/modules/actions/commit_status_info.go new file mode 100644 index 0000000000..af06d35a2a --- /dev/null +++ b/modules/actions/commit_status_info.go @@ -0,0 +1,70 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "maps" + "slices" + + 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/modules/log" +) + +// CommitActionsStatusMap maps CommitStatus.ID to the live ActionRunJob status +// for Gitea Actions rows. +type CommitActionsStatusMap map[int64]actions_model.Status + +// IconStatus returns the action status name to route the icon through +// repo/icons/action_status, or "" when the row isn't from Gitea Actions. +func (m CommitActionsStatusMap) IconStatus(s *git_model.CommitStatus) string { + if status, ok := m[s.ID]; ok { + return status.String() + } + return "" +} + +// GetCommitActionsStatusMap resolves the live ActionRunJob.Status for every +// CommitStatus row backed by Gitea Actions. Rows from other sources (external +// CIs, API) are left untouched and rendered from their stored State. +func GetCommitActionsStatusMap(ctx context.Context, statuses []*git_model.CommitStatus) CommitActionsStatusMap { + if len(statuses) == 0 { + return nil + } + statusByJobID := make(map[int64]*git_model.CommitStatus) + repoByID := make(map[int64]*repo_model.Repository) + for _, status := range statuses { + if status == nil || status.TargetURL == "" { + continue + } + if status.Repo == nil { + status.Repo = repoByID[status.RepoID] + } + // ParseGiteaActionsTargetURL lazy-loads status.Repo on miss; cache the + // outcome so later entries with the same RepoID skip that load. + _, jobID, ok := status.ParseGiteaActionsTargetURL(ctx) + repoByID[status.RepoID] = status.Repo + if ok { + statusByJobID[jobID] = status + } + } + if len(statusByJobID) == 0 { + return nil + } + jobs := make(map[int64]*actions_model.ActionRunJob, len(statusByJobID)) + if err := db.GetEngine(ctx).In("id", slices.Collect(maps.Keys(statusByJobID))).Cols("id", "status").Find(&jobs); err != nil { + log.Error("db.Find: failed to find action run jobs: %v", err) + return nil + } + info := make(CommitActionsStatusMap, len(jobs)) + for jobID, status := range statusByJobID { + if job, ok := jobs[jobID]; ok { + info[status.ID] = job.Status + } + } + return info +} diff --git a/modules/actions/github.go b/modules/actions/github.go index 68116ec83a..b7f475aa91 100644 --- a/modules/actions/github.go +++ b/modules/actions/github.go @@ -59,6 +59,10 @@ func IsDefaultBranchWorkflow(triggedEvent webhook_module.HookEventType) bool { // Github "issues" event // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues return true + case webhook_module.HookEventWorkflowRun: + // GitHub "workflow_run" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run + return true } return false diff --git a/modules/actions/jobparser/evaluator.go b/modules/actions/jobparser/evaluator.go index 363cb2c18b..3e314afb62 100644 --- a/modules/actions/jobparser/evaluator.go +++ b/modules/actions/jobparser/evaluator.go @@ -9,7 +9,7 @@ import ( "regexp" "strings" - "github.com/nektos/act/pkg/exprparser" + "gitea.com/gitea/runner/act/exprparser" "go.yaml.in/yaml/v4" ) @@ -135,7 +135,7 @@ func rewriteSubExpression(in string, forceFormat bool) (string, error) { exprStart := -1 strStart := -1 var results []string - formatOut := "" + var formatOut strings.Builder for pos < len(in) { if strStart > -1 { matches := strPattern.FindStringIndex(in[pos:]) @@ -158,7 +158,7 @@ func rewriteSubExpression(in string, forceFormat bool) (string, error) { } if exprEnd > -1 { - formatOut += fmt.Sprintf("{%d}", len(results)) + fmt.Fprintf(&formatOut, "{%d}", len(results)) results = append(results, strings.TrimSpace(in[exprStart:pos+exprEnd])) pos += exprEnd + 2 exprStart = -1 @@ -170,20 +170,20 @@ func rewriteSubExpression(in string, forceFormat bool) (string, error) { } else { exprStart = strings.Index(in[pos:], "${{") if exprStart != -1 { - formatOut += escapeFormatString(in[pos : pos+exprStart]) + formatOut.WriteString(escapeFormatString(in[pos : pos+exprStart])) exprStart = pos + exprStart + 3 pos = exprStart } else { - formatOut += escapeFormatString(in[pos:]) + formatOut.WriteString(escapeFormatString(in[pos:])) pos = len(in) } } } - if len(results) == 1 && formatOut == "{0}" && !forceFormat { + if len(results) == 1 && formatOut.String() == "{0}" && !forceFormat { return in, nil } - out := fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut, "'", "''"), strings.Join(results, ", ")) + out := fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut.String(), "'", "''"), strings.Join(results, ", ")) return out, nil } diff --git a/modules/actions/jobparser/interpeter.go b/modules/actions/jobparser/interpeter.go index 512b6f02ab..37defeff7a 100644 --- a/modules/actions/jobparser/interpeter.go +++ b/modules/actions/jobparser/interpeter.go @@ -4,8 +4,8 @@ package jobparser import ( - "github.com/nektos/act/pkg/exprparser" - "github.com/nektos/act/pkg/model" + "gitea.com/gitea/runner/act/exprparser" + "gitea.com/gitea/runner/act/model" "go.yaml.in/yaml/v4" ) diff --git a/modules/actions/jobparser/jobparser.go b/modules/actions/jobparser/jobparser.go index 1d4c4c1756..e7a2b48498 100644 --- a/modules/actions/jobparser/jobparser.go +++ b/modules/actions/jobparser/jobparser.go @@ -9,8 +9,8 @@ import ( "sort" "strings" - "github.com/nektos/act/pkg/exprparser" - "github.com/nektos/act/pkg/model" + "gitea.com/gitea/runner/act/exprparser" + "gitea.com/gitea/runner/act/model" "go.yaml.in/yaml/v4" ) @@ -31,6 +31,9 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) { } results := map[string]*JobResult{} for id, job := range origin.Jobs { + if job == nil { + return nil, fmt.Errorf("needed job not found: %q", id) + } results[id] = &JobResult{ Needs: job.Needs(), Result: pc.jobResults[id], @@ -83,12 +86,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/actions/jobparser/jobparser_test.go b/modules/actions/jobparser/jobparser_test.go index 51ba70fc2a..e74f0644f8 100644 --- a/modules/actions/jobparser/jobparser_test.go +++ b/modules/actions/jobparser/jobparser_test.go @@ -59,6 +59,13 @@ func TestParse(t *testing.T) { wantErr: false, }, } + invalidFileTests := []struct { + name string + }{ + {name: "null_job_implicit"}, + {name: "null_job_explicit"}, + } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { content := ReadTestdata(t, tt.name+".in.yaml") @@ -84,4 +91,14 @@ func TestParse(t *testing.T) { assert.Equal(t, string(want), builder.String()) }) } + + for _, tt := range invalidFileTests { + t.Run(tt.name, func(t *testing.T) { + content := ReadTestdata(t, tt.name+".in.yaml") + require.NotPanics(t, func() { + _, err := Parse(content) + require.Error(t, err) + }) + }) + } } diff --git a/modules/actions/jobparser/model.go b/modules/actions/jobparser/model.go index 7132c278e9..2c4bd1f93a 100644 --- a/modules/actions/jobparser/model.go +++ b/modules/actions/jobparser/model.go @@ -8,7 +8,7 @@ import ( "errors" "fmt" - "github.com/nektos/act/pkg/model" + "gitea.com/gitea/runner/act/model" "go.yaml.in/yaml/v4" ) diff --git a/modules/actions/jobparser/model_test.go b/modules/actions/jobparser/model_test.go index d0e8204161..bd29cb9425 100644 --- a/modules/actions/jobparser/model_test.go +++ b/modules/actions/jobparser/model_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/nektos/act/pkg/model" + "gitea.com/gitea/runner/act/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v4" diff --git a/modules/actions/jobparser/testdata/null_job_explicit.in.yaml b/modules/actions/jobparser/testdata/null_job_explicit.in.yaml new file mode 100644 index 0000000000..6731507a32 --- /dev/null +++ b/modules/actions/jobparser/testdata/null_job_explicit.in.yaml @@ -0,0 +1,9 @@ +# null_job_explicit.in.yaml +on: push +jobs: + empty: null + notempty: + needs: empty + runs-on: ubuntu-latest + steps: + - run: echo ok diff --git a/modules/actions/jobparser/testdata/null_job_implicit.in.yaml b/modules/actions/jobparser/testdata/null_job_implicit.in.yaml new file mode 100644 index 0000000000..26591aadcd --- /dev/null +++ b/modules/actions/jobparser/testdata/null_job_implicit.in.yaml @@ -0,0 +1,9 @@ +# null_job_implicit.in.yaml +on: push +jobs: + empty: + notempty: + needs: empty + runs-on: ubuntu-latest + steps: + - run: echo ok diff --git a/modules/actions/workflowpattern/workflow_pattern.go b/modules/actions/workflowpattern/workflow_pattern.go new file mode 100644 index 0000000000..7e9a5d8597 --- /dev/null +++ b/modules/actions/workflowpattern/workflow_pattern.go @@ -0,0 +1,65 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package workflowpattern + +import ( + "strings" + + "code.gitea.io/gitea/modules/glob" +) + +type WorkflowPattern struct { + negative bool + glob glob.Glob +} + +func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) { + ret := make([]*WorkflowPattern, 0, len(patterns)) + for _, pattern := range patterns { + cp, err := glob.CompileWorkflow(pattern) + if err != nil { + return nil, err + } + ret = append(ret, &WorkflowPattern{glob: cp, negative: strings.HasPrefix(pattern, "!")}) + } + return ret, nil +} + +// Skip returns true if the workflow should be skipped per paths/branches semantics. +func Skip(sequence []*WorkflowPattern, input []string) bool { + allSkipped := true + for _, file := range input { + shouldSkip := true + for _, item := range sequence { + if item.negative { + // "!README.md" doesn't match "README.md", so "README.md" should be skipped + // "!README.md" matches "help.md" but it shouldn't affect "skip or not", because "help.md" might have been skipped by other rules like "docs/*.md" + if !item.glob.Match(file) { + shouldSkip = true + } + } else if item.glob.Match(file) { + // if "*.md" matches "help.md" so it shouldn't be skipped + shouldSkip = false + } + } + allSkipped = allSkipped && shouldSkip + } + return len(sequence) > 0 && allSkipped +} + +// Filter returns true if the workflow should be skipped per paths-ignore/branches-ignore semantics. +func Filter(sequence []*WorkflowPattern, input []string) bool { + for _, file := range input { + anyMatched := false + for _, item := range sequence { + if anyMatched = item.glob.Match(file); anyMatched { + break + } + } + if !anyMatched { + return false + } + } + return len(sequence) != 0 +} diff --git a/modules/actions/workflowpattern/workflow_pattern_test.go b/modules/actions/workflowpattern/workflow_pattern_test.go new file mode 100644 index 0000000000..59a3832634 --- /dev/null +++ b/modules/actions/workflowpattern/workflow_pattern_test.go @@ -0,0 +1,416 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package workflowpattern + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMatchPattern(t *testing.T) { + kases := []struct { + inputs []string + patterns []string + skipResult bool + filterResult bool + }{ + { + patterns: []string{"*"}, + inputs: []string{"path/with/slash"}, + skipResult: true, + filterResult: false, + }, + { + patterns: []string{"path/a", "path/b", "path/c"}, + inputs: []string{"meta", "path/b", "otherfile"}, + skipResult: false, + filterResult: false, + }, + { + patterns: []string{"path/a", "path/b", "path/c"}, + inputs: []string{"path/b"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"path/a", "path/b", "path/c"}, + inputs: []string{"path/c", "path/b"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"path/a", "path/b", "path/c"}, + inputs: []string{"path/c", "path/b", "path/a"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"path/a", "path/b", "path/c"}, + inputs: []string{"path/c", "path/b", "path/d", "path/a"}, + skipResult: false, + filterResult: false, + }, + { + patterns: []string{}, + inputs: []string{}, + skipResult: false, + filterResult: false, + }, + { + patterns: []string{"\\!file"}, + inputs: []string{"!file"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"escape\\\\backslash"}, + inputs: []string{"escape\\backslash"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{".yml"}, + inputs: []string{"fyml"}, + skipResult: true, + filterResult: false, + }, + // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-branches-and-tags + { + patterns: []string{"feature/*"}, + inputs: []string{"feature/my-branch"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"feature/*"}, + inputs: []string{"feature/your-branch"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"feature/**"}, + inputs: []string{"feature/beta-a/my-branch"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"feature/**"}, + inputs: []string{"feature/beta-a/my-branch"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"feature/**"}, + inputs: []string{"feature/mona/the/octocat"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"main", "releases/mona-the-octocat"}, + inputs: []string{"main"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"main", "releases/mona-the-octocat"}, + inputs: []string{"releases/mona-the-octocat"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*"}, + inputs: []string{"main"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*"}, + inputs: []string{"releases"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**"}, + inputs: []string{"all/the/branches"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**"}, + inputs: []string{"every/tag"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*feature"}, + inputs: []string{"mona-feature"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*feature"}, + inputs: []string{"feature"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*feature"}, + inputs: []string{"ver-10-feature"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"v2*"}, + inputs: []string{"v2"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"v2*"}, + inputs: []string{"v2.0"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"v2*"}, + inputs: []string{"v2.9"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"v[12].[0-9]+.[0-9]+"}, + inputs: []string{"v1.10.1"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"v[12].[0-9]+.[0-9]+"}, + inputs: []string{"v2.0.0"}, + skipResult: false, + filterResult: true, + }, + // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-file-paths + { + patterns: []string{"*"}, + inputs: []string{"README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*"}, + inputs: []string{"server.rb"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.jsx?"}, + inputs: []string{"page.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.jsx?"}, + inputs: []string{"page.jsx"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**"}, + inputs: []string{"all/the/files.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.js"}, + inputs: []string{"app.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.js"}, + inputs: []string{"index.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**.js"}, + inputs: []string{"index.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**.js"}, + inputs: []string{"js/index.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**.js"}, + inputs: []string{"src/js/app.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/*"}, + inputs: []string{"docs/README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/*"}, + inputs: []string{"docs/file.txt"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/**"}, + inputs: []string{"docs/README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/**"}, + inputs: []string{"docs/mona/octocat.txt"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/**/*.md"}, + inputs: []string{"docs/README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/**/*.md"}, + inputs: []string{"docs/mona/hello-world.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/**/*.md"}, + inputs: []string{"docs/a/markdown/file.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/docs/**"}, + inputs: []string{"docs/hello.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/docs/**"}, + inputs: []string{"dir/docs/my-file.txt"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/docs/**"}, + inputs: []string{"space/docs/plan/space.doc"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/README.md"}, + inputs: []string{"README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/README.md"}, + inputs: []string{"js/README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/*src/**"}, + inputs: []string{"a/src/app.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/*src/**"}, + inputs: []string{"my-src/code/js/app.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/*-post.md"}, + inputs: []string{"my-post.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/*-post.md"}, + inputs: []string{"path/their-post.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/migrate-*.sql"}, + inputs: []string{"migrate-10909.sql"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/migrate-*.sql"}, + inputs: []string{"db/migrate-v1.0.sql"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/migrate-*.sql"}, + inputs: []string{"db/sept/migrate-v1.sql"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.md", "!README.md"}, + inputs: []string{"hello.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.md", "!README.md"}, + inputs: []string{"README.md"}, + skipResult: true, + filterResult: true, + }, + { + patterns: []string{"*.md", "!README.md"}, + inputs: []string{"docs/hello.md"}, + skipResult: true, + filterResult: true, + }, + { + patterns: []string{"*.md", "!README.md", "README*"}, + inputs: []string{"hello.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.md", "!README.md", "README*"}, + inputs: []string{"README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.md", "!README.md", "README*"}, + inputs: []string{"README.doc"}, + skipResult: false, + filterResult: true, + }, + } + + for _, kase := range kases { + msg := fmt.Sprintf("patterns=%s, input=%s", strings.Join(kase.patterns, ","), strings.Join(kase.inputs, ",")) + patterns, err := CompilePatterns(kase.patterns...) + assert.NoError(t, err, "compile error: %s", msg) + assert.Equal(t, kase.skipResult, Skip(patterns, kase.inputs), "unexpected skip result: %s", msg) + assert.Equal(t, kase.filterResult, Filter(patterns, kase.inputs), "unexpected filter result: %s", msg) + } +} diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 4ac06def4d..991de1af10 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -9,6 +9,7 @@ import ( "strings" "code.gitea.io/gitea/modules/actions/jobparser" + "code.gitea.io/gitea/modules/actions/workflowpattern" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/glob" "code.gitea.io/gitea/modules/log" @@ -17,8 +18,7 @@ import ( "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" - "github.com/nektos/act/pkg/model" - "github.com/nektos/act/pkg/workflowpattern" + "gitea.com/gitea/runner/act/model" "go.yaml.in/yaml/v4" ) @@ -103,10 +103,20 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) { if err != nil { return nil, err } + if err := ValidateWorkflowContent(content); err != nil { + return nil, err + } return events, nil } +// ValidateWorkflowContent catches structural errors (e.g. blank lines in run: | blocks) +// that model.ReadWorkflow alone does not detect. +func ValidateWorkflowContent(content []byte) error { + _, err := jobparser.Parse(content) + return err +} + func DetectWorkflows( gitRepo *git.Repository, commit *git.Commit, @@ -287,7 +297,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa if err != nil { break } - if !workflowpattern.Skip(patterns, []string{refName.BranchName()}, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Skip(patterns, []string{refName.BranchName()}) { matchTimes++ } case "branches-ignore": @@ -299,7 +309,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa if err != nil { break } - if !workflowpattern.Filter(patterns, []string{refName.BranchName()}, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Filter(patterns, []string{refName.BranchName()}) { matchTimes++ } case "tags": @@ -311,7 +321,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa if err != nil { break } - if !workflowpattern.Skip(patterns, []string{refName.TagName()}, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Skip(patterns, []string{refName.TagName()}) { matchTimes++ } case "tags-ignore": @@ -323,7 +333,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa if err != nil { break } - if !workflowpattern.Filter(patterns, []string{refName.TagName()}, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Filter(patterns, []string{refName.TagName()}) { matchTimes++ } case "paths": @@ -339,7 +349,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa if err != nil { break } - if !workflowpattern.Skip(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Skip(patterns, filesChanged) { matchTimes++ } } @@ -356,7 +366,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa if err != nil { break } - if !workflowpattern.Filter(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Filter(patterns, filesChanged) { matchTimes++ } } @@ -482,7 +492,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa if err != nil { break } - if !workflowpattern.Skip(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Skip(patterns, []string{refName.ShortName()}) { matchTimes++ } case "branches-ignore": @@ -491,7 +501,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa if err != nil { break } - if !workflowpattern.Filter(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Filter(patterns, []string{refName.ShortName()}) { matchTimes++ } case "paths": @@ -503,7 +513,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa if err != nil { break } - if !workflowpattern.Skip(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Skip(patterns, filesChanged) { matchTimes++ } } @@ -516,7 +526,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa if err != nil { break } - if !workflowpattern.Filter(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Filter(patterns, filesChanged) { matchTimes++ } } @@ -737,7 +747,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event if err != nil { break } - if !workflowpattern.Skip(patterns, []string{workflow.Name}, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Skip(patterns, []string{workflow.Name}) { matchTimes++ } case "branches": @@ -745,7 +755,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event if err != nil { break } - if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}) { matchTimes++ } case "branches-ignore": @@ -753,7 +763,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event if err != nil { break } - if !workflowpattern.Filter(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) { + if !workflowpattern.Filter(patterns, []string{payload.WorkflowRun.HeadBranch}) { matchTimes++ } default: diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index ea027366f7..cda2de13e2 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -9,16 +9,26 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" webhook_module "code.gitea.io/gitea/modules/webhook" "github.com/stretchr/testify/assert" ) +func fullWorkflowContent(part string) []byte { + return []byte(` +name: test +` + part + ` +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo hello +`) +} + func TestIsWorkflow(t *testing.T) { - oldDirs := setting.Actions.WorkflowDirs - defer func() { - setting.Actions.WorkflowDirs = oldDirs - }() + defer test.MockVariableValue(&setting.Actions.WorkflowDirs)() tests := []struct { name string @@ -218,7 +228,7 @@ func TestDetectMatched(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - evts, err := GetEventsFromContent([]byte(tc.yamlOn)) + evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn)) assert.NoError(t, err) assert.Len(t, evts, 1) assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0])) @@ -373,7 +383,7 @@ func TestMatchIssuesEvent(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - evts, err := GetEventsFromContent([]byte(tc.yamlOn)) + evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn)) assert.NoError(t, err) assert.Len(t, evts, 1) diff --git a/modules/assetfs/layered.go b/modules/assetfs/layered.go index 41e4ca7376..380c3ac455 100644 --- a/modules/assetfs/layered.go +++ b/modules/assetfs/layered.go @@ -9,7 +9,9 @@ import ( "io/fs" "os" "path/filepath" + "slices" "sort" + "strings" "time" "code.gitea.io/gitea/modules/container" @@ -61,6 +63,8 @@ type LayeredFS struct { layers []*Layer } +var _ fs.ReadDirFS = (*LayeredFS)(nil) + // Layered returns a new LayeredFS with the given layers. The first layer is the top layer. func Layered(layers ...*Layer) *LayeredFS { return &LayeredFS{layers: layers} @@ -83,6 +87,27 @@ func (l *LayeredFS) ReadFile(elems ...string) ([]byte, error) { return bs, err } +func (l *LayeredFS) ReadDir(name string) (files []fs.DirEntry, _ error) { + filesMap := map[string]fs.DirEntry{} + for _, layer := range l.layers { + entries, err := readDirOptional(layer, name) + if err != nil { + return nil, err + } + for _, entry := range entries { + entryName := entry.Name() + if _, exist := filesMap[entryName]; !exist && shouldInclude(entry) { + filesMap[entryName] = entry + } + } + } + for _, file := range filesMap { + files = append(files, file) + } + slices.SortFunc(files, func(a, b fs.DirEntry) int { return strings.Compare(a.Name(), b.Name()) }) + return files, nil +} + // ReadLayeredFile reads the named file, and returns the layer name. func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) { name := util.PathJoinRel(elems...) diff --git a/modules/auth/pam/pam.go b/modules/auth/pam/pam.go index cca1482b1d..a8b608e6be 100644 --- a/modules/auth/pam/pam.go +++ b/modules/auth/pam/pam.go @@ -8,7 +8,7 @@ package pam import ( "errors" - "github.com/msteinert/pam" + "github.com/msteinert/pam/v2" ) // Supported is true when built with PAM @@ -28,6 +28,7 @@ func Auth(serviceName, userName, passwd string) (string, error) { if err != nil { return "", err } + defer t.End() if err = t.Authenticate(0); err != nil { return "", err diff --git a/modules/auth/pam/pam_test.go b/modules/auth/pam/pam_test.go index d4ab058ec7..1180e75d88 100644 --- a/modules/auth/pam/pam_test.go +++ b/modules/auth/pam/pam_test.go @@ -1,8 +1,8 @@ -//go:build pam - // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT +//go:build pam + package pam import ( diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index 3b622b99af..44c61e1ff6 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -11,15 +11,14 @@ import ( "image/color" "image/png" - _ "image/gif" // for processing gif images - _ "image/jpeg" // for processing jpeg images - "code.gitea.io/gitea/modules/avatar/identicon" "code.gitea.io/gitea/modules/setting" - "golang.org/x/image/draw" - _ "golang.org/x/image/webp" // for processing webp images + _ "image/gif" // for processing gif images + _ "image/jpeg" // for processing jpeg images + + "golang.org/x/image/draw" ) // DefaultAvatarSize is the target CSS pixel size for avatar generation. It is diff --git a/modules/charset/ambiguous.go b/modules/charset/ambiguous.go index 96e0561e15..c87d3cfa5a 100644 --- a/modules/charset/ambiguous.go +++ b/modules/charset/ambiguous.go @@ -1,4 +1,3 @@ -// This file is generated by modules/charset/ambiguous/generate.go DO NOT EDIT // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT @@ -14,11 +13,12 @@ import ( // AmbiguousTablesForLocale provides the table of ambiguous characters for this locale. func AmbiguousTablesForLocale(locale translation.Locale) []*AmbiguousTable { + ambiguousTableMap := globalVars().ambiguousTableMap key := locale.Language() var table *AmbiguousTable var ok bool for len(key) > 0 { - if table, ok = AmbiguousCharacters[key]; ok { + if table, ok = ambiguousTableMap[key]; ok { break } idx := strings.LastIndexAny(key, "-_") @@ -29,18 +29,18 @@ func AmbiguousTablesForLocale(locale translation.Locale) []*AmbiguousTable { } } if table == nil && (locale.Language() == "zh-CN" || locale.Language() == "zh_CN") { - table = AmbiguousCharacters["zh-hans"] + table = ambiguousTableMap["zh-hans"] } if table == nil && strings.HasPrefix(locale.Language(), "zh") { - table = AmbiguousCharacters["zh-hant"] + table = ambiguousTableMap["zh-hant"] } if table == nil { - table = AmbiguousCharacters["_default"] + table = ambiguousTableMap["_default"] } return []*AmbiguousTable{ table, - AmbiguousCharacters["_common"], + ambiguousTableMap["_common"], } } @@ -52,7 +52,7 @@ func isAmbiguous(r rune, confusableTo *rune, tables ...*AmbiguousTable) bool { i := sort.Search(len(table.Confusable), func(i int) bool { return table.Confusable[i] >= r }) - (*confusableTo) = table.With[i] + *confusableTo = table.With[i] return true } return false diff --git a/modules/charset/ambiguous/generate.go b/modules/charset/ambiguous/generate.go deleted file mode 100644 index e3fda5be98..0000000000 --- a/modules/charset/ambiguous/generate.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package main - -import ( - "bytes" - "flag" - "fmt" - "go/format" - "os" - "sort" - "text/template" - "unicode" - - "code.gitea.io/gitea/modules/json" - - "golang.org/x/text/unicode/rangetable" -) - -// ambiguous.json provides a one to one mapping of ambiguous characters to other characters -// See https://github.com/hediet/vscode-unicode-data/blob/main/out/ambiguous.json - -type AmbiguousTable struct { - Confusable []rune - With []rune - Locale string - RangeTable *unicode.RangeTable -} - -type RunePair struct { - Confusable rune - With rune -} - -var verbose bool - -func main() { - flag.Usage = func() { - fmt.Fprintf(os.Stderr, `%s: Generate AmbiguousCharacter - -Usage: %[1]s [-v] [-o output.go] ambiguous.json -`, os.Args[0]) - flag.PrintDefaults() - } - - output := "" - flag.BoolVar(&verbose, "v", false, "verbose output") - flag.StringVar(&output, "o", "ambiguous_gen.go", "file to output to") - flag.Parse() - input := flag.Arg(0) - if input == "" { - input = "ambiguous.json" - } - - bs, err := os.ReadFile(input) - if err != nil { - fatalf("Unable to read: %s Err: %v", input, err) - } - - var unwrapped string - if err := json.Unmarshal(bs, &unwrapped); err != nil { - fatalf("Unable to unwrap content in: %s Err: %v", input, err) - } - - fromJSON := map[string][]uint32{} - if err := json.Unmarshal([]byte(unwrapped), &fromJSON); err != nil { - fatalf("Unable to unmarshal content in: %s Err: %v", input, err) - } - - tables := make([]*AmbiguousTable, 0, len(fromJSON)) - for locale, chars := range fromJSON { - table := &AmbiguousTable{Locale: locale} - table.Confusable = make([]rune, 0, len(chars)/2) - table.With = make([]rune, 0, len(chars)/2) - pairs := make([]RunePair, len(chars)/2) - for i := 0; i < len(chars); i += 2 { - pairs[i/2].Confusable, pairs[i/2].With = rune(chars[i]), rune(chars[i+1]) - } - sort.Slice(pairs, func(i, j int) bool { - return pairs[i].Confusable < pairs[j].Confusable - }) - for _, pair := range pairs { - table.Confusable = append(table.Confusable, pair.Confusable) - table.With = append(table.With, pair.With) - } - table.RangeTable = rangetable.New(table.Confusable...) - tables = append(tables, table) - } - sort.Slice(tables, func(i, j int) bool { - return tables[i].Locale < tables[j].Locale - }) - data := map[string]any{ - "Tables": tables, - } - - if err := runTemplate(generatorTemplate, output, &data); err != nil { - fatalf("Unable to run template: %v", err) - } -} - -func runTemplate(t *template.Template, filename string, data any) error { - buf := bytes.NewBuffer(nil) - if err := t.Execute(buf, data); err != nil { - return fmt.Errorf("unable to execute template: %w", err) - } - bs, err := format.Source(buf.Bytes()) - if err != nil { - verbosef("Bad source:\n%s", buf.String()) - return fmt.Errorf("unable to format source: %w", err) - } - - old, err := os.ReadFile(filename) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to read old file %s because %w", filename, err) - } else if err == nil { - if bytes.Equal(bs, old) { - // files are the same don't rewrite it. - return nil - } - } - - file, err := os.Create(filename) - if err != nil { - return fmt.Errorf("failed to create file %s because %w", filename, err) - } - defer file.Close() - _, err = file.Write(bs) - if err != nil { - return fmt.Errorf("unable to write generated source: %w", err) - } - return nil -} - -var generatorTemplate = template.Must(template.New("ambiguousTemplate").Parse(`// This file is generated by modules/charset/ambiguous/generate.go DO NOT EDIT -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - - -package charset - -import "unicode" - -// This file is generated from https://github.com/hediet/vscode-unicode-data/blob/main/out/ambiguous.json - -// AmbiguousTable matches a confusable rune with its partner for the Locale -type AmbiguousTable struct { - Confusable []rune - With []rune - Locale string - RangeTable *unicode.RangeTable -} - -// AmbiguousCharacters provides a map by locale name to the confusable characters in that locale -var AmbiguousCharacters = map[string]*AmbiguousTable{ - {{range .Tables}}{{printf "%q:" .Locale}} { - Confusable: []rune{ {{range .Confusable}}{{.}},{{end}} }, - With: []rune{ {{range .With}}{{.}},{{end}} }, - Locale: {{printf "%q" .Locale}}, - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {{range .RangeTable.R16 }} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, - {{end}} }, - R32: []unicode.Range32{ - {{range .RangeTable.R32}} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, - {{end}} }, - LatinOffset: {{.RangeTable.LatinOffset}}, - }, - }, - {{end}} -} - -`)) - -func logf(format string, args ...any) { - fmt.Fprintf(os.Stderr, format+"\n", args...) -} - -func verbosef(format string, args ...any) { - if verbose { - logf(format, args...) - } -} - -func fatalf(format string, args ...any) { - logf("fatal: "+format+"\n", args...) - os.Exit(1) -} diff --git a/modules/charset/ambiguous_gen.go b/modules/charset/ambiguous_gen.go index c88ffd5aa5..669a46c91a 100644 --- a/modules/charset/ambiguous_gen.go +++ b/modules/charset/ambiguous_gen.go @@ -1,5 +1,5 @@ -// This file is generated by modules/charset/ambiguous/generate.go DO NOT EDIT -// Copyright 2022 The Gitea Authors. All rights reserved. +// This file is generated by modules/charset/generate/generate.go DO NOT EDIT +// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package charset @@ -16,821 +16,837 @@ type AmbiguousTable struct { RangeTable *unicode.RangeTable } -// AmbiguousCharacters provides a map by locale name to the confusable characters in that locale -var AmbiguousCharacters = map[string]*AmbiguousTable{ - "_common": { - Confusable: []rune{184, 383, 388, 397, 422, 423, 439, 444, 445, 448, 451, 540, 546, 547, 577, 593, 609, 611, 617, 618, 623, 651, 655, 660, 697, 699, 700, 701, 702, 706, 707, 708, 710, 712, 714, 715, 720, 727, 731, 732, 756, 760, 884, 890, 894, 895, 900, 913, 914, 917, 918, 919, 922, 924, 925, 927, 929, 932, 933, 935, 945, 947, 953, 957, 959, 961, 963, 965, 978, 988, 1000, 1010, 1011, 1017, 1018, 1029, 1030, 1032, 1109, 1110, 1112, 1121, 1140, 1141, 1198, 1199, 1211, 1213, 1216, 1231, 1248, 1281, 1292, 1307, 1308, 1309, 1357, 1359, 1365, 1370, 1373, 1377, 1379, 1382, 1392, 1400, 1404, 1405, 1409, 1412, 1413, 1417, 1472, 1475, 1493, 1496, 1497, 1503, 1505, 1523, 1549, 1575, 1607, 1632, 1633, 1637, 1639, 1643, 1645, 1726, 1729, 1748, 1749, 1776, 1777, 1781, 1783, 1793, 1794, 1795, 1796, 1984, 1994, 2036, 2037, 2042, 2307, 2406, 2429, 2534, 2538, 2541, 2662, 2663, 2666, 2691, 2790, 2819, 2848, 2918, 2920, 3046, 3074, 3174, 3202, 3302, 3330, 3360, 3430, 3437, 3458, 3664, 3792, 4125, 4160, 4327, 4351, 4608, 4816, 5024, 5025, 5026, 5029, 5033, 5034, 5035, 5036, 5038, 5043, 5047, 5051, 5053, 5056, 5058, 5059, 5070, 5071, 5074, 5076, 5077, 5081, 5082, 5086, 5087, 5090, 5094, 5095, 5102, 5107, 5108, 5120, 5167, 5171, 5176, 5194, 5196, 5229, 5231, 5234, 5261, 5290, 5311, 5441, 5500, 5501, 5511, 5551, 5556, 5573, 5598, 5610, 5616, 5623, 5741, 5742, 5760, 5810, 5815, 5825, 5836, 5845, 5846, 5868, 5869, 5941, 6147, 6153, 7428, 7439, 7441, 7452, 7456, 7457, 7458, 7462, 7555, 7564, 7837, 7935, 8125, 8126, 8127, 8128, 8175, 8189, 8190, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8208, 8209, 8210, 8218, 8219, 8228, 8232, 8233, 8239, 8242, 8249, 8250, 8257, 8259, 8260, 8270, 8275, 8282, 8287, 8450, 8458, 8459, 8460, 8461, 8462, 8464, 8465, 8466, 8467, 8469, 8473, 8474, 8475, 8476, 8477, 8484, 8488, 8490, 8492, 8493, 8494, 8495, 8496, 8497, 8499, 8500, 8505, 8509, 8517, 8518, 8519, 8520, 8521, 8544, 8548, 8553, 8556, 8557, 8558, 8559, 8560, 8564, 8569, 8572, 8573, 8574, 8722, 8725, 8726, 8727, 8739, 8744, 8746, 8758, 8764, 8868, 8897, 8899, 8959, 9075, 9076, 9082, 9213, 9585, 9587, 10088, 10089, 10094, 10095, 10098, 10099, 10100, 10101, 10133, 10134, 10187, 10189, 10201, 10539, 10540, 10741, 10744, 10745, 10799, 11397, 11406, 11410, 11412, 11416, 11418, 11422, 11423, 11426, 11427, 11428, 11429, 11430, 11432, 11436, 11450, 11462, 11466, 11468, 11472, 11474, 11576, 11577, 11599, 11601, 11604, 11605, 11613, 11840, 12034, 12035, 12295, 12308, 12309, 12339, 12448, 12755, 12756, 20022, 20031, 42192, 42193, 42194, 42195, 42196, 42198, 42199, 42201, 42202, 42204, 42205, 42207, 42208, 42209, 42210, 42211, 42214, 42215, 42218, 42219, 42220, 42222, 42224, 42226, 42227, 42228, 42232, 42233, 42237, 42239, 42510, 42564, 42567, 42719, 42731, 42735, 42801, 42842, 42858, 42862, 42872, 42889, 42892, 42904, 42905, 42911, 42923, 42930, 42931, 42932, 43826, 43829, 43837, 43847, 43848, 43854, 43858, 43866, 43893, 43905, 43907, 43923, 43945, 43946, 43951, 64422, 64423, 64424, 64425, 64426, 64427, 64428, 64429, 64830, 64831, 65072, 65101, 65102, 65103, 65112, 65128, 65165, 65166, 65257, 65258, 65259, 65260, 65282, 65284, 65285, 65286, 65287, 65290, 65291, 65293, 65294, 65295, 65296, 65297, 65298, 65299, 65300, 65301, 65302, 65303, 65304, 65305, 65308, 65309, 65310, 65312, 65313, 65314, 65315, 65316, 65317, 65318, 65319, 65320, 65321, 65322, 65323, 65324, 65325, 65326, 65327, 65328, 65329, 65330, 65331, 65332, 65333, 65334, 65335, 65336, 65337, 65338, 65339, 65340, 65341, 65342, 65343, 65344, 65345, 65346, 65347, 65348, 65349, 65350, 65351, 65352, 65353, 65354, 65355, 65356, 65357, 65358, 65359, 65360, 65361, 65362, 65363, 65364, 65365, 65366, 65367, 65368, 65369, 65370, 65371, 65372, 65373, 65512, 66178, 66182, 66183, 66186, 66192, 66194, 66197, 66198, 66199, 66203, 66208, 66209, 66210, 66213, 66219, 66224, 66225, 66226, 66228, 66255, 66293, 66305, 66306, 66313, 66321, 66325, 66327, 66330, 66335, 66336, 66338, 66564, 66581, 66587, 66592, 66604, 66621, 66632, 66740, 66754, 66766, 66770, 66794, 66806, 66835, 66838, 66840, 66844, 66845, 66853, 66854, 66855, 68176, 70864, 71430, 71434, 71438, 71439, 71840, 71842, 71843, 71844, 71846, 71849, 71852, 71854, 71855, 71858, 71861, 71864, 71867, 71868, 71872, 71873, 71874, 71875, 71876, 71878, 71880, 71882, 71884, 71893, 71894, 71895, 71896, 71900, 71904, 71909, 71910, 71913, 71916, 71919, 71922, 93960, 93962, 93974, 93992, 94005, 94010, 94011, 94015, 94016, 94018, 94019, 94033, 94034, 119060, 119149, 119302, 119309, 119311, 119314, 119315, 119318, 119338, 119350, 119351, 119354, 119355, 119808, 119809, 119810, 119811, 119812, 119813, 119814, 119815, 119816, 119817, 119818, 119819, 119820, 119821, 119822, 119823, 119824, 119825, 119826, 119827, 119828, 119829, 119830, 119831, 119832, 119833, 119834, 119835, 119836, 119837, 119838, 119839, 119840, 119841, 119842, 119843, 119844, 119845, 119847, 119848, 119849, 119850, 119851, 119852, 119853, 119854, 119855, 119856, 119857, 119858, 119859, 119860, 119861, 119862, 119863, 119864, 119865, 119866, 119867, 119868, 119869, 119870, 119871, 119872, 119873, 119874, 119875, 119876, 119877, 119878, 119879, 119880, 119881, 119882, 119883, 119884, 119885, 119886, 119887, 119888, 119889, 119890, 119891, 119892, 119894, 119895, 119896, 119897, 119899, 119900, 119901, 119902, 119903, 119904, 119905, 119906, 119907, 119908, 119909, 119910, 119911, 119912, 119913, 119914, 119915, 119916, 119917, 119918, 119919, 119920, 119921, 119922, 119923, 119924, 119925, 119926, 119927, 119928, 119929, 119930, 119931, 119932, 119933, 119934, 119935, 119936, 119937, 119938, 119939, 119940, 119941, 119942, 119943, 119944, 119945, 119946, 119947, 119948, 119949, 119951, 119952, 119953, 119954, 119955, 119956, 119957, 119958, 119959, 119960, 119961, 119962, 119963, 119964, 119966, 119967, 119970, 119973, 119974, 119977, 119978, 119979, 119980, 119982, 119983, 119984, 119985, 119986, 119987, 119988, 119989, 119990, 119991, 119992, 119993, 119995, 119997, 119998, 119999, 120000, 120001, 120003, 120005, 120006, 120007, 120008, 120009, 120010, 120011, 120012, 120013, 120014, 120015, 120016, 120017, 120018, 120019, 120020, 120021, 120022, 120023, 120024, 120025, 120026, 120027, 120028, 120029, 120030, 120031, 120032, 120033, 120034, 120035, 120036, 120037, 120038, 120039, 120040, 120041, 120042, 120043, 120044, 120045, 120046, 120047, 120048, 120049, 120050, 120051, 120052, 120053, 120055, 120056, 120057, 120058, 120059, 120060, 120061, 120062, 120063, 120064, 120065, 120066, 120067, 120068, 120069, 120071, 120072, 120073, 120074, 120077, 120078, 120079, 120080, 120081, 120082, 120083, 120084, 120086, 120087, 120088, 120089, 120090, 120091, 120092, 120094, 120095, 120096, 120097, 120098, 120099, 120100, 120101, 120102, 120103, 120104, 120105, 120107, 120108, 120109, 120110, 120111, 120112, 120113, 120114, 120115, 120116, 120117, 120118, 120119, 120120, 120121, 120123, 120124, 120125, 120126, 120128, 120129, 120130, 120131, 120132, 120134, 120138, 120139, 120140, 120141, 120142, 120143, 120144, 120146, 120147, 120148, 120149, 120150, 120151, 120152, 120153, 120154, 120155, 120156, 120157, 120159, 120160, 120161, 120162, 120163, 120164, 120165, 120166, 120167, 120168, 120169, 120170, 120171, 120172, 120173, 120174, 120175, 120176, 120177, 120178, 120179, 120180, 120181, 120182, 120183, 120184, 120185, 120186, 120187, 120188, 120189, 120190, 120191, 120192, 120193, 120194, 120195, 120196, 120197, 120198, 120199, 120200, 120201, 120202, 120203, 120204, 120205, 120206, 120207, 120208, 120209, 120211, 120212, 120213, 120214, 120215, 120216, 120217, 120218, 120219, 120220, 120221, 120222, 120223, 120224, 120225, 120226, 120227, 120228, 120229, 120230, 120231, 120232, 120233, 120234, 120235, 120236, 120237, 120238, 120239, 120240, 120241, 120242, 120243, 120244, 120245, 120246, 120247, 120248, 120249, 120250, 120251, 120252, 120253, 120254, 120255, 120256, 120257, 120258, 120259, 120260, 120261, 120263, 120264, 120265, 120266, 120267, 120268, 120269, 120270, 120271, 120272, 120273, 120274, 120275, 120276, 120277, 120278, 120279, 120280, 120281, 120282, 120283, 120284, 120285, 120286, 120287, 120288, 120289, 120290, 120291, 120292, 120293, 120294, 120295, 120296, 120297, 120298, 120299, 120300, 120301, 120302, 120303, 120304, 120305, 120306, 120307, 120308, 120309, 120310, 120311, 120312, 120313, 120315, 120316, 120317, 120318, 120319, 120320, 120321, 120322, 120323, 120324, 120325, 120326, 120327, 120328, 120329, 120330, 120331, 120332, 120333, 120334, 120335, 120336, 120337, 120338, 120339, 120340, 120341, 120342, 120343, 120344, 120345, 120346, 120347, 120348, 120349, 120350, 120351, 120352, 120353, 120354, 120355, 120356, 120357, 120358, 120359, 120360, 120361, 120362, 120363, 120364, 120365, 120367, 120368, 120369, 120370, 120371, 120372, 120373, 120374, 120375, 120376, 120377, 120378, 120379, 120380, 120381, 120382, 120383, 120384, 120385, 120386, 120387, 120388, 120389, 120390, 120391, 120392, 120393, 120394, 120395, 120396, 120397, 120398, 120399, 120400, 120401, 120402, 120403, 120404, 120405, 120406, 120407, 120408, 120409, 120410, 120411, 120412, 120413, 120414, 120415, 120416, 120417, 120419, 120420, 120421, 120422, 120423, 120424, 120425, 120426, 120427, 120428, 120429, 120430, 120431, 120432, 120433, 120434, 120435, 120436, 120437, 120438, 120439, 120440, 120441, 120442, 120443, 120444, 120445, 120446, 120447, 120448, 120449, 120450, 120451, 120452, 120453, 120454, 120455, 120456, 120457, 120458, 120459, 120460, 120461, 120462, 120463, 120464, 120465, 120466, 120467, 120468, 120469, 120471, 120472, 120473, 120474, 120475, 120476, 120477, 120478, 120479, 120480, 120481, 120482, 120483, 120484, 120488, 120489, 120492, 120493, 120494, 120496, 120497, 120499, 120500, 120502, 120504, 120507, 120508, 120510, 120514, 120516, 120522, 120526, 120528, 120530, 120532, 120534, 120544, 120546, 120547, 120550, 120551, 120552, 120554, 120555, 120557, 120558, 120560, 120562, 120565, 120566, 120568, 120572, 120574, 120580, 120584, 120586, 120588, 120590, 120592, 120602, 120604, 120605, 120608, 120609, 120610, 120612, 120613, 120615, 120616, 120618, 120620, 120623, 120624, 120626, 120630, 120632, 120638, 120642, 120644, 120646, 120648, 120650, 120660, 120662, 120663, 120666, 120667, 120668, 120670, 120671, 120673, 120674, 120676, 120678, 120681, 120682, 120684, 120688, 120690, 120696, 120700, 120702, 120704, 120706, 120708, 120718, 120720, 120721, 120724, 120725, 120726, 120728, 120729, 120731, 120732, 120734, 120736, 120739, 120740, 120742, 120746, 120748, 120754, 120758, 120760, 120762, 120764, 120766, 120776, 120778, 120782, 120783, 120784, 120785, 120786, 120787, 120788, 120789, 120790, 120791, 120792, 120793, 120794, 120795, 120796, 120797, 120798, 120799, 120800, 120801, 120802, 120803, 120804, 120805, 120806, 120807, 120808, 120809, 120810, 120811, 120812, 120813, 120814, 120815, 120816, 120817, 120818, 120819, 120820, 120821, 120822, 120823, 120824, 120825, 120826, 120827, 120828, 120829, 120830, 120831, 125127, 125131, 126464, 126500, 126564, 126592, 126596, 128844, 128872, 130032, 130033, 130034, 130035, 130036, 130037, 130038, 130039, 130040, 130041}, - With: []rune{44, 102, 98, 103, 82, 50, 51, 53, 115, 73, 33, 51, 56, 56, 63, 97, 103, 121, 105, 105, 119, 117, 121, 63, 96, 96, 96, 96, 96, 60, 62, 94, 94, 96, 96, 96, 58, 45, 105, 126, 96, 58, 96, 105, 59, 74, 96, 65, 66, 69, 90, 72, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 89, 70, 50, 99, 106, 67, 77, 83, 73, 74, 115, 105, 106, 119, 86, 118, 89, 121, 104, 101, 73, 105, 51, 100, 71, 113, 87, 119, 85, 83, 79, 96, 96, 119, 113, 113, 104, 110, 110, 117, 103, 102, 111, 58, 108, 58, 108, 118, 96, 108, 111, 96, 44, 108, 111, 46, 108, 111, 86, 44, 42, 111, 111, 45, 111, 46, 73, 111, 86, 46, 46, 58, 58, 79, 108, 96, 96, 95, 58, 111, 63, 79, 56, 57, 111, 57, 56, 58, 111, 56, 79, 79, 57, 111, 111, 111, 111, 111, 111, 111, 111, 57, 111, 111, 111, 111, 111, 121, 111, 85, 79, 68, 82, 84, 105, 89, 65, 74, 69, 63, 87, 77, 72, 89, 71, 104, 90, 52, 98, 82, 87, 83, 86, 83, 76, 67, 80, 75, 100, 54, 71, 66, 61, 86, 62, 60, 96, 85, 80, 100, 98, 74, 76, 50, 120, 72, 120, 82, 98, 70, 65, 68, 68, 77, 66, 88, 120, 32, 60, 88, 73, 96, 75, 77, 58, 43, 47, 58, 58, 99, 111, 111, 117, 118, 119, 122, 114, 103, 121, 102, 121, 96, 105, 96, 126, 96, 96, 96, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 45, 45, 45, 44, 96, 46, 32, 32, 32, 96, 60, 62, 47, 45, 47, 42, 126, 58, 32, 67, 103, 72, 72, 72, 104, 73, 73, 76, 108, 78, 80, 81, 82, 82, 82, 90, 90, 75, 66, 67, 101, 101, 69, 70, 77, 111, 105, 121, 68, 100, 101, 105, 106, 73, 86, 88, 76, 67, 68, 77, 105, 118, 120, 73, 99, 100, 45, 47, 92, 42, 73, 118, 85, 58, 126, 84, 118, 85, 69, 105, 112, 97, 73, 47, 88, 40, 41, 60, 62, 40, 41, 123, 125, 43, 45, 47, 92, 84, 120, 120, 92, 47, 92, 120, 114, 72, 73, 75, 77, 78, 79, 111, 80, 112, 67, 99, 84, 89, 88, 45, 47, 57, 51, 76, 54, 86, 69, 73, 33, 79, 81, 88, 61, 92, 47, 79, 40, 41, 47, 61, 47, 92, 92, 47, 66, 80, 100, 68, 84, 71, 75, 74, 67, 90, 70, 77, 78, 76, 83, 82, 86, 72, 87, 88, 89, 65, 69, 73, 79, 85, 46, 44, 58, 61, 46, 50, 105, 86, 63, 50, 115, 50, 51, 57, 38, 58, 96, 70, 102, 117, 51, 74, 88, 66, 101, 102, 111, 114, 114, 117, 117, 121, 105, 114, 119, 122, 118, 115, 99, 111, 111, 111, 111, 111, 111, 111, 111, 40, 41, 58, 95, 95, 95, 45, 92, 108, 108, 111, 111, 111, 111, 34, 36, 37, 38, 96, 42, 43, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 73, 66, 69, 70, 124, 88, 79, 80, 83, 84, 43, 65, 66, 67, 70, 79, 77, 84, 89, 88, 72, 90, 66, 67, 124, 77, 84, 88, 56, 42, 108, 88, 79, 67, 76, 83, 111, 99, 115, 82, 79, 85, 55, 111, 117, 78, 79, 75, 67, 86, 70, 76, 88, 46, 79, 118, 119, 119, 119, 86, 70, 76, 89, 69, 90, 57, 69, 52, 76, 79, 85, 53, 84, 118, 115, 70, 105, 122, 55, 111, 51, 57, 54, 57, 111, 117, 121, 79, 90, 87, 67, 88, 87, 67, 86, 84, 76, 73, 82, 83, 51, 62, 65, 85, 89, 96, 96, 123, 46, 51, 86, 92, 55, 70, 82, 76, 60, 62, 47, 92, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 67, 68, 71, 74, 75, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 102, 104, 105, 106, 107, 108, 110, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 68, 69, 70, 71, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 68, 69, 70, 71, 73, 74, 75, 76, 77, 79, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 105, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 70, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 108, 56, 108, 111, 111, 108, 111, 67, 84, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57}, - Locale: "_common", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 184, Hi: 383, Stride: 199}, - {Lo: 388, Hi: 397, Stride: 9}, - {Lo: 422, Hi: 423, Stride: 1}, - {Lo: 439, Hi: 444, Stride: 5}, - {Lo: 445, Hi: 451, Stride: 3}, - {Lo: 540, Hi: 546, Stride: 6}, - {Lo: 547, Hi: 577, Stride: 30}, - {Lo: 593, Hi: 609, Stride: 16}, - {Lo: 611, Hi: 617, Stride: 6}, - {Lo: 618, Hi: 623, Stride: 5}, - {Lo: 651, Hi: 655, Stride: 4}, - {Lo: 660, Hi: 697, Stride: 37}, - {Lo: 699, Hi: 702, Stride: 1}, - {Lo: 706, Hi: 708, Stride: 1}, - {Lo: 710, Hi: 714, Stride: 2}, - {Lo: 715, Hi: 720, Stride: 5}, - {Lo: 727, Hi: 731, Stride: 4}, - {Lo: 732, Hi: 756, Stride: 24}, - {Lo: 760, Hi: 884, Stride: 124}, - {Lo: 890, Hi: 894, Stride: 4}, - {Lo: 895, Hi: 900, Stride: 5}, - {Lo: 913, Hi: 914, Stride: 1}, - {Lo: 917, Hi: 919, Stride: 1}, - {Lo: 922, Hi: 924, Stride: 2}, - {Lo: 925, Hi: 929, Stride: 2}, - {Lo: 932, Hi: 933, Stride: 1}, - {Lo: 935, Hi: 945, Stride: 10}, - {Lo: 947, Hi: 953, Stride: 6}, - {Lo: 957, Hi: 965, Stride: 2}, - {Lo: 978, Hi: 988, Stride: 10}, - {Lo: 1000, Hi: 1010, Stride: 10}, - {Lo: 1011, Hi: 1017, Stride: 6}, - {Lo: 1018, Hi: 1029, Stride: 11}, - {Lo: 1030, Hi: 1032, Stride: 2}, - {Lo: 1109, Hi: 1110, Stride: 1}, - {Lo: 1112, Hi: 1121, Stride: 9}, - {Lo: 1140, Hi: 1141, Stride: 1}, - {Lo: 1198, Hi: 1199, Stride: 1}, - {Lo: 1211, Hi: 1213, Stride: 2}, - {Lo: 1216, Hi: 1231, Stride: 15}, - {Lo: 1248, Hi: 1281, Stride: 33}, - {Lo: 1292, Hi: 1307, Stride: 15}, - {Lo: 1308, Hi: 1309, Stride: 1}, - {Lo: 1357, Hi: 1359, Stride: 2}, - {Lo: 1365, Hi: 1370, Stride: 5}, - {Lo: 1373, Hi: 1377, Stride: 4}, - {Lo: 1379, Hi: 1382, Stride: 3}, - {Lo: 1392, Hi: 1400, Stride: 8}, - {Lo: 1404, Hi: 1405, Stride: 1}, - {Lo: 1409, Hi: 1412, Stride: 3}, - {Lo: 1413, Hi: 1417, Stride: 4}, - {Lo: 1472, Hi: 1475, Stride: 3}, - {Lo: 1493, Hi: 1496, Stride: 3}, - {Lo: 1497, Hi: 1503, Stride: 6}, - {Lo: 1505, Hi: 1523, Stride: 18}, - {Lo: 1549, Hi: 1575, Stride: 26}, - {Lo: 1607, Hi: 1632, Stride: 25}, - {Lo: 1633, Hi: 1637, Stride: 4}, - {Lo: 1639, Hi: 1643, Stride: 4}, - {Lo: 1645, Hi: 1726, Stride: 81}, - {Lo: 1729, Hi: 1748, Stride: 19}, - {Lo: 1749, Hi: 1776, Stride: 27}, - {Lo: 1777, Hi: 1781, Stride: 4}, - {Lo: 1783, Hi: 1793, Stride: 10}, - {Lo: 1794, Hi: 1796, Stride: 1}, - {Lo: 1984, Hi: 1994, Stride: 10}, - {Lo: 2036, Hi: 2037, Stride: 1}, - {Lo: 2042, Hi: 2307, Stride: 265}, - {Lo: 2406, Hi: 2429, Stride: 23}, - {Lo: 2534, Hi: 2538, Stride: 4}, - {Lo: 2541, Hi: 2662, Stride: 121}, - {Lo: 2663, Hi: 2666, Stride: 3}, - {Lo: 2691, Hi: 2790, Stride: 99}, - {Lo: 2819, Hi: 2848, Stride: 29}, - {Lo: 2918, Hi: 2920, Stride: 2}, - {Lo: 3046, Hi: 3074, Stride: 28}, - {Lo: 3174, Hi: 3202, Stride: 28}, - {Lo: 3302, Hi: 3330, Stride: 28}, - {Lo: 3360, Hi: 3430, Stride: 70}, - {Lo: 3437, Hi: 3458, Stride: 21}, - {Lo: 3664, Hi: 3792, Stride: 128}, - {Lo: 4125, Hi: 4160, Stride: 35}, - {Lo: 4327, Hi: 4351, Stride: 24}, - {Lo: 4608, Hi: 5024, Stride: 208}, - {Lo: 5025, Hi: 5026, Stride: 1}, - {Lo: 5029, Hi: 5033, Stride: 4}, - {Lo: 5034, Hi: 5036, Stride: 1}, - {Lo: 5038, Hi: 5043, Stride: 5}, - {Lo: 5047, Hi: 5051, Stride: 4}, - {Lo: 5053, Hi: 5056, Stride: 3}, - {Lo: 5058, Hi: 5059, Stride: 1}, - {Lo: 5070, Hi: 5071, Stride: 1}, - {Lo: 5074, Hi: 5076, Stride: 2}, - {Lo: 5077, Hi: 5081, Stride: 4}, - {Lo: 5082, Hi: 5086, Stride: 4}, - {Lo: 5087, Hi: 5090, Stride: 3}, - {Lo: 5094, Hi: 5095, Stride: 1}, - {Lo: 5102, Hi: 5107, Stride: 5}, - {Lo: 5108, Hi: 5120, Stride: 12}, - {Lo: 5167, Hi: 5171, Stride: 4}, - {Lo: 5176, Hi: 5194, Stride: 18}, - {Lo: 5196, Hi: 5229, Stride: 33}, - {Lo: 5231, Hi: 5234, Stride: 3}, - {Lo: 5261, Hi: 5290, Stride: 29}, - {Lo: 5311, Hi: 5441, Stride: 130}, - {Lo: 5500, Hi: 5501, Stride: 1}, - {Lo: 5511, Hi: 5551, Stride: 40}, - {Lo: 5556, Hi: 5573, Stride: 17}, - {Lo: 5598, Hi: 5610, Stride: 12}, - {Lo: 5616, Hi: 5623, Stride: 7}, - {Lo: 5741, Hi: 5742, Stride: 1}, - {Lo: 5760, Hi: 5810, Stride: 50}, - {Lo: 5815, Hi: 5825, Stride: 10}, - {Lo: 5836, Hi: 5845, Stride: 9}, - {Lo: 5846, Hi: 5868, Stride: 22}, - {Lo: 5869, Hi: 5941, Stride: 72}, - {Lo: 6147, Hi: 6153, Stride: 6}, - {Lo: 7428, Hi: 7439, Stride: 11}, - {Lo: 7441, Hi: 7452, Stride: 11}, - {Lo: 7456, Hi: 7458, Stride: 1}, - {Lo: 7462, Hi: 7555, Stride: 93}, - {Lo: 7564, Hi: 7837, Stride: 273}, - {Lo: 7935, Hi: 8125, Stride: 190}, - {Lo: 8126, Hi: 8128, Stride: 1}, - {Lo: 8175, Hi: 8189, Stride: 14}, - {Lo: 8190, Hi: 8192, Stride: 2}, - {Lo: 8193, Hi: 8202, Stride: 1}, - {Lo: 8208, Hi: 8210, Stride: 1}, - {Lo: 8218, Hi: 8219, Stride: 1}, - {Lo: 8228, Hi: 8232, Stride: 4}, - {Lo: 8233, Hi: 8239, Stride: 6}, - {Lo: 8242, Hi: 8249, Stride: 7}, - {Lo: 8250, Hi: 8257, Stride: 7}, - {Lo: 8259, Hi: 8260, Stride: 1}, - {Lo: 8270, Hi: 8275, Stride: 5}, - {Lo: 8282, Hi: 8287, Stride: 5}, - {Lo: 8450, Hi: 8458, Stride: 8}, - {Lo: 8459, Hi: 8462, Stride: 1}, - {Lo: 8464, Hi: 8467, Stride: 1}, - {Lo: 8469, Hi: 8473, Stride: 4}, - {Lo: 8474, Hi: 8477, Stride: 1}, - {Lo: 8484, Hi: 8488, Stride: 4}, - {Lo: 8490, Hi: 8492, Stride: 2}, - {Lo: 8493, Hi: 8497, Stride: 1}, - {Lo: 8499, Hi: 8500, Stride: 1}, - {Lo: 8505, Hi: 8509, Stride: 4}, - {Lo: 8517, Hi: 8521, Stride: 1}, - {Lo: 8544, Hi: 8548, Stride: 4}, - {Lo: 8553, Hi: 8556, Stride: 3}, - {Lo: 8557, Hi: 8560, Stride: 1}, - {Lo: 8564, Hi: 8569, Stride: 5}, - {Lo: 8572, Hi: 8574, Stride: 1}, - {Lo: 8722, Hi: 8725, Stride: 3}, - {Lo: 8726, Hi: 8727, Stride: 1}, - {Lo: 8739, Hi: 8744, Stride: 5}, - {Lo: 8746, Hi: 8758, Stride: 12}, - {Lo: 8764, Hi: 8868, Stride: 104}, - {Lo: 8897, Hi: 8899, Stride: 2}, - {Lo: 8959, Hi: 9075, Stride: 116}, - {Lo: 9076, Hi: 9082, Stride: 6}, - {Lo: 9213, Hi: 9585, Stride: 372}, - {Lo: 9587, Hi: 10088, Stride: 501}, - {Lo: 10089, Hi: 10094, Stride: 5}, - {Lo: 10095, Hi: 10098, Stride: 3}, - {Lo: 10099, Hi: 10101, Stride: 1}, - {Lo: 10133, Hi: 10134, Stride: 1}, - {Lo: 10187, Hi: 10189, Stride: 2}, - {Lo: 10201, Hi: 10539, Stride: 338}, - {Lo: 10540, Hi: 10741, Stride: 201}, - {Lo: 10744, Hi: 10745, Stride: 1}, - {Lo: 10799, Hi: 11397, Stride: 598}, - {Lo: 11406, Hi: 11410, Stride: 4}, - {Lo: 11412, Hi: 11416, Stride: 4}, - {Lo: 11418, Hi: 11422, Stride: 4}, - {Lo: 11423, Hi: 11426, Stride: 3}, - {Lo: 11427, Hi: 11430, Stride: 1}, - {Lo: 11432, Hi: 11436, Stride: 4}, - {Lo: 11450, Hi: 11462, Stride: 12}, - {Lo: 11466, Hi: 11468, Stride: 2}, - {Lo: 11472, Hi: 11474, Stride: 2}, - {Lo: 11576, Hi: 11577, Stride: 1}, - {Lo: 11599, Hi: 11601, Stride: 2}, - {Lo: 11604, Hi: 11605, Stride: 1}, - {Lo: 11613, Hi: 11840, Stride: 227}, - {Lo: 12034, Hi: 12035, Stride: 1}, - {Lo: 12295, Hi: 12308, Stride: 13}, - {Lo: 12309, Hi: 12339, Stride: 30}, - {Lo: 12448, Hi: 12755, Stride: 307}, - {Lo: 12756, Hi: 20022, Stride: 7266}, - {Lo: 20031, Hi: 42192, Stride: 22161}, - {Lo: 42193, Hi: 42196, Stride: 1}, - {Lo: 42198, Hi: 42199, Stride: 1}, - {Lo: 42201, Hi: 42202, Stride: 1}, - {Lo: 42204, Hi: 42205, Stride: 1}, - {Lo: 42207, Hi: 42211, Stride: 1}, - {Lo: 42214, Hi: 42215, Stride: 1}, - {Lo: 42218, Hi: 42220, Stride: 1}, - {Lo: 42222, Hi: 42226, Stride: 2}, - {Lo: 42227, Hi: 42228, Stride: 1}, - {Lo: 42232, Hi: 42233, Stride: 1}, - {Lo: 42237, Hi: 42239, Stride: 2}, - {Lo: 42510, Hi: 42564, Stride: 54}, - {Lo: 42567, Hi: 42719, Stride: 152}, - {Lo: 42731, Hi: 42735, Stride: 4}, - {Lo: 42801, Hi: 42842, Stride: 41}, - {Lo: 42858, Hi: 42862, Stride: 4}, - {Lo: 42872, Hi: 42889, Stride: 17}, - {Lo: 42892, Hi: 42904, Stride: 12}, - {Lo: 42905, Hi: 42911, Stride: 6}, - {Lo: 42923, Hi: 42930, Stride: 7}, - {Lo: 42931, Hi: 42932, Stride: 1}, - {Lo: 43826, Hi: 43829, Stride: 3}, - {Lo: 43837, Hi: 43847, Stride: 10}, - {Lo: 43848, Hi: 43854, Stride: 6}, - {Lo: 43858, Hi: 43866, Stride: 8}, - {Lo: 43893, Hi: 43905, Stride: 12}, - {Lo: 43907, Hi: 43923, Stride: 16}, - {Lo: 43945, Hi: 43946, Stride: 1}, - {Lo: 43951, Hi: 64422, Stride: 20471}, - {Lo: 64423, Hi: 64429, Stride: 1}, - {Lo: 64830, Hi: 64831, Stride: 1}, - {Lo: 65072, Hi: 65101, Stride: 29}, - {Lo: 65102, Hi: 65103, Stride: 1}, - {Lo: 65112, Hi: 65128, Stride: 16}, - {Lo: 65165, Hi: 65166, Stride: 1}, - {Lo: 65257, Hi: 65260, Stride: 1}, - {Lo: 65282, Hi: 65284, Stride: 2}, - {Lo: 65285, Hi: 65287, Stride: 1}, - {Lo: 65290, Hi: 65291, Stride: 1}, - {Lo: 65293, Hi: 65305, Stride: 1}, - {Lo: 65308, Hi: 65310, Stride: 1}, - {Lo: 65312, Hi: 65373, Stride: 1}, - {Lo: 65512, Hi: 65512, Stride: 1}, +func newAmbiguousTableMap() map[string]*AmbiguousTable { + return map[string]*AmbiguousTable{ + "_common": { + Confusable: []rune{184, 383, 388, 397, 422, 423, 439, 444, 445, 448, 451, 540, 546, 547, 577, 593, 609, 611, 617, 618, 623, 651, 655, 660, 697, 699, 700, 701, 702, 706, 707, 708, 710, 712, 714, 715, 720, 727, 731, 732, 756, 760, 884, 890, 894, 895, 900, 913, 914, 917, 918, 919, 922, 924, 925, 927, 929, 932, 933, 935, 945, 947, 953, 957, 959, 961, 963, 965, 978, 988, 1000, 1010, 1011, 1017, 1018, 1029, 1030, 1032, 1109, 1110, 1112, 1121, 1140, 1141, 1198, 1199, 1211, 1213, 1216, 1231, 1248, 1281, 1292, 1307, 1308, 1309, 1357, 1359, 1365, 1370, 1373, 1377, 1379, 1382, 1392, 1400, 1404, 1405, 1409, 1412, 1413, 1417, 1472, 1475, 1493, 1496, 1497, 1503, 1505, 1523, 1549, 1575, 1607, 1632, 1633, 1637, 1639, 1643, 1645, 1726, 1729, 1748, 1749, 1776, 1777, 1781, 1783, 1793, 1794, 1795, 1796, 1984, 1994, 2036, 2037, 2042, 2307, 2406, 2429, 2534, 2538, 2541, 2662, 2663, 2666, 2691, 2790, 2819, 2848, 2918, 2920, 3046, 3074, 3174, 3202, 3302, 3330, 3360, 3430, 3437, 3458, 3664, 3792, 4125, 4160, 4327, 4351, 4608, 4816, 5024, 5025, 5026, 5029, 5033, 5034, 5035, 5036, 5038, 5043, 5047, 5051, 5053, 5056, 5058, 5059, 5070, 5071, 5074, 5076, 5077, 5081, 5082, 5086, 5087, 5090, 5094, 5095, 5102, 5107, 5108, 5120, 5167, 5171, 5176, 5194, 5196, 5229, 5231, 5234, 5261, 5290, 5311, 5441, 5500, 5501, 5511, 5551, 5556, 5573, 5598, 5610, 5616, 5623, 5741, 5742, 5760, 5810, 5815, 5825, 5836, 5845, 5846, 5868, 5869, 5941, 6147, 6153, 7428, 7439, 7441, 7452, 7456, 7457, 7458, 7462, 7555, 7564, 7837, 7935, 8125, 8126, 8127, 8128, 8175, 8189, 8190, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8208, 8209, 8210, 8218, 8219, 8228, 8232, 8233, 8239, 8242, 8249, 8250, 8257, 8259, 8260, 8270, 8275, 8282, 8287, 8450, 8458, 8459, 8460, 8461, 8462, 8464, 8465, 8466, 8467, 8469, 8473, 8474, 8475, 8476, 8477, 8484, 8488, 8490, 8492, 8493, 8494, 8495, 8496, 8497, 8499, 8500, 8505, 8509, 8517, 8518, 8519, 8520, 8521, 8544, 8548, 8553, 8556, 8557, 8558, 8559, 8560, 8564, 8569, 8572, 8573, 8574, 8722, 8725, 8726, 8727, 8739, 8744, 8746, 8758, 8764, 8868, 8897, 8899, 8959, 9075, 9076, 9082, 9213, 9585, 9587, 10088, 10089, 10094, 10095, 10098, 10099, 10100, 10101, 10133, 10134, 10187, 10189, 10201, 10539, 10540, 10741, 10744, 10745, 10799, 11397, 11406, 11410, 11412, 11416, 11418, 11422, 11423, 11426, 11427, 11428, 11429, 11430, 11432, 11436, 11450, 11462, 11466, 11468, 11472, 11474, 11576, 11577, 11599, 11601, 11604, 11605, 11613, 11840, 12034, 12035, 12295, 12308, 12309, 12339, 12448, 12755, 12756, 20022, 20031, 42192, 42193, 42194, 42195, 42196, 42198, 42199, 42201, 42202, 42204, 42205, 42207, 42208, 42209, 42210, 42211, 42214, 42215, 42218, 42219, 42220, 42222, 42224, 42226, 42227, 42228, 42232, 42233, 42237, 42239, 42510, 42564, 42567, 42719, 42731, 42735, 42801, 42842, 42858, 42862, 42872, 42889, 42892, 42904, 42905, 42911, 42923, 42930, 42931, 42932, 43826, 43829, 43837, 43847, 43848, 43854, 43858, 43866, 43893, 43905, 43907, 43923, 43945, 43946, 43951, 64422, 64423, 64424, 64425, 64426, 64427, 64428, 64429, 64830, 64831, 65072, 65101, 65102, 65103, 65112, 65128, 65165, 65166, 65257, 65258, 65259, 65260, 65282, 65284, 65285, 65286, 65287, 65290, 65291, 65293, 65294, 65295, 65296, 65297, 65298, 65299, 65300, 65301, 65302, 65303, 65304, 65305, 65308, 65309, 65310, 65312, 65313, 65314, 65315, 65316, 65317, 65318, 65319, 65320, 65321, 65322, 65323, 65324, 65325, 65326, 65327, 65328, 65329, 65330, 65331, 65332, 65333, 65334, 65335, 65336, 65337, 65338, 65339, 65340, 65341, 65342, 65343, 65344, 65345, 65346, 65347, 65348, 65349, 65350, 65351, 65352, 65353, 65354, 65355, 65356, 65357, 65358, 65359, 65360, 65361, 65362, 65363, 65364, 65365, 65366, 65367, 65368, 65369, 65370, 65371, 65372, 65373, 65512, 66178, 66182, 66183, 66186, 66192, 66194, 66197, 66198, 66199, 66203, 66208, 66209, 66210, 66213, 66219, 66224, 66225, 66226, 66228, 66255, 66293, 66305, 66306, 66313, 66321, 66325, 66327, 66330, 66335, 66336, 66338, 66564, 66581, 66587, 66592, 66604, 66621, 66632, 66740, 66754, 66766, 66770, 66794, 66806, 66835, 66838, 66840, 66844, 66845, 66853, 66854, 66855, 68176, 70864, 71430, 71434, 71438, 71439, 71840, 71842, 71843, 71844, 71846, 71849, 71852, 71854, 71855, 71858, 71861, 71864, 71867, 71868, 71872, 71873, 71874, 71875, 71876, 71878, 71880, 71882, 71884, 71893, 71894, 71895, 71896, 71900, 71904, 71909, 71910, 71913, 71916, 71919, 71922, 93960, 93962, 93974, 93992, 94005, 94010, 94011, 94015, 94016, 94018, 94019, 94033, 94034, 119060, 119149, 119302, 119309, 119311, 119314, 119315, 119318, 119338, 119350, 119351, 119354, 119355, 119808, 119809, 119810, 119811, 119812, 119813, 119814, 119815, 119816, 119817, 119818, 119819, 119820, 119821, 119822, 119823, 119824, 119825, 119826, 119827, 119828, 119829, 119830, 119831, 119832, 119833, 119834, 119835, 119836, 119837, 119838, 119839, 119840, 119841, 119842, 119843, 119844, 119845, 119847, 119848, 119849, 119850, 119851, 119852, 119853, 119854, 119855, 119856, 119857, 119858, 119859, 119860, 119861, 119862, 119863, 119864, 119865, 119866, 119867, 119868, 119869, 119870, 119871, 119872, 119873, 119874, 119875, 119876, 119877, 119878, 119879, 119880, 119881, 119882, 119883, 119884, 119885, 119886, 119887, 119888, 119889, 119890, 119891, 119892, 119894, 119895, 119896, 119897, 119899, 119900, 119901, 119902, 119903, 119904, 119905, 119906, 119907, 119908, 119909, 119910, 119911, 119912, 119913, 119914, 119915, 119916, 119917, 119918, 119919, 119920, 119921, 119922, 119923, 119924, 119925, 119926, 119927, 119928, 119929, 119930, 119931, 119932, 119933, 119934, 119935, 119936, 119937, 119938, 119939, 119940, 119941, 119942, 119943, 119944, 119945, 119946, 119947, 119948, 119949, 119951, 119952, 119953, 119954, 119955, 119956, 119957, 119958, 119959, 119960, 119961, 119962, 119963, 119964, 119966, 119967, 119970, 119973, 119974, 119977, 119978, 119979, 119980, 119982, 119983, 119984, 119985, 119986, 119987, 119988, 119989, 119990, 119991, 119992, 119993, 119995, 119997, 119998, 119999, 120000, 120001, 120003, 120005, 120006, 120007, 120008, 120009, 120010, 120011, 120012, 120013, 120014, 120015, 120016, 120017, 120018, 120019, 120020, 120021, 120022, 120023, 120024, 120025, 120026, 120027, 120028, 120029, 120030, 120031, 120032, 120033, 120034, 120035, 120036, 120037, 120038, 120039, 120040, 120041, 120042, 120043, 120044, 120045, 120046, 120047, 120048, 120049, 120050, 120051, 120052, 120053, 120055, 120056, 120057, 120058, 120059, 120060, 120061, 120062, 120063, 120064, 120065, 120066, 120067, 120068, 120069, 120071, 120072, 120073, 120074, 120077, 120078, 120079, 120080, 120081, 120082, 120083, 120084, 120086, 120087, 120088, 120089, 120090, 120091, 120092, 120094, 120095, 120096, 120097, 120098, 120099, 120100, 120101, 120102, 120103, 120104, 120105, 120107, 120108, 120109, 120110, 120111, 120112, 120113, 120114, 120115, 120116, 120117, 120118, 120119, 120120, 120121, 120123, 120124, 120125, 120126, 120128, 120129, 120130, 120131, 120132, 120134, 120138, 120139, 120140, 120141, 120142, 120143, 120144, 120146, 120147, 120148, 120149, 120150, 120151, 120152, 120153, 120154, 120155, 120156, 120157, 120159, 120160, 120161, 120162, 120163, 120164, 120165, 120166, 120167, 120168, 120169, 120170, 120171, 120172, 120173, 120174, 120175, 120176, 120177, 120178, 120179, 120180, 120181, 120182, 120183, 120184, 120185, 120186, 120187, 120188, 120189, 120190, 120191, 120192, 120193, 120194, 120195, 120196, 120197, 120198, 120199, 120200, 120201, 120202, 120203, 120204, 120205, 120206, 120207, 120208, 120209, 120211, 120212, 120213, 120214, 120215, 120216, 120217, 120218, 120219, 120220, 120221, 120222, 120223, 120224, 120225, 120226, 120227, 120228, 120229, 120230, 120231, 120232, 120233, 120234, 120235, 120236, 120237, 120238, 120239, 120240, 120241, 120242, 120243, 120244, 120245, 120246, 120247, 120248, 120249, 120250, 120251, 120252, 120253, 120254, 120255, 120256, 120257, 120258, 120259, 120260, 120261, 120263, 120264, 120265, 120266, 120267, 120268, 120269, 120270, 120271, 120272, 120273, 120274, 120275, 120276, 120277, 120278, 120279, 120280, 120281, 120282, 120283, 120284, 120285, 120286, 120287, 120288, 120289, 120290, 120291, 120292, 120293, 120294, 120295, 120296, 120297, 120298, 120299, 120300, 120301, 120302, 120303, 120304, 120305, 120306, 120307, 120308, 120309, 120310, 120311, 120312, 120313, 120315, 120316, 120317, 120318, 120319, 120320, 120321, 120322, 120323, 120324, 120325, 120326, 120327, 120328, 120329, 120330, 120331, 120332, 120333, 120334, 120335, 120336, 120337, 120338, 120339, 120340, 120341, 120342, 120343, 120344, 120345, 120346, 120347, 120348, 120349, 120350, 120351, 120352, 120353, 120354, 120355, 120356, 120357, 120358, 120359, 120360, 120361, 120362, 120363, 120364, 120365, 120367, 120368, 120369, 120370, 120371, 120372, 120373, 120374, 120375, 120376, 120377, 120378, 120379, 120380, 120381, 120382, 120383, 120384, 120385, 120386, 120387, 120388, 120389, 120390, 120391, 120392, 120393, 120394, 120395, 120396, 120397, 120398, 120399, 120400, 120401, 120402, 120403, 120404, 120405, 120406, 120407, 120408, 120409, 120410, 120411, 120412, 120413, 120414, 120415, 120416, 120417, 120419, 120420, 120421, 120422, 120423, 120424, 120425, 120426, 120427, 120428, 120429, 120430, 120431, 120432, 120433, 120434, 120435, 120436, 120437, 120438, 120439, 120440, 120441, 120442, 120443, 120444, 120445, 120446, 120447, 120448, 120449, 120450, 120451, 120452, 120453, 120454, 120455, 120456, 120457, 120458, 120459, 120460, 120461, 120462, 120463, 120464, 120465, 120466, 120467, 120468, 120469, 120471, 120472, 120473, 120474, 120475, 120476, 120477, 120478, 120479, 120480, 120481, 120482, 120483, 120484, 120488, 120489, 120492, 120493, 120494, 120496, 120497, 120499, 120500, 120502, 120504, 120507, 120508, 120510, 120514, 120516, 120522, 120526, 120528, 120530, 120532, 120534, 120544, 120546, 120547, 120550, 120551, 120552, 120554, 120555, 120557, 120558, 120560, 120562, 120565, 120566, 120568, 120572, 120574, 120580, 120584, 120586, 120588, 120590, 120592, 120602, 120604, 120605, 120608, 120609, 120610, 120612, 120613, 120615, 120616, 120618, 120620, 120623, 120624, 120626, 120630, 120632, 120638, 120642, 120644, 120646, 120648, 120650, 120660, 120662, 120663, 120666, 120667, 120668, 120670, 120671, 120673, 120674, 120676, 120678, 120681, 120682, 120684, 120688, 120690, 120696, 120700, 120702, 120704, 120706, 120708, 120718, 120720, 120721, 120724, 120725, 120726, 120728, 120729, 120731, 120732, 120734, 120736, 120739, 120740, 120742, 120746, 120748, 120754, 120758, 120760, 120762, 120764, 120766, 120776, 120778, 120782, 120783, 120784, 120785, 120786, 120787, 120788, 120789, 120790, 120791, 120792, 120793, 120794, 120795, 120796, 120797, 120798, 120799, 120800, 120801, 120802, 120803, 120804, 120805, 120806, 120807, 120808, 120809, 120810, 120811, 120812, 120813, 120814, 120815, 120816, 120817, 120818, 120819, 120820, 120821, 120822, 120823, 120824, 120825, 120826, 120827, 120828, 120829, 120830, 120831, 125127, 125131, 126464, 126500, 126564, 126592, 126596, 128844, 128872, 130032, 130033, 130034, 130035, 130036, 130037, 130038, 130039, 130040, 130041}, + With: []rune{44, 102, 98, 103, 82, 50, 51, 53, 115, 73, 33, 51, 56, 56, 63, 97, 103, 121, 105, 105, 119, 117, 121, 63, 96, 96, 96, 96, 96, 60, 62, 94, 94, 96, 96, 96, 58, 45, 105, 126, 96, 58, 96, 105, 59, 74, 96, 65, 66, 69, 90, 72, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 89, 70, 50, 99, 106, 67, 77, 83, 73, 74, 115, 105, 106, 119, 86, 118, 89, 121, 104, 101, 73, 105, 51, 100, 71, 113, 87, 119, 85, 83, 79, 96, 96, 119, 113, 113, 104, 110, 110, 117, 103, 102, 111, 58, 108, 58, 108, 118, 96, 108, 111, 96, 44, 108, 111, 46, 108, 111, 86, 44, 42, 111, 111, 45, 111, 46, 73, 111, 86, 46, 46, 58, 58, 79, 108, 96, 96, 95, 58, 111, 63, 79, 56, 57, 111, 57, 56, 58, 111, 56, 79, 79, 57, 111, 111, 111, 111, 111, 111, 111, 111, 57, 111, 111, 111, 111, 111, 121, 111, 85, 79, 68, 82, 84, 105, 89, 65, 74, 69, 63, 87, 77, 72, 89, 71, 104, 90, 52, 98, 82, 87, 83, 86, 83, 76, 67, 80, 75, 100, 54, 71, 66, 61, 86, 62, 60, 96, 85, 80, 100, 98, 74, 76, 50, 120, 72, 120, 82, 98, 70, 65, 68, 68, 77, 66, 88, 120, 32, 60, 88, 73, 96, 75, 77, 58, 43, 47, 58, 58, 99, 111, 111, 117, 118, 119, 122, 114, 103, 121, 102, 121, 96, 105, 96, 126, 96, 96, 96, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 45, 45, 45, 44, 96, 46, 32, 32, 32, 96, 60, 62, 47, 45, 47, 42, 126, 58, 32, 67, 103, 72, 72, 72, 104, 73, 73, 76, 108, 78, 80, 81, 82, 82, 82, 90, 90, 75, 66, 67, 101, 101, 69, 70, 77, 111, 105, 121, 68, 100, 101, 105, 106, 73, 86, 88, 76, 67, 68, 77, 105, 118, 120, 73, 99, 100, 45, 47, 92, 42, 73, 118, 85, 58, 126, 84, 118, 85, 69, 105, 112, 97, 73, 47, 88, 40, 41, 60, 62, 40, 41, 123, 125, 43, 45, 47, 92, 84, 120, 120, 92, 47, 92, 120, 114, 72, 73, 75, 77, 78, 79, 111, 80, 112, 67, 99, 84, 89, 88, 45, 47, 57, 51, 76, 54, 86, 69, 73, 33, 79, 81, 88, 61, 92, 47, 79, 40, 41, 47, 61, 47, 92, 92, 47, 66, 80, 100, 68, 84, 71, 75, 74, 67, 90, 70, 77, 78, 76, 83, 82, 86, 72, 87, 88, 89, 65, 69, 73, 79, 85, 46, 44, 58, 61, 46, 50, 105, 86, 63, 50, 115, 50, 51, 57, 38, 58, 96, 70, 102, 117, 51, 74, 88, 66, 101, 102, 111, 114, 114, 117, 117, 121, 105, 114, 119, 122, 118, 115, 99, 111, 111, 111, 111, 111, 111, 111, 111, 40, 41, 58, 95, 95, 95, 45, 92, 108, 108, 111, 111, 111, 111, 34, 36, 37, 38, 96, 42, 43, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 73, 66, 69, 70, 124, 88, 79, 80, 83, 84, 43, 65, 66, 67, 70, 79, 77, 84, 89, 88, 72, 90, 66, 67, 124, 77, 84, 88, 56, 42, 108, 88, 79, 67, 76, 83, 111, 99, 115, 82, 79, 85, 55, 111, 117, 78, 79, 75, 67, 86, 70, 76, 88, 46, 79, 118, 119, 119, 119, 86, 70, 76, 89, 69, 90, 57, 69, 52, 76, 79, 85, 53, 84, 118, 115, 70, 105, 122, 55, 111, 51, 57, 54, 57, 111, 117, 121, 79, 90, 87, 67, 88, 87, 67, 86, 84, 76, 73, 82, 83, 51, 62, 65, 85, 89, 96, 96, 123, 46, 51, 86, 92, 55, 70, 82, 76, 60, 62, 47, 92, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 67, 68, 71, 74, 75, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 102, 104, 105, 106, 107, 108, 110, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 68, 69, 70, 71, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 68, 69, 70, 71, 73, 74, 75, 76, 77, 79, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 105, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 70, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 108, 56, 108, 111, 111, 108, 111, 67, 84, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57}, + Locale: "_common", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 184, Hi: 383, Stride: 199}, + {Lo: 388, Hi: 397, Stride: 9}, + {Lo: 422, Hi: 423, Stride: 1}, + {Lo: 439, Hi: 444, Stride: 5}, + {Lo: 445, Hi: 451, Stride: 3}, + {Lo: 540, Hi: 546, Stride: 6}, + {Lo: 547, Hi: 577, Stride: 30}, + {Lo: 593, Hi: 609, Stride: 16}, + {Lo: 611, Hi: 617, Stride: 6}, + {Lo: 618, Hi: 623, Stride: 5}, + {Lo: 651, Hi: 655, Stride: 4}, + {Lo: 660, Hi: 697, Stride: 37}, + {Lo: 699, Hi: 702, Stride: 1}, + {Lo: 706, Hi: 708, Stride: 1}, + {Lo: 710, Hi: 714, Stride: 2}, + {Lo: 715, Hi: 720, Stride: 5}, + {Lo: 727, Hi: 731, Stride: 4}, + {Lo: 732, Hi: 756, Stride: 24}, + {Lo: 760, Hi: 884, Stride: 124}, + {Lo: 890, Hi: 894, Stride: 4}, + {Lo: 895, Hi: 900, Stride: 5}, + {Lo: 913, Hi: 914, Stride: 1}, + {Lo: 917, Hi: 919, Stride: 1}, + {Lo: 922, Hi: 924, Stride: 2}, + {Lo: 925, Hi: 929, Stride: 2}, + {Lo: 932, Hi: 933, Stride: 1}, + {Lo: 935, Hi: 945, Stride: 10}, + {Lo: 947, Hi: 953, Stride: 6}, + {Lo: 957, Hi: 965, Stride: 2}, + {Lo: 978, Hi: 988, Stride: 10}, + {Lo: 1000, Hi: 1010, Stride: 10}, + {Lo: 1011, Hi: 1017, Stride: 6}, + {Lo: 1018, Hi: 1029, Stride: 11}, + {Lo: 1030, Hi: 1032, Stride: 2}, + {Lo: 1109, Hi: 1110, Stride: 1}, + {Lo: 1112, Hi: 1121, Stride: 9}, + {Lo: 1140, Hi: 1141, Stride: 1}, + {Lo: 1198, Hi: 1199, Stride: 1}, + {Lo: 1211, Hi: 1213, Stride: 2}, + {Lo: 1216, Hi: 1231, Stride: 15}, + {Lo: 1248, Hi: 1281, Stride: 33}, + {Lo: 1292, Hi: 1307, Stride: 15}, + {Lo: 1308, Hi: 1309, Stride: 1}, + {Lo: 1357, Hi: 1359, Stride: 2}, + {Lo: 1365, Hi: 1370, Stride: 5}, + {Lo: 1373, Hi: 1377, Stride: 4}, + {Lo: 1379, Hi: 1382, Stride: 3}, + {Lo: 1392, Hi: 1400, Stride: 8}, + {Lo: 1404, Hi: 1405, Stride: 1}, + {Lo: 1409, Hi: 1412, Stride: 3}, + {Lo: 1413, Hi: 1417, Stride: 4}, + {Lo: 1472, Hi: 1475, Stride: 3}, + {Lo: 1493, Hi: 1496, Stride: 3}, + {Lo: 1497, Hi: 1503, Stride: 6}, + {Lo: 1505, Hi: 1523, Stride: 18}, + {Lo: 1549, Hi: 1575, Stride: 26}, + {Lo: 1607, Hi: 1632, Stride: 25}, + {Lo: 1633, Hi: 1637, Stride: 4}, + {Lo: 1639, Hi: 1643, Stride: 4}, + {Lo: 1645, Hi: 1726, Stride: 81}, + {Lo: 1729, Hi: 1748, Stride: 19}, + {Lo: 1749, Hi: 1776, Stride: 27}, + {Lo: 1777, Hi: 1781, Stride: 4}, + {Lo: 1783, Hi: 1793, Stride: 10}, + {Lo: 1794, Hi: 1796, Stride: 1}, + {Lo: 1984, Hi: 1994, Stride: 10}, + {Lo: 2036, Hi: 2037, Stride: 1}, + {Lo: 2042, Hi: 2307, Stride: 265}, + {Lo: 2406, Hi: 2429, Stride: 23}, + {Lo: 2534, Hi: 2538, Stride: 4}, + {Lo: 2541, Hi: 2662, Stride: 121}, + {Lo: 2663, Hi: 2666, Stride: 3}, + {Lo: 2691, Hi: 2790, Stride: 99}, + {Lo: 2819, Hi: 2848, Stride: 29}, + {Lo: 2918, Hi: 2920, Stride: 2}, + {Lo: 3046, Hi: 3074, Stride: 28}, + {Lo: 3174, Hi: 3202, Stride: 28}, + {Lo: 3302, Hi: 3330, Stride: 28}, + {Lo: 3360, Hi: 3430, Stride: 70}, + {Lo: 3437, Hi: 3458, Stride: 21}, + {Lo: 3664, Hi: 3792, Stride: 128}, + {Lo: 4125, Hi: 4160, Stride: 35}, + {Lo: 4327, Hi: 4351, Stride: 24}, + {Lo: 4608, Hi: 5024, Stride: 208}, + {Lo: 5025, Hi: 5026, Stride: 1}, + {Lo: 5029, Hi: 5033, Stride: 4}, + {Lo: 5034, Hi: 5036, Stride: 1}, + {Lo: 5038, Hi: 5043, Stride: 5}, + {Lo: 5047, Hi: 5051, Stride: 4}, + {Lo: 5053, Hi: 5056, Stride: 3}, + {Lo: 5058, Hi: 5059, Stride: 1}, + {Lo: 5070, Hi: 5071, Stride: 1}, + {Lo: 5074, Hi: 5076, Stride: 2}, + {Lo: 5077, Hi: 5081, Stride: 4}, + {Lo: 5082, Hi: 5086, Stride: 4}, + {Lo: 5087, Hi: 5090, Stride: 3}, + {Lo: 5094, Hi: 5095, Stride: 1}, + {Lo: 5102, Hi: 5107, Stride: 5}, + {Lo: 5108, Hi: 5120, Stride: 12}, + {Lo: 5167, Hi: 5171, Stride: 4}, + {Lo: 5176, Hi: 5194, Stride: 18}, + {Lo: 5196, Hi: 5229, Stride: 33}, + {Lo: 5231, Hi: 5234, Stride: 3}, + {Lo: 5261, Hi: 5290, Stride: 29}, + {Lo: 5311, Hi: 5441, Stride: 130}, + {Lo: 5500, Hi: 5501, Stride: 1}, + {Lo: 5511, Hi: 5551, Stride: 40}, + {Lo: 5556, Hi: 5573, Stride: 17}, + {Lo: 5598, Hi: 5610, Stride: 12}, + {Lo: 5616, Hi: 5623, Stride: 7}, + {Lo: 5741, Hi: 5742, Stride: 1}, + {Lo: 5760, Hi: 5810, Stride: 50}, + {Lo: 5815, Hi: 5825, Stride: 10}, + {Lo: 5836, Hi: 5845, Stride: 9}, + {Lo: 5846, Hi: 5868, Stride: 22}, + {Lo: 5869, Hi: 5941, Stride: 72}, + {Lo: 6147, Hi: 6153, Stride: 6}, + {Lo: 7428, Hi: 7439, Stride: 11}, + {Lo: 7441, Hi: 7452, Stride: 11}, + {Lo: 7456, Hi: 7458, Stride: 1}, + {Lo: 7462, Hi: 7555, Stride: 93}, + {Lo: 7564, Hi: 7837, Stride: 273}, + {Lo: 7935, Hi: 8125, Stride: 190}, + {Lo: 8126, Hi: 8128, Stride: 1}, + {Lo: 8175, Hi: 8189, Stride: 14}, + {Lo: 8190, Hi: 8192, Stride: 2}, + {Lo: 8193, Hi: 8202, Stride: 1}, + {Lo: 8208, Hi: 8210, Stride: 1}, + {Lo: 8218, Hi: 8219, Stride: 1}, + {Lo: 8228, Hi: 8232, Stride: 4}, + {Lo: 8233, Hi: 8239, Stride: 6}, + {Lo: 8242, Hi: 8249, Stride: 7}, + {Lo: 8250, Hi: 8257, Stride: 7}, + {Lo: 8259, Hi: 8260, Stride: 1}, + {Lo: 8270, Hi: 8275, Stride: 5}, + {Lo: 8282, Hi: 8287, Stride: 5}, + {Lo: 8450, Hi: 8458, Stride: 8}, + {Lo: 8459, Hi: 8462, Stride: 1}, + {Lo: 8464, Hi: 8467, Stride: 1}, + {Lo: 8469, Hi: 8473, Stride: 4}, + {Lo: 8474, Hi: 8477, Stride: 1}, + {Lo: 8484, Hi: 8488, Stride: 4}, + {Lo: 8490, Hi: 8492, Stride: 2}, + {Lo: 8493, Hi: 8497, Stride: 1}, + {Lo: 8499, Hi: 8500, Stride: 1}, + {Lo: 8505, Hi: 8509, Stride: 4}, + {Lo: 8517, Hi: 8521, Stride: 1}, + {Lo: 8544, Hi: 8548, Stride: 4}, + {Lo: 8553, Hi: 8556, Stride: 3}, + {Lo: 8557, Hi: 8560, Stride: 1}, + {Lo: 8564, Hi: 8569, Stride: 5}, + {Lo: 8572, Hi: 8574, Stride: 1}, + {Lo: 8722, Hi: 8725, Stride: 3}, + {Lo: 8726, Hi: 8727, Stride: 1}, + {Lo: 8739, Hi: 8744, Stride: 5}, + {Lo: 8746, Hi: 8758, Stride: 12}, + {Lo: 8764, Hi: 8868, Stride: 104}, + {Lo: 8897, Hi: 8899, Stride: 2}, + {Lo: 8959, Hi: 9075, Stride: 116}, + {Lo: 9076, Hi: 9082, Stride: 6}, + {Lo: 9213, Hi: 9585, Stride: 372}, + {Lo: 9587, Hi: 10088, Stride: 501}, + {Lo: 10089, Hi: 10094, Stride: 5}, + {Lo: 10095, Hi: 10098, Stride: 3}, + {Lo: 10099, Hi: 10101, Stride: 1}, + {Lo: 10133, Hi: 10134, Stride: 1}, + {Lo: 10187, Hi: 10189, Stride: 2}, + {Lo: 10201, Hi: 10539, Stride: 338}, + {Lo: 10540, Hi: 10741, Stride: 201}, + {Lo: 10744, Hi: 10745, Stride: 1}, + {Lo: 10799, Hi: 11397, Stride: 598}, + {Lo: 11406, Hi: 11410, Stride: 4}, + {Lo: 11412, Hi: 11416, Stride: 4}, + {Lo: 11418, Hi: 11422, Stride: 4}, + {Lo: 11423, Hi: 11426, Stride: 3}, + {Lo: 11427, Hi: 11430, Stride: 1}, + {Lo: 11432, Hi: 11436, Stride: 4}, + {Lo: 11450, Hi: 11462, Stride: 12}, + {Lo: 11466, Hi: 11468, Stride: 2}, + {Lo: 11472, Hi: 11474, Stride: 2}, + {Lo: 11576, Hi: 11577, Stride: 1}, + {Lo: 11599, Hi: 11601, Stride: 2}, + {Lo: 11604, Hi: 11605, Stride: 1}, + {Lo: 11613, Hi: 11840, Stride: 227}, + {Lo: 12034, Hi: 12035, Stride: 1}, + {Lo: 12295, Hi: 12308, Stride: 13}, + {Lo: 12309, Hi: 12339, Stride: 30}, + {Lo: 12448, Hi: 12755, Stride: 307}, + {Lo: 12756, Hi: 20022, Stride: 7266}, + {Lo: 20031, Hi: 42192, Stride: 22161}, + {Lo: 42193, Hi: 42196, Stride: 1}, + {Lo: 42198, Hi: 42199, Stride: 1}, + {Lo: 42201, Hi: 42202, Stride: 1}, + {Lo: 42204, Hi: 42205, Stride: 1}, + {Lo: 42207, Hi: 42211, Stride: 1}, + {Lo: 42214, Hi: 42215, Stride: 1}, + {Lo: 42218, Hi: 42220, Stride: 1}, + {Lo: 42222, Hi: 42226, Stride: 2}, + {Lo: 42227, Hi: 42228, Stride: 1}, + {Lo: 42232, Hi: 42233, Stride: 1}, + {Lo: 42237, Hi: 42239, Stride: 2}, + {Lo: 42510, Hi: 42564, Stride: 54}, + {Lo: 42567, Hi: 42719, Stride: 152}, + {Lo: 42731, Hi: 42735, Stride: 4}, + {Lo: 42801, Hi: 42842, Stride: 41}, + {Lo: 42858, Hi: 42862, Stride: 4}, + {Lo: 42872, Hi: 42889, Stride: 17}, + {Lo: 42892, Hi: 42904, Stride: 12}, + {Lo: 42905, Hi: 42911, Stride: 6}, + {Lo: 42923, Hi: 42930, Stride: 7}, + {Lo: 42931, Hi: 42932, Stride: 1}, + {Lo: 43826, Hi: 43829, Stride: 3}, + {Lo: 43837, Hi: 43847, Stride: 10}, + {Lo: 43848, Hi: 43854, Stride: 6}, + {Lo: 43858, Hi: 43866, Stride: 8}, + {Lo: 43893, Hi: 43905, Stride: 12}, + {Lo: 43907, Hi: 43923, Stride: 16}, + {Lo: 43945, Hi: 43946, Stride: 1}, + {Lo: 43951, Hi: 64422, Stride: 20471}, + {Lo: 64423, Hi: 64429, Stride: 1}, + {Lo: 64830, Hi: 64831, Stride: 1}, + {Lo: 65072, Hi: 65101, Stride: 29}, + {Lo: 65102, Hi: 65103, Stride: 1}, + {Lo: 65112, Hi: 65128, Stride: 16}, + {Lo: 65165, Hi: 65166, Stride: 1}, + {Lo: 65257, Hi: 65260, Stride: 1}, + {Lo: 65282, Hi: 65284, Stride: 2}, + {Lo: 65285, Hi: 65287, Stride: 1}, + {Lo: 65290, Hi: 65291, Stride: 1}, + {Lo: 65293, Hi: 65305, Stride: 1}, + {Lo: 65308, Hi: 65310, Stride: 1}, + {Lo: 65312, Hi: 65373, Stride: 1}, + {Lo: 65512, Hi: 65512, Stride: 1}, + }, + R32: []unicode.Range32{ + {Lo: 66178, Hi: 66182, Stride: 4}, + {Lo: 66183, Hi: 66186, Stride: 3}, + {Lo: 66192, Hi: 66194, Stride: 2}, + {Lo: 66197, Hi: 66199, Stride: 1}, + {Lo: 66203, Hi: 66208, Stride: 5}, + {Lo: 66209, Hi: 66210, Stride: 1}, + {Lo: 66213, Hi: 66219, Stride: 6}, + {Lo: 66224, Hi: 66226, Stride: 1}, + {Lo: 66228, Hi: 66255, Stride: 27}, + {Lo: 66293, Hi: 66305, Stride: 12}, + {Lo: 66306, Hi: 66313, Stride: 7}, + {Lo: 66321, Hi: 66325, Stride: 4}, + {Lo: 66327, Hi: 66330, Stride: 3}, + {Lo: 66335, Hi: 66336, Stride: 1}, + {Lo: 66338, Hi: 66564, Stride: 226}, + {Lo: 66581, Hi: 66587, Stride: 6}, + {Lo: 66592, Hi: 66604, Stride: 12}, + {Lo: 66621, Hi: 66632, Stride: 11}, + {Lo: 66740, Hi: 66754, Stride: 14}, + {Lo: 66766, Hi: 66770, Stride: 4}, + {Lo: 66794, Hi: 66806, Stride: 12}, + {Lo: 66835, Hi: 66838, Stride: 3}, + {Lo: 66840, Hi: 66844, Stride: 4}, + {Lo: 66845, Hi: 66853, Stride: 8}, + {Lo: 66854, Hi: 66855, Stride: 1}, + {Lo: 68176, Hi: 70864, Stride: 2688}, + {Lo: 71430, Hi: 71438, Stride: 4}, + {Lo: 71439, Hi: 71840, Stride: 401}, + {Lo: 71842, Hi: 71844, Stride: 1}, + {Lo: 71846, Hi: 71852, Stride: 3}, + {Lo: 71854, Hi: 71855, Stride: 1}, + {Lo: 71858, Hi: 71867, Stride: 3}, + {Lo: 71868, Hi: 71872, Stride: 4}, + {Lo: 71873, Hi: 71876, Stride: 1}, + {Lo: 71878, Hi: 71884, Stride: 2}, + {Lo: 71893, Hi: 71896, Stride: 1}, + {Lo: 71900, Hi: 71904, Stride: 4}, + {Lo: 71909, Hi: 71910, Stride: 1}, + {Lo: 71913, Hi: 71922, Stride: 3}, + {Lo: 93960, Hi: 93962, Stride: 2}, + {Lo: 93974, Hi: 93992, Stride: 18}, + {Lo: 94005, Hi: 94010, Stride: 5}, + {Lo: 94011, Hi: 94015, Stride: 4}, + {Lo: 94016, Hi: 94018, Stride: 2}, + {Lo: 94019, Hi: 94033, Stride: 14}, + {Lo: 94034, Hi: 119060, Stride: 25026}, + {Lo: 119149, Hi: 119302, Stride: 153}, + {Lo: 119309, Hi: 119311, Stride: 2}, + {Lo: 119314, Hi: 119315, Stride: 1}, + {Lo: 119318, Hi: 119338, Stride: 20}, + {Lo: 119350, Hi: 119351, Stride: 1}, + {Lo: 119354, Hi: 119355, Stride: 1}, + {Lo: 119808, Hi: 119845, Stride: 1}, + {Lo: 119847, Hi: 119892, Stride: 1}, + {Lo: 119894, Hi: 119897, Stride: 1}, + {Lo: 119899, Hi: 119949, Stride: 1}, + {Lo: 119951, Hi: 119964, Stride: 1}, + {Lo: 119966, Hi: 119967, Stride: 1}, + {Lo: 119970, Hi: 119973, Stride: 3}, + {Lo: 119974, Hi: 119977, Stride: 3}, + {Lo: 119978, Hi: 119980, Stride: 1}, + {Lo: 119982, Hi: 119993, Stride: 1}, + {Lo: 119995, Hi: 119997, Stride: 2}, + {Lo: 119998, Hi: 120001, Stride: 1}, + {Lo: 120003, Hi: 120005, Stride: 2}, + {Lo: 120006, Hi: 120053, Stride: 1}, + {Lo: 120055, Hi: 120069, Stride: 1}, + {Lo: 120071, Hi: 120074, Stride: 1}, + {Lo: 120077, Hi: 120084, Stride: 1}, + {Lo: 120086, Hi: 120092, Stride: 1}, + {Lo: 120094, Hi: 120105, Stride: 1}, + {Lo: 120107, Hi: 120121, Stride: 1}, + {Lo: 120123, Hi: 120126, Stride: 1}, + {Lo: 120128, Hi: 120132, Stride: 1}, + {Lo: 120134, Hi: 120138, Stride: 4}, + {Lo: 120139, Hi: 120144, Stride: 1}, + {Lo: 120146, Hi: 120157, Stride: 1}, + {Lo: 120159, Hi: 120209, Stride: 1}, + {Lo: 120211, Hi: 120261, Stride: 1}, + {Lo: 120263, Hi: 120313, Stride: 1}, + {Lo: 120315, Hi: 120365, Stride: 1}, + {Lo: 120367, Hi: 120417, Stride: 1}, + {Lo: 120419, Hi: 120469, Stride: 1}, + {Lo: 120471, Hi: 120484, Stride: 1}, + {Lo: 120488, Hi: 120489, Stride: 1}, + {Lo: 120492, Hi: 120494, Stride: 1}, + {Lo: 120496, Hi: 120497, Stride: 1}, + {Lo: 120499, Hi: 120500, Stride: 1}, + {Lo: 120502, Hi: 120504, Stride: 2}, + {Lo: 120507, Hi: 120508, Stride: 1}, + {Lo: 120510, Hi: 120514, Stride: 4}, + {Lo: 120516, Hi: 120522, Stride: 6}, + {Lo: 120526, Hi: 120534, Stride: 2}, + {Lo: 120544, Hi: 120546, Stride: 2}, + {Lo: 120547, Hi: 120550, Stride: 3}, + {Lo: 120551, Hi: 120552, Stride: 1}, + {Lo: 120554, Hi: 120555, Stride: 1}, + {Lo: 120557, Hi: 120558, Stride: 1}, + {Lo: 120560, Hi: 120562, Stride: 2}, + {Lo: 120565, Hi: 120566, Stride: 1}, + {Lo: 120568, Hi: 120572, Stride: 4}, + {Lo: 120574, Hi: 120580, Stride: 6}, + {Lo: 120584, Hi: 120592, Stride: 2}, + {Lo: 120602, Hi: 120604, Stride: 2}, + {Lo: 120605, Hi: 120608, Stride: 3}, + {Lo: 120609, Hi: 120610, Stride: 1}, + {Lo: 120612, Hi: 120613, Stride: 1}, + {Lo: 120615, Hi: 120616, Stride: 1}, + {Lo: 120618, Hi: 120620, Stride: 2}, + {Lo: 120623, Hi: 120624, Stride: 1}, + {Lo: 120626, Hi: 120630, Stride: 4}, + {Lo: 120632, Hi: 120638, Stride: 6}, + {Lo: 120642, Hi: 120650, Stride: 2}, + {Lo: 120660, Hi: 120662, Stride: 2}, + {Lo: 120663, Hi: 120666, Stride: 3}, + {Lo: 120667, Hi: 120668, Stride: 1}, + {Lo: 120670, Hi: 120671, Stride: 1}, + {Lo: 120673, Hi: 120674, Stride: 1}, + {Lo: 120676, Hi: 120678, Stride: 2}, + {Lo: 120681, Hi: 120682, Stride: 1}, + {Lo: 120684, Hi: 120688, Stride: 4}, + {Lo: 120690, Hi: 120696, Stride: 6}, + {Lo: 120700, Hi: 120708, Stride: 2}, + {Lo: 120718, Hi: 120720, Stride: 2}, + {Lo: 120721, Hi: 120724, Stride: 3}, + {Lo: 120725, Hi: 120726, Stride: 1}, + {Lo: 120728, Hi: 120729, Stride: 1}, + {Lo: 120731, Hi: 120732, Stride: 1}, + {Lo: 120734, Hi: 120736, Stride: 2}, + {Lo: 120739, Hi: 120740, Stride: 1}, + {Lo: 120742, Hi: 120746, Stride: 4}, + {Lo: 120748, Hi: 120754, Stride: 6}, + {Lo: 120758, Hi: 120766, Stride: 2}, + {Lo: 120776, Hi: 120778, Stride: 2}, + {Lo: 120782, Hi: 120831, Stride: 1}, + {Lo: 125127, Hi: 125131, Stride: 4}, + {Lo: 126464, Hi: 126500, Stride: 36}, + {Lo: 126564, Hi: 126592, Stride: 28}, + {Lo: 126596, Hi: 128844, Stride: 2248}, + {Lo: 128872, Hi: 130032, Stride: 1160}, + {Lo: 130033, Hi: 130041, Stride: 1}, + }, + LatinOffset: 0, }, - R32: []unicode.Range32{ - {Lo: 66178, Hi: 66182, Stride: 4}, - {Lo: 66183, Hi: 66186, Stride: 3}, - {Lo: 66192, Hi: 66194, Stride: 2}, - {Lo: 66197, Hi: 66199, Stride: 1}, - {Lo: 66203, Hi: 66208, Stride: 5}, - {Lo: 66209, Hi: 66210, Stride: 1}, - {Lo: 66213, Hi: 66219, Stride: 6}, - {Lo: 66224, Hi: 66226, Stride: 1}, - {Lo: 66228, Hi: 66255, Stride: 27}, - {Lo: 66293, Hi: 66305, Stride: 12}, - {Lo: 66306, Hi: 66313, Stride: 7}, - {Lo: 66321, Hi: 66325, Stride: 4}, - {Lo: 66327, Hi: 66330, Stride: 3}, - {Lo: 66335, Hi: 66336, Stride: 1}, - {Lo: 66338, Hi: 66564, Stride: 226}, - {Lo: 66581, Hi: 66587, Stride: 6}, - {Lo: 66592, Hi: 66604, Stride: 12}, - {Lo: 66621, Hi: 66632, Stride: 11}, - {Lo: 66740, Hi: 66754, Stride: 14}, - {Lo: 66766, Hi: 66770, Stride: 4}, - {Lo: 66794, Hi: 66806, Stride: 12}, - {Lo: 66835, Hi: 66838, Stride: 3}, - {Lo: 66840, Hi: 66844, Stride: 4}, - {Lo: 66845, Hi: 66853, Stride: 8}, - {Lo: 66854, Hi: 66855, Stride: 1}, - {Lo: 68176, Hi: 70864, Stride: 2688}, - {Lo: 71430, Hi: 71438, Stride: 4}, - {Lo: 71439, Hi: 71840, Stride: 401}, - {Lo: 71842, Hi: 71844, Stride: 1}, - {Lo: 71846, Hi: 71852, Stride: 3}, - {Lo: 71854, Hi: 71855, Stride: 1}, - {Lo: 71858, Hi: 71867, Stride: 3}, - {Lo: 71868, Hi: 71872, Stride: 4}, - {Lo: 71873, Hi: 71876, Stride: 1}, - {Lo: 71878, Hi: 71884, Stride: 2}, - {Lo: 71893, Hi: 71896, Stride: 1}, - {Lo: 71900, Hi: 71904, Stride: 4}, - {Lo: 71909, Hi: 71910, Stride: 1}, - {Lo: 71913, Hi: 71922, Stride: 3}, - {Lo: 93960, Hi: 93962, Stride: 2}, - {Lo: 93974, Hi: 93992, Stride: 18}, - {Lo: 94005, Hi: 94010, Stride: 5}, - {Lo: 94011, Hi: 94015, Stride: 4}, - {Lo: 94016, Hi: 94018, Stride: 2}, - {Lo: 94019, Hi: 94033, Stride: 14}, - {Lo: 94034, Hi: 119060, Stride: 25026}, - {Lo: 119149, Hi: 119302, Stride: 153}, - {Lo: 119309, Hi: 119311, Stride: 2}, - {Lo: 119314, Hi: 119315, Stride: 1}, - {Lo: 119318, Hi: 119338, Stride: 20}, - {Lo: 119350, Hi: 119351, Stride: 1}, - {Lo: 119354, Hi: 119355, Stride: 1}, - {Lo: 119808, Hi: 119845, Stride: 1}, - {Lo: 119847, Hi: 119892, Stride: 1}, - {Lo: 119894, Hi: 119897, Stride: 1}, - {Lo: 119899, Hi: 119949, Stride: 1}, - {Lo: 119951, Hi: 119964, Stride: 1}, - {Lo: 119966, Hi: 119967, Stride: 1}, - {Lo: 119970, Hi: 119973, Stride: 3}, - {Lo: 119974, Hi: 119977, Stride: 3}, - {Lo: 119978, Hi: 119980, Stride: 1}, - {Lo: 119982, Hi: 119993, Stride: 1}, - {Lo: 119995, Hi: 119997, Stride: 2}, - {Lo: 119998, Hi: 120001, Stride: 1}, - {Lo: 120003, Hi: 120005, Stride: 2}, - {Lo: 120006, Hi: 120053, Stride: 1}, - {Lo: 120055, Hi: 120069, Stride: 1}, - {Lo: 120071, Hi: 120074, Stride: 1}, - {Lo: 120077, Hi: 120084, Stride: 1}, - {Lo: 120086, Hi: 120092, Stride: 1}, - {Lo: 120094, Hi: 120105, Stride: 1}, - {Lo: 120107, Hi: 120121, Stride: 1}, - {Lo: 120123, Hi: 120126, Stride: 1}, - {Lo: 120128, Hi: 120132, Stride: 1}, - {Lo: 120134, Hi: 120138, Stride: 4}, - {Lo: 120139, Hi: 120144, Stride: 1}, - {Lo: 120146, Hi: 120157, Stride: 1}, - {Lo: 120159, Hi: 120209, Stride: 1}, - {Lo: 120211, Hi: 120261, Stride: 1}, - {Lo: 120263, Hi: 120313, Stride: 1}, - {Lo: 120315, Hi: 120365, Stride: 1}, - {Lo: 120367, Hi: 120417, Stride: 1}, - {Lo: 120419, Hi: 120469, Stride: 1}, - {Lo: 120471, Hi: 120484, Stride: 1}, - {Lo: 120488, Hi: 120489, Stride: 1}, - {Lo: 120492, Hi: 120494, Stride: 1}, - {Lo: 120496, Hi: 120497, Stride: 1}, - {Lo: 120499, Hi: 120500, Stride: 1}, - {Lo: 120502, Hi: 120504, Stride: 2}, - {Lo: 120507, Hi: 120508, Stride: 1}, - {Lo: 120510, Hi: 120514, Stride: 4}, - {Lo: 120516, Hi: 120522, Stride: 6}, - {Lo: 120526, Hi: 120534, Stride: 2}, - {Lo: 120544, Hi: 120546, Stride: 2}, - {Lo: 120547, Hi: 120550, Stride: 3}, - {Lo: 120551, Hi: 120552, Stride: 1}, - {Lo: 120554, Hi: 120555, Stride: 1}, - {Lo: 120557, Hi: 120558, Stride: 1}, - {Lo: 120560, Hi: 120562, Stride: 2}, - {Lo: 120565, Hi: 120566, Stride: 1}, - {Lo: 120568, Hi: 120572, Stride: 4}, - {Lo: 120574, Hi: 120580, Stride: 6}, - {Lo: 120584, Hi: 120592, Stride: 2}, - {Lo: 120602, Hi: 120604, Stride: 2}, - {Lo: 120605, Hi: 120608, Stride: 3}, - {Lo: 120609, Hi: 120610, Stride: 1}, - {Lo: 120612, Hi: 120613, Stride: 1}, - {Lo: 120615, Hi: 120616, Stride: 1}, - {Lo: 120618, Hi: 120620, Stride: 2}, - {Lo: 120623, Hi: 120624, Stride: 1}, - {Lo: 120626, Hi: 120630, Stride: 4}, - {Lo: 120632, Hi: 120638, Stride: 6}, - {Lo: 120642, Hi: 120650, Stride: 2}, - {Lo: 120660, Hi: 120662, Stride: 2}, - {Lo: 120663, Hi: 120666, Stride: 3}, - {Lo: 120667, Hi: 120668, Stride: 1}, - {Lo: 120670, Hi: 120671, Stride: 1}, - {Lo: 120673, Hi: 120674, Stride: 1}, - {Lo: 120676, Hi: 120678, Stride: 2}, - {Lo: 120681, Hi: 120682, Stride: 1}, - {Lo: 120684, Hi: 120688, Stride: 4}, - {Lo: 120690, Hi: 120696, Stride: 6}, - {Lo: 120700, Hi: 120708, Stride: 2}, - {Lo: 120718, Hi: 120720, Stride: 2}, - {Lo: 120721, Hi: 120724, Stride: 3}, - {Lo: 120725, Hi: 120726, Stride: 1}, - {Lo: 120728, Hi: 120729, Stride: 1}, - {Lo: 120731, Hi: 120732, Stride: 1}, - {Lo: 120734, Hi: 120736, Stride: 2}, - {Lo: 120739, Hi: 120740, Stride: 1}, - {Lo: 120742, Hi: 120746, Stride: 4}, - {Lo: 120748, Hi: 120754, Stride: 6}, - {Lo: 120758, Hi: 120766, Stride: 2}, - {Lo: 120776, Hi: 120778, Stride: 2}, - {Lo: 120782, Hi: 120831, Stride: 1}, - {Lo: 125127, Hi: 125131, Stride: 4}, - {Lo: 126464, Hi: 126500, Stride: 36}, - {Lo: 126564, Hi: 126592, Stride: 28}, - {Lo: 126596, Hi: 128844, Stride: 2248}, - {Lo: 128872, Hi: 130032, Stride: 1160}, - {Lo: 130033, Hi: 130041, Stride: 1}, - }, - LatinOffset: 0, }, - }, - "_default": { - Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "_default", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 160, Hi: 180, Stride: 20}, - {Lo: 215, Hi: 305, Stride: 90}, - {Lo: 921, Hi: 1009, Stride: 88}, - {Lo: 1040, Hi: 1042, Stride: 2}, - {Lo: 1045, Hi: 1047, Stride: 2}, - {Lo: 1050, Hi: 1052, Stride: 2}, - {Lo: 1053, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8216, Stride: 5}, - {Lo: 8217, Hi: 8245, Stride: 28}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "_default": { + Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "_default", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 160, Hi: 180, Stride: 20}, + {Lo: 215, Hi: 305, Stride: 90}, + {Lo: 921, Hi: 1009, Stride: 88}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8216, Stride: 5}, + {Lo: 8217, Hi: 8245, Stride: 28}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "cs": { - Confusable: []rune{180, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "cs", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 305, Stride: 125}, - {Lo: 921, Hi: 1009, Stride: 88}, - {Lo: 1040, Hi: 1042, Stride: 2}, - {Lo: 1045, Hi: 1047, Stride: 2}, - {Lo: 1050, Hi: 1052, Stride: 2}, - {Lo: 1053, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8217, Stride: 1}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65311, Stride: 4}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "cs": { + Confusable: []rune{180, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "cs", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 305, Stride: 125}, + {Lo: 921, Hi: 1009, Stride: 88}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8216, Hi: 8217, Stride: 1}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 65288, Hi: 65289, Stride: 1}, + {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 0, }, - R32: []unicode.Range32{}, - LatinOffset: 0, }, - }, - "de": { - Confusable: []rune{180, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "de", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 305, Stride: 125}, - {Lo: 921, Hi: 1009, Stride: 88}, - {Lo: 1040, Hi: 1042, Stride: 2}, - {Lo: 1045, Hi: 1047, Stride: 2}, - {Lo: 1050, Hi: 1052, Stride: 2}, - {Lo: 1053, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8217, Stride: 1}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65311, Stride: 4}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "de": { + Confusable: []rune{180, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "de", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 305, Stride: 125}, + {Lo: 921, Hi: 1009, Stride: 88}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8216, Hi: 8217, Stride: 1}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 65288, Hi: 65289, Stride: 1}, + {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 0, }, - R32: []unicode.Range32{}, - LatinOffset: 0, }, - }, - "es": { - Confusable: []rune{180, 215, 305, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "es", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 1009, Stride: 704}, - {Lo: 1040, Hi: 1042, Stride: 2}, - {Lo: 1045, Hi: 1047, Stride: 2}, - {Lo: 1050, Hi: 1052, Stride: 2}, - {Lo: 1053, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8245, Stride: 34}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "es": { + Confusable: []rune{180, 215, 305, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "es", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 1009, Stride: 704}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8245, Stride: 34}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "fr": { - Confusable: []rune{215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "fr", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 215, Hi: 305, Stride: 90}, - {Lo: 921, Hi: 1009, Stride: 88}, - {Lo: 1040, Hi: 1042, Stride: 2}, - {Lo: 1045, Hi: 1047, Stride: 2}, - {Lo: 1050, Hi: 1052, Stride: 2}, - {Lo: 1053, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8245, Stride: 29}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "fr": { + Confusable: []rune{215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "fr", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 215, Hi: 305, Stride: 90}, + {Lo: 921, Hi: 1009, Stride: 88}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8216, Hi: 8245, Stride: 29}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 0, }, - R32: []unicode.Range32{}, - LatinOffset: 0, }, - }, - "it": { - Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "it", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 160, Hi: 180, Stride: 20}, - {Lo: 215, Hi: 305, Stride: 90}, - {Lo: 921, Hi: 1009, Stride: 88}, - {Lo: 1040, Hi: 1042, Stride: 2}, - {Lo: 1045, Hi: 1047, Stride: 2}, - {Lo: 1050, Hi: 1052, Stride: 2}, - {Lo: 1053, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8216, Stride: 5}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65311, Stride: 4}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "it": { + Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "it", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 160, Hi: 180, Stride: 20}, + {Lo: 215, Hi: 305, Stride: 90}, + {Lo: 921, Hi: 1009, Stride: 88}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8216, Stride: 5}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 65288, Hi: 65289, Stride: 1}, + {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "ja": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 65281, 65283, 65292, 65306, 65307}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 33, 35, 44, 58, 59}, - Locale: "ja", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8216, Stride: 5}, - {Lo: 8217, Hi: 8245, Stride: 28}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65307, Stride: 1}, + + "ja": { + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 65281, 65283, 65292, 65306, 65307}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 33, 35, 44, 58, 59}, + Locale: "ja", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8216, Stride: 5}, + {Lo: 8217, Hi: 8245, Stride: 28}, + {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65307, Hi: 65307, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "ko": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "ko", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8245, Stride: 34}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "ko": { + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "ko", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8245, Stride: 34}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "pl": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "pl", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8217, Stride: 1}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65311, Stride: 4}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "pl": { + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "pl", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8216, Hi: 8217, Stride: 1}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 65288, Hi: 65289, Stride: 1}, + {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "pt-BR": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "pt-BR", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8217, Stride: 1}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65311, Stride: 4}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "pt-BR": { + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "pt-BR", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8216, Hi: 8217, Stride: 1}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 65288, Hi: 65289, Stride: 1}, + {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "qps-ploc": { - Confusable: []rune{160, 180, 215, 305, 921, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{32, 96, 120, 105, 73, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "qps-ploc", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 160, Hi: 180, Stride: 20}, - {Lo: 215, Hi: 305, Stride: 90}, - {Lo: 921, Hi: 1040, Stride: 119}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8216, Stride: 5}, - {Lo: 8217, Hi: 8245, Stride: 28}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "qps-ploc": { + Confusable: []rune{160, 180, 215, 305, 921, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{32, 96, 120, 105, 73, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "qps-ploc", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 160, Hi: 180, Stride: 20}, + {Lo: 215, Hi: 305, Stride: 90}, + {Lo: 921, Hi: 1040, Stride: 119}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8216, Stride: 5}, + {Lo: 8217, Hi: 8245, Stride: 28}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "ru": { - Confusable: []rune{180, 215, 305, 921, 1009, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 73, 112, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "ru", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 8216, Stride: 7207}, - {Lo: 8217, Hi: 8245, Stride: 28}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "ru": { + Confusable: []rune{180, 215, 305, 921, 1009, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 73, 112, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "ru", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 8216, Stride: 7207}, + {Lo: 8217, Hi: 8245, Stride: 28}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "tr": { - Confusable: []rune{160, 180, 215, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{32, 96, 120, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "tr", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 160, Hi: 180, Stride: 20}, - {Lo: 215, Hi: 921, Stride: 706}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8245, Stride: 34}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "tr": { + Confusable: []rune{160, 180, 215, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{32, 96, 120, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "tr", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 160, Hi: 180, Stride: 20}, + {Lo: 215, Hi: 921, Stride: 706}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8245, Stride: 34}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "zh-hans": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8245, 12494, 65281, 65288, 65289, 65306, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 47, 33, 40, 41, 58, 126}, - Locale: "zh-hans", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65288, Stride: 7}, - {Lo: 65289, Hi: 65306, Stride: 17}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "zh-hans": { + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8245, 12494, 65281, 65288, 65289, 65306, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 47, 33, 40, 41, 58, 126}, + Locale: "zh-hans", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65288, Stride: 7}, + {Lo: 65289, Hi: 65306, Stride: 17}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "zh-hant": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 12494, 65283, 65307, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 47, 35, 59, 126}, - Locale: "zh-hant", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 12494, Stride: 4283}, - {Lo: 65283, Hi: 65307, Stride: 24}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "zh-hant": { + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 12494, 65283, 65307, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 47, 35, 59, 126}, + Locale: "zh-hant", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 12494, Stride: 4283}, + {Lo: 65283, Hi: 65307, Stride: 24}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, + } } diff --git a/modules/charset/ambiguous_gen_test.go b/modules/charset/ambiguous_gen_test.go index d3be0b1a13..81d2e8065b 100644 --- a/modules/charset/ambiguous_gen_test.go +++ b/modules/charset/ambiguous_gen_test.go @@ -8,11 +8,13 @@ import ( "testing" "unicode" + "code.gitea.io/gitea/modules/translation" + "github.com/stretchr/testify/assert" ) func TestAmbiguousCharacters(t *testing.T) { - for locale, ambiguous := range AmbiguousCharacters { + for locale, ambiguous := range globalVars().ambiguousTableMap { assert.Equal(t, locale, ambiguous.Locale) assert.Len(t, ambiguous.With, len(ambiguous.Confusable)) assert.True(t, sort.SliceIsSorted(ambiguous.Confusable, func(i, j int) bool { @@ -28,4 +30,8 @@ func TestAmbiguousCharacters(t *testing.T) { assert.True(t, found, "%c is not in %d", confusable, i) } } + + var confusableTo rune + ret := isAmbiguous('𝐾', &confusableTo, AmbiguousTablesForLocale(&translation.MockLocale{})...) + assert.True(t, ret) } diff --git a/modules/charset/breakwriter.go b/modules/charset/breakwriter.go deleted file mode 100644 index a87e846466..0000000000 --- a/modules/charset/breakwriter.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package charset - -import ( - "bytes" - "io" -) - -// BreakWriter wraps an io.Writer to always write '\n' as '
' -type BreakWriter struct { - io.Writer -} - -// Write writes the provided byte slice transparently replacing '\n' with '
' -func (b *BreakWriter) Write(bs []byte) (n int, err error) { - pos := 0 - for pos < len(bs) { - idx := bytes.IndexByte(bs[pos:], '\n') - if idx < 0 { - wn, err := b.Writer.Write(bs[pos:]) - return n + wn, err - } - - if idx > 0 { - wn, err := b.Writer.Write(bs[pos : pos+idx]) - n += wn - if err != nil { - return n, err - } - } - - if _, err = b.Writer.Write([]byte("
")); err != nil { - return n, err - } - pos += idx + 1 - - n++ - } - - return n, err -} diff --git a/modules/charset/breakwriter_test.go b/modules/charset/breakwriter_test.go deleted file mode 100644 index 5eeeedc4e2..0000000000 --- a/modules/charset/breakwriter_test.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package charset - -import ( - "strings" - "testing" -) - -func TestBreakWriter_Write(t *testing.T) { - tests := []struct { - name string - kase string - expect string - wantErr bool - }{ - { - name: "noline", - kase: "abcdefghijklmnopqrstuvwxyz", - expect: "abcdefghijklmnopqrstuvwxyz", - }, - { - name: "endline", - kase: "abcdefghijklmnopqrstuvwxyz\n", - expect: "abcdefghijklmnopqrstuvwxyz
", - }, - { - name: "startline", - kase: "\nabcdefghijklmnopqrstuvwxyz", - expect: "
abcdefghijklmnopqrstuvwxyz", - }, - { - name: "onlyline", - kase: "\n\n\n", - expect: "


", - }, - { - name: "empty", - kase: "", - expect: "", - }, - { - name: "midline", - kase: "\nabc\ndefghijkl\nmnopqrstuvwxy\nz", - expect: "
abc
defghijkl
mnopqrstuvwxy
z", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - buf := &strings.Builder{} - b := &BreakWriter{ - Writer: buf, - } - n, err := b.Write([]byte(tt.kase)) - if (err != nil) != tt.wantErr { - t.Errorf("BreakWriter.Write() error = %v, wantErr %v", err, tt.wantErr) - return - } - if n != len(tt.kase) { - t.Errorf("BreakWriter.Write() = %v, want %v", n, len(tt.kase)) - } - if buf.String() != tt.expect { - t.Errorf("BreakWriter.Write() wrote %q, want %v", buf.String(), tt.expect) - } - }) - } -} diff --git a/modules/charset/charset.go b/modules/charset/charset.go index b156654973..e5df55c9b6 100644 --- a/modules/charset/charset.go +++ b/modules/charset/charset.go @@ -6,7 +6,10 @@ package charset import ( "bytes" "io" + "regexp" "strings" + "sync" + "unicode" "unicode/utf8" "code.gitea.io/gitea/modules/setting" @@ -17,8 +20,19 @@ import ( "golang.org/x/text/transform" ) -// UTF8BOM is the utf-8 byte-order marker -var UTF8BOM = []byte{'\xef', '\xbb', '\xbf'} +var globalVars = sync.OnceValue(func() (ret struct { + utf8Bom []byte + + defaultWordRegexp *regexp.Regexp + ambiguousTableMap map[string]*AmbiguousTable + invisibleRangeTable *unicode.RangeTable +}, +) { + ret.utf8Bom = []byte{'\xef', '\xbb', '\xbf'} + ret.ambiguousTableMap = newAmbiguousTableMap() + ret.invisibleRangeTable = newInvisibleRangeTable() + return ret +}) type ConvertOpts struct { KeepBOM bool @@ -75,7 +89,10 @@ func ToUTF8(content []byte, opts ConvertOpts) []byte { encoding, _ := charset.Lookup(charsetLabel) if encoding == nil { setting.PanicInDevOrTesting("unsupported detected charset %q, it shouldn't happen", charsetLabel) - return content + if opts.ErrorReturnOrigin { + return content + } + return bytes.ToValidUTF8(content, opts.ErrorReplacement) } var decoded []byte @@ -105,7 +122,7 @@ func maybeRemoveBOM(content []byte, opts ConvertOpts) []byte { if opts.KeepBOM { return content } - return bytes.TrimPrefix(content, UTF8BOM) + return bytes.TrimPrefix(content, globalVars().utf8Bom) } // DetectEncoding detect the encoding of content diff --git a/modules/charset/escape.go b/modules/charset/escape.go index 167683a298..8f25e7876d 100644 --- a/modules/charset/escape.go +++ b/modules/charset/escape.go @@ -1,10 +1,6 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:generate go run invisible/generate.go -v -o ./invisible_gen.go - -//go:generate go run ambiguous/generate.go -v -o ./ambiguous_gen.go ambiguous/ambiguous.json - package charset import ( @@ -12,36 +8,36 @@ import ( "io" "strings" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" ) -// RuneNBSP is the codepoint for NBSP -const RuneNBSP = 0xa0 +type EscapeOptions struct { + Allowed map[rune]bool +} + +func AllowRuneNBSP() map[rune]bool { + return map[rune]bool{0xa0: true} +} + +func EscapeOptionsForView() EscapeOptions { + return EscapeOptions{ + // it's safe to see NBSP in the view, but maybe not in the diff + Allowed: AllowRuneNBSP(), + } +} // EscapeControlHTML escapes the Unicode control sequences in a provided html document -func EscapeControlHTML(html template.HTML, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output template.HTML) { +func EscapeControlHTML(html template.HTML, locale translation.Locale, opts ...EscapeOptions) (escaped *EscapeStatus, output template.HTML) { if !setting.UI.AmbiguousUnicodeDetection { return &EscapeStatus{}, html } sb := &strings.Builder{} - escaped, _ = EscapeControlReader(strings.NewReader(string(html)), sb, locale, allowed...) // err has been handled in EscapeControlReader + escaped, _ = EscapeControlReader(strings.NewReader(string(html)), sb, locale, opts...) // err has been handled in EscapeControlReader return escaped, template.HTML(sb.String()) } // EscapeControlReader escapes the Unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus -func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) { - if !setting.UI.AmbiguousUnicodeDetection { - _, err = io.Copy(writer, reader) - return &EscapeStatus{}, err - } - outputStream := &HTMLStreamerWriter{Writer: writer} - streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer) - - if err = StreamHTML(reader, streamer); err != nil { - streamer.escaped.HasError = true - log.Error("Error whilst escaping: %v", err) - } - return streamer.escaped, err +func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, opts ...EscapeOptions) (*EscapeStatus, error) { + return escapeStream(locale, reader, writer, opts...) } diff --git a/modules/charset/escape_status.go b/modules/charset/escape_status.go index 37b6ad86d4..fb9ebbb228 100644 --- a/modules/charset/escape_status.go +++ b/modules/charset/escape_status.go @@ -3,11 +3,9 @@ package charset -// EscapeStatus represents the findings of the unicode escaper +// EscapeStatus represents the findings of the Unicode escaper type EscapeStatus struct { - Escaped bool - HasError bool - HasBadRunes bool + Escaped bool // it means that some characters were escaped, and they can also be unescaped back HasInvisible bool HasAmbiguous bool } @@ -19,8 +17,6 @@ func (status *EscapeStatus) Or(other *EscapeStatus) *EscapeStatus { st = &EscapeStatus{} } st.Escaped = st.Escaped || other.Escaped - st.HasError = st.HasError || other.HasError - st.HasBadRunes = st.HasBadRunes || other.HasBadRunes st.HasAmbiguous = st.HasAmbiguous || other.HasAmbiguous st.HasInvisible = st.HasInvisible || other.HasInvisible return st diff --git a/modules/charset/escape_stream.go b/modules/charset/escape_stream.go index 22e7f14f39..11d09083fc 100644 --- a/modules/charset/escape_stream.go +++ b/modules/charset/escape_stream.go @@ -4,288 +4,415 @@ package charset import ( + "bytes" "fmt" - "regexp" - "strings" + "html" + "io" "unicode" "unicode/utf8" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" - - "golang.org/x/net/html" ) -// VScode defaultWordRegexp -var defaultWordRegexp = regexp.MustCompile(`(-?\d*\.\d\w*)|([^\` + "`" + `\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s\x00-\x1f]+)`) - -func NewEscapeStreamer(locale translation.Locale, next HTMLStreamer, allowed ...rune) HTMLStreamer { - allowedM := make(map[rune]bool, len(allowed)) - for _, v := range allowed { - allowedM[v] = true - } - return &escapeStreamer{ - escaped: &EscapeStatus{}, - PassthroughHTMLStreamer: *NewPassthroughStreamer(next), - locale: locale, - ambiguousTables: AmbiguousTablesForLocale(locale), - allowed: allowedM, - } +type htmlChunkReader struct { + in io.Reader + readErr error + readBuf []byte + curInTag bool } type escapeStreamer struct { - PassthroughHTMLStreamer + htmlChunkReader + escaped *EscapeStatus locale translation.Locale ambiguousTables []*AmbiguousTable allowed map[rune]bool + + out io.Writer } -func (e *escapeStreamer) EscapeStatus() *EscapeStatus { - return e.escaped +func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts ...EscapeOptions) (*EscapeStatus, error) { + es := &escapeStreamer{ + escaped: &EscapeStatus{}, + locale: locale, + ambiguousTables: AmbiguousTablesForLocale(locale), + htmlChunkReader: htmlChunkReader{ + in: in, + readBuf: make([]byte, 0, 32*1024), + }, + out: out, + } + + if len(opts) > 0 { + es.allowed = opts[0].Allowed + } + + readCount := 0 + lastIsTag := false + for { + parts, partInTag, err := es.readRunes() + readCount++ + if err == io.EOF { + return es.escaped, nil + } else if err != nil { + return nil, err + } + for i, part := range parts { + if partInTag[i] { + lastIsTag = true + if _, err := out.Write(part); err != nil { + return nil, err + } + } else { + // if last part is tag, then this part is content begin + // if the content is the first part of the first read, then it's also content begin + isContentBegin := lastIsTag || (readCount == 1 && i == 0) + lastIsTag = false + if isContentBegin { + if part, err = es.trimAndWriteBom(part); err != nil { + return nil, err + } + } + if err = es.detectAndWriteRunes(part); err != nil { + return nil, err + } + } + } + } } -// Text tells the next streamer there is a text -func (e *escapeStreamer) Text(data string) error { - sb := &strings.Builder{} - var until int - var next int +func (e *escapeStreamer) trimAndWriteBom(part []byte) ([]byte, error) { + remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom) + if ok { + part = remaining + if _, err := e.out.Write(globalVars().utf8Bom); err != nil { + return part, err + } + } + return part, nil +} + +const longSentenceDetectionLimit = 20 + +func (e *escapeStreamer) possibleLongSentence(results []detectResult, pos int) bool { + countBasic := 0 + countNonASCII := 0 + for i := max(pos-longSentenceDetectionLimit, 0); i < min(pos+longSentenceDetectionLimit, len(results)); i++ { + if results[i].runeType == runeTypeBasic && results[i].runeChar != ' ' { + countBasic++ + } + if results[i].runeType == runeTypeNonASCII || results[i].runeType == runeTypeAmbiguous { + countNonASCII++ + } + } + countChar := countBasic + countNonASCII + // many non-ASCII runes around, it seems to be a sentence, + // don't handle the invisible/ambiguous chars in it, otherwise it will be too noisy + return countChar != 0 && countNonASCII*100/countChar >= 50 +} + +func (e *escapeStreamer) analyzeDetectResults(results []detectResult) { + for i := range results { + res := &results[i] + if res.runeType == runeTypeInvisible || res.runeType == runeTypeAmbiguous { + leftIsNonASCII := i > 0 && (results[i-1].runeType == runeTypeNonASCII || results[i-1].runeType == runeTypeAmbiguous) + rightIsNonASCII := i < len(results)-1 && (results[i+1].runeType == runeTypeNonASCII || results[i+1].runeType == runeTypeAmbiguous) + surroundingNonASCII := leftIsNonASCII || rightIsNonASCII + if !surroundingNonASCII { + if len(results) < longSentenceDetectionLimit { + res.needEscape = setting.UI.AmbiguousUnicodeDetection + } else if !e.possibleLongSentence(results, i) { + res.needEscape = setting.UI.AmbiguousUnicodeDetection + } + } + } + } +} + +func (e *escapeStreamer) detectAndWriteRunes(part []byte) error { + results := e.detectRunes(part) + e.analyzeDetectResults(results) + return e.writeDetectResults(part, results) +} + +func (e *htmlChunkReader) readRunes() (parts [][]byte, partInTag []bool, _ error) { + // we have read everything, eof + if e.readErr != nil && len(e.readBuf) == 0 { + return nil, nil, e.readErr + } + + // not eof, and the there is space in the buffer, try to read more data + if e.readErr == nil && len(e.readBuf) <= cap(e.readBuf)*3/4 { + n, err := e.in.Read(e.readBuf[len(e.readBuf):cap(e.readBuf)]) + e.readErr = err + e.readBuf = e.readBuf[:len(e.readBuf)+n] + } + if len(e.readBuf) == 0 { + return nil, nil, e.readErr + } + + // try to exact tag parts and content parts pos := 0 - if len(data) > len(UTF8BOM) && data[:len(UTF8BOM)] == string(UTF8BOM) { - _, _ = sb.WriteString(data[:len(UTF8BOM)]) - pos = len(UTF8BOM) - } - dataBytes := []byte(data) - for pos < len(data) { - nextIdxs := defaultWordRegexp.FindStringIndex(data[pos:]) - if nextIdxs == nil { - until = len(data) - next = until - } else { - until = min(nextIdxs[0]+pos, len(data)) - next = min(nextIdxs[1]+pos, len(data)) - } - - // from pos until we know that the runes are not \r\t\n or even ' ' - n := next - until - runes := make([]rune, 0, n) - positions := make([]int, 0, n+1) - - for pos < until { - r, sz := utf8.DecodeRune(dataBytes[pos:]) - positions = positions[:0] - positions = append(positions, pos, pos+sz) - types, confusables, _ := e.runeTypes(r) - if err := e.handleRunes(dataBytes, []rune{r}, positions, types, confusables, sb); err != nil { - return err - } - pos += sz - } - - for i := pos; i < next; { - r, sz := utf8.DecodeRune(dataBytes[i:]) - runes = append(runes, r) - positions = append(positions, i) - i += sz - } - positions = append(positions, next) - types, confusables, runeCounts := e.runeTypes(runes...) - if runeCounts.needsEscape() { - if err := e.handleRunes(dataBytes, runes, positions, types, confusables, sb); err != nil { - return err + for pos < len(e.readBuf) { + var curPartEnd int + nextInTag := e.curInTag + if e.curInTag { + // if cur part is in tag, try to find the tag close char '>' + idx := bytes.IndexByte(e.readBuf[pos:], '>') + if idx == -1 { + // if no tag close char, then the whole buffer is in tag + curPartEnd = len(e.readBuf) + } else { + // tag part ends, switch to content part + curPartEnd = pos + idx + 1 + nextInTag = !nextInTag } } else { - _, _ = sb.Write(dataBytes[pos:next]) + // if cur part is in content, try to find the tag open char '<' + idx := bytes.IndexByte(e.readBuf[pos:], '<') + if idx == -1 { + // if no tag open char, then the whole buffer is in content + curPartEnd = len(e.readBuf) + } else { + // content part ends, switch to tag part + curPartEnd = pos + idx + nextInTag = !nextInTag + } + } + + curPartLen := curPartEnd - pos + if curPartLen == 0 { + // if cur part is empty, only need to switch the part type + if e.curInTag == nextInTag { + panic("impossible, curPartLen is 0 but the part in tag status is not switched") + } + e.curInTag = nextInTag + continue + } + + // now, curPartLen can't be 0 + curPart := make([]byte, curPartLen) + copy(curPart, e.readBuf[pos:curPartEnd]) + // now we get the curPart bytes, but we can't directly use it, the last rune in it might have been cut + // try to decode the last rune, if it's invalid, then we cut the last byte and try again until we get a valid rune or no byte left + for i := curPartLen - 1; i >= 0; i-- { + last, lastSize := utf8.DecodeRune(curPart[i:]) + if last == utf8.RuneError && lastSize == 1 { + curPartLen-- + } else { + curPartLen += lastSize - 1 + break + } + } + if curPartLen == 0 { + // actually it's impossible that the part doesn't contain any valid rune, + // the only case is that the cap(readBuf) is too small, or the origin contain indeed doesn't contain any valid rune + // * try to leave the last 4 bytes (possible longest utf-8 encoding) to next round + // * at least consume 1 byte to avoid infinite loop + curPartLen = max(len(curPart)-utf8.UTFMax, 1) + } + + // if curPartLen is not the same as curPart, it means we have cut some bytes, + // need to wait for more data if not eof + trailingCorrupted := curPartLen != len(curPart) + + // finally, we get the real part we need + curPart = curPart[:curPartLen] + parts = append(parts, curPart) + partInTag = append(partInTag, e.curInTag) + + pos += curPartLen + e.curInTag = nextInTag + + if trailingCorrupted && e.readErr == nil { + // if the last part is corrupted, and we haven't reach eof, then we need to wait for more data to get the complete part + break } - pos = next } - if sb.Len() > 0 { - if err := e.PassthroughHTMLStreamer.Text(sb.String()); err != nil { + + copy(e.readBuf, e.readBuf[pos:]) + e.readBuf = e.readBuf[:len(e.readBuf)-pos] + return parts, partInTag, nil +} + +func (e *escapeStreamer) writeDetectResults(data []byte, results []detectResult) error { + lastWriteRawIdx := -1 + for idx := range results { + res := &results[idx] + if !res.needEscape { + if lastWriteRawIdx == -1 { + lastWriteRawIdx = idx + } + continue + } + + if lastWriteRawIdx != -1 { + if _, err := e.out.Write(data[results[lastWriteRawIdx].position:res.position]); err != nil { + return err + } + lastWriteRawIdx = -1 + } + switch res.runeType { + case runeTypeBroken: + if err := e.writeBrokenRune(data[res.position : res.position+res.runeSize]); err != nil { + return err + } + case runeTypeAmbiguous: + if err := e.writeAmbiguousRune(res.runeChar, res.confusable); err != nil { + return err + } + case runeTypeInvisible: + if err := e.writeInvisibleRune(res.runeChar); err != nil { + return err + } + case runeTypeControlChar: + if err := e.writeControlRune(res.runeChar); err != nil { + return err + } + default: + panic("unreachable") + } + } + if lastWriteRawIdx != -1 { + lastResult := results[len(results)-1] + if _, err := e.out.Write(data[results[lastWriteRawIdx].position : lastResult.position+lastResult.runeSize]); err != nil { return err } } return nil } -func (e *escapeStreamer) handleRunes(data []byte, runes []rune, positions []int, types []runeType, confusables []rune, sb *strings.Builder) error { - for i, r := range runes { - switch types[i] { - case brokenRuneType: - if sb.Len() > 0 { - if err := e.PassthroughHTMLStreamer.Text(sb.String()); err != nil { - return err - } - sb.Reset() - } - end := positions[i+1] - start := positions[i] - if err := e.brokenRune(data[start:end]); err != nil { - return err - } - case ambiguousRuneType: - if sb.Len() > 0 { - if err := e.PassthroughHTMLStreamer.Text(sb.String()); err != nil { - return err - } - sb.Reset() - } - if err := e.ambiguousRune(r, confusables[0]); err != nil { - return err - } - confusables = confusables[1:] - case invisibleRuneType: - if sb.Len() > 0 { - if err := e.PassthroughHTMLStreamer.Text(sb.String()); err != nil { - return err - } - sb.Reset() - } - if err := e.invisibleRune(r); err != nil { - return err - } - default: - _, _ = sb.WriteRune(r) - } - } - return nil +func (e *escapeStreamer) writeBrokenRune(_ []byte) (err error) { + // Although we'd like to use the original bytes to display (show the real broken content to users), + // however, when this "escape stream" module is applied to the content, the content has already been processed by other modules. + // So the invalid bytes just can't be kept till this step, in most (all) cases, the only thing we see here is utf8.RuneError + _, err = io.WriteString(e.out, ``) + return err } -func (e *escapeStreamer) brokenRune(bs []byte) error { - e.escaped.Escaped = true - e.escaped.HasBadRunes = true - - if err := e.PassthroughHTMLStreamer.StartTag("span", html.Attribute{ - Key: "class", - Val: "broken-code-point", - }); err != nil { +func (e *escapeStreamer) writeEscapedCharHTML(tag1, attr, tag2, content, tag3 string) (err error) { + _, err = io.WriteString(e.out, tag1) + if err != nil { return err } - if err := e.PassthroughHTMLStreamer.Text(fmt.Sprintf("<%X>", bs)); err != nil { + _, err = io.WriteString(e.out, html.EscapeString(attr)) + if err != nil { return err } - - return e.PassthroughHTMLStreamer.EndTag("span") + _, err = io.WriteString(e.out, tag2) + if err != nil { + return err + } + _, err = io.WriteString(e.out, html.EscapeString(content)) + if err != nil { + return err + } + _, err = io.WriteString(e.out, tag3) + return err } -func (e *escapeStreamer) ambiguousRune(r, c rune) error { +func runeToHex(r rune) string { + return fmt.Sprintf("[U+%04X]", r) +} + +func (e *escapeStreamer) writeAmbiguousRune(r, c rune) (err error) { e.escaped.Escaped = true e.escaped.HasAmbiguous = true - - if err := e.PassthroughHTMLStreamer.StartTag("span", html.Attribute{ - Key: "class", - Val: "ambiguous-code-point", - }, html.Attribute{ - Key: "data-tooltip-content", - Val: e.locale.TrString("repo.ambiguous_character", r, c), - }); err != nil { - return err - } - if err := e.PassthroughHTMLStreamer.StartTag("span", html.Attribute{ - Key: "class", - Val: "char", - }); err != nil { - return err - } - if err := e.PassthroughHTMLStreamer.Text(string(r)); err != nil { - return err - } - if err := e.PassthroughHTMLStreamer.EndTag("span"); err != nil { - return err - } - - return e.PassthroughHTMLStreamer.EndTag("span") + return e.writeEscapedCharHTML( + ``, + string(r), + ``, + ) } -func (e *escapeStreamer) invisibleRune(r rune) error { +func (e *escapeStreamer) writeInvisibleRune(r rune) error { e.escaped.Escaped = true e.escaped.HasInvisible = true - - if err := e.PassthroughHTMLStreamer.StartTag("span", html.Attribute{ - Key: "class", - Val: "escaped-code-point", - }, html.Attribute{ - Key: "data-escaped", - Val: fmt.Sprintf("[U+%04X]", r), - }); err != nil { - return err - } - if err := e.PassthroughHTMLStreamer.StartTag("span", html.Attribute{ - Key: "class", - Val: "char", - }); err != nil { - return err - } - if err := e.PassthroughHTMLStreamer.Text(string(r)); err != nil { - return err - } - if err := e.PassthroughHTMLStreamer.EndTag("span"); err != nil { - return err - } - - return e.PassthroughHTMLStreamer.EndTag("span") + return e.writeEscapedCharHTML( + ``, + string(r), + ``, + ) } -type runeCountType struct { - numBasicRunes int - numNonConfusingNonBasicRunes int - numAmbiguousRunes int - numInvisibleRunes int - numBrokenRunes int +func (e *escapeStreamer) writeControlRune(r rune) error { + var display string + if r >= 0 && r <= 0x1f { + display = string(0x2400 + r) + } else if r == 0x7f { + display = string(rune(0x2421)) + } else { + display = runeToHex(r) + } + return e.writeEscapedCharHTML( + ``, + string(r), + ``, + ) } -func (counts runeCountType) needsEscape() bool { - if counts.numBrokenRunes > 0 { - return true - } - if counts.numBasicRunes == 0 && - counts.numNonConfusingNonBasicRunes > 0 { - return false - } - return counts.numAmbiguousRunes > 0 || counts.numInvisibleRunes > 0 +type detectResult struct { + runeChar rune + runeType int + runeSize int + position int + confusable rune + needEscape bool } -type runeType int - const ( - basicASCIIRuneType runeType = iota // <- This is technically deadcode but its self-documenting so it should stay - brokenRuneType - nonBasicASCIIRuneType - ambiguousRuneType - invisibleRuneType + runeTypeBasic int = iota + runeTypeBroken + runeTypeNonASCII + runeTypeAmbiguous + runeTypeInvisible + runeTypeControlChar ) -func (e *escapeStreamer) runeTypes(runes ...rune) (types []runeType, confusables []rune, runeCounts runeCountType) { - types = make([]runeType, len(runes)) - for i, r := range runes { - var confusable rune +func (e *escapeStreamer) detectRunes(data []byte) []detectResult { + runeCount := utf8.RuneCount(data) + results := make([]detectResult, runeCount) + invisibleRangeTable := globalVars().invisibleRangeTable + var i int + var confusable rune + for pos := 0; pos < len(data); i++ { + r, runeSize := utf8.DecodeRune(data[pos:]) + results[i].runeChar = r + results[i].runeSize = runeSize + results[i].position = pos + pos += runeSize + switch { case r == utf8.RuneError: - types[i] = brokenRuneType - runeCounts.numBrokenRunes++ - case r == ' ' || r == '\t' || r == '\n': - runeCounts.numBasicRunes++ - case e.allowed[r]: - if r > 0x7e || r < 0x20 { - types[i] = nonBasicASCIIRuneType - runeCounts.numNonConfusingNonBasicRunes++ - } else { - runeCounts.numBasicRunes++ + results[i].runeType = runeTypeBroken + results[i].needEscape = true + case r == ' ' || r == '\t' || r == '\n' || e.allowed[r]: + results[i].runeType = runeTypeBasic + if r >= 0x80 { + results[i].runeType = runeTypeNonASCII } - case unicode.Is(InvisibleRanges, r): - types[i] = invisibleRuneType - runeCounts.numInvisibleRunes++ - case unicode.IsControl(r): - types[i] = invisibleRuneType - runeCounts.numInvisibleRunes++ + case r < 0x20 || r == 0x7f: + results[i].runeType = runeTypeControlChar + results[i].needEscape = true + case unicode.Is(invisibleRangeTable, r): + results[i].runeType = runeTypeInvisible + // not sure about results[i].needEscape, will be detected separately case isAmbiguous(r, &confusable, e.ambiguousTables...): - confusables = append(confusables, confusable) - types[i] = ambiguousRuneType - runeCounts.numAmbiguousRunes++ - case r > 0x7e || r < 0x20: - types[i] = nonBasicASCIIRuneType - runeCounts.numNonConfusingNonBasicRunes++ - default: - runeCounts.numBasicRunes++ + results[i].runeType = runeTypeAmbiguous + results[i].confusable = confusable + // not sure about results[i].needEscape, will be detected separately + case r >= 0x80: + results[i].runeType = runeTypeNonASCII + default: // details to basic runes } } - return types, confusables, runeCounts + return results } diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go index 9d796a0c18..4e1ff0fcf4 100644 --- a/modules/charset/escape_test.go +++ b/modules/charset/escape_test.go @@ -4,7 +4,6 @@ package charset import ( - "regexp" "strings" "testing" @@ -13,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/translation" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type escapeControlTest struct { @@ -57,24 +57,24 @@ var escapeControlTests = []escapeControlTest{ status: EscapeStatus{}, }, { - name: "hebrew", + name: "hebrew", // old test was wrong, such text shouldn't be escaped text: "עד תקופת יוון העתיקה היה העיסוק במתמטיקה תכליתי בלבד: היא שימשה כאוסף של נוסחאות לחישוב קרקע, אוכלוסין וכו'. פריצת הדרך של היוונים, פרט לתרומותיהם הגדולות לידע המתמטי, הייתה בלימוד המתמטיקה כשלעצמה, מתוקף ערכה הרוחני. יחסם של חלק מהיוונים הקדמונים למתמטיקה היה דתי - למשל, הכת שאסף סביבו פיתגורס האמינה כי המתמטיקה היא הבסיס לכל הדברים. היוונים נחשבים ליוצרי מושג ההוכחה המתמטית, וכן לראשונים שעסקו במתמטיקה לשם עצמה, כלומר כתחום מחקרי עיוני ומופשט ולא רק כעזר שימושי. עם זאת, לצדה", - result: `עד תקופת יוון העתיקה היה העיסוק במתמטיקה תכליתי בלבד: היא שימשה כאוסף של נוסחאות לחישוב קרקע, אוכלוסין וכו'. פריצת הדרך של היוונים, פרט לתרומותיהם הגדולות לידע המתמטי, הייתה בלימוד המתמטיקה כשלעצמה, מתוקף ערכה הרוחני. יחסם של חלק מהיוונים הקדמונים למתמטיקה היה דתי - למשל, הכת שאסף סביבו פיתגורס האמינה כי המתמטיקה היא הבסיס לכל הדברים. היוונים נחשבים ליוצרי מושג ההוכחה המתמטית, וכן לראשונים שעסקו במתמטיקה לשם עצמה, כלומר כתחום מחקרי עיוני ומופשט ולא רק כעזר שימושי. עם זאת, לצדה`, - status: EscapeStatus{Escaped: true, HasAmbiguous: true}, + result: "עד תקופת יוון העתיקה היה העיסוק במתמטיקה תכליתי בלבד: היא שימשה כאוסף של נוסחאות לחישוב קרקע, אוכלוסין וכו'. פריצת הדרך של היוונים, פרט לתרומותיהם הגדולות לידע המתמטי, הייתה בלימוד המתמטיקה כשלעצמה, מתוקף ערכה הרוחני. יחסם של חלק מהיוונים הקדמונים למתמטיקה היה דתי - למשל, הכת שאסף סביבו פיתגורס האמינה כי המתמטיקה היא הבסיס לכל הדברים. היוונים נחשבים ליוצרי מושג ההוכחה המתמטית, וכן לראשונים שעסקו במתמטיקה לשם עצמה, כלומר כתחום מחקרי עיוני ומופשט ולא רק כעזר שימושי. עם זאת, לצדה", + status: EscapeStatus{}, }, { - name: "more hebrew", + name: "more hebrew", // old test was wrong, such text shouldn't be escaped text: `בתקופה מאוחרת יותר, השתמשו היוונים בשיטת סימון מתקדמת יותר, שבה הוצגו המספרים לפי 22 אותיות האלפבית היווני. לסימון המספרים בין 1 ל-9 נקבעו תשע האותיות הראשונות, בתוספת גרש ( ' ) בצד ימין של האות, למעלה; תשע האותיות הבאות ייצגו את העשרות מ-10 עד 90, והבאות את המאות. לסימון הספרות בין 1000 ל-900,000, השתמשו היוונים באותן אותיות, אך הוסיפו לאותיות את הגרש דווקא מצד שמאל של האותיות, למטה. ממיליון ומעלה, כנראה השתמשו היוונים בשני תגים במקום אחד. המתמטיקאי הבולט הראשון ביוון העתיקה, ויש האומרים בתולדות האנושות, הוא תאלס (624 לפנה"ס - 546 לפנה"ס בקירוב).[1] לא יהיה זה משולל יסוד להניח שהוא האדם הראשון שהוכיח משפט מתמטי, ולא רק גילה אותו. תאלס הוכיח שישרים מקבילים חותכים מצד אחד של שוקי זווית קטעים בעלי יחסים שווים (משפט תאלס הראשון), שהזווית המונחת על קוטר במעגל היא זווית ישרה (משפט תאלס השני), שהקוטר מחלק את המעגל לשני חלקים שווים, ושזוויות הבסיס במשולש שווה-שוקיים שוות זו לזו. מיוחסות לו גם שיטות למדידת גובהן של הפירמידות בעזרת מדידת צילן ולקביעת מיקומה של ספינה הנראית מן החוף. בשנים 582 לפנה"ס עד 496 לפנה"ס, בקירוב, חי מתמטיקאי חשוב במיוחד - פיתגורס. המקורות הראשוניים עליו מועטים, וההיסטוריונים מתקשים להפריד את העובדות משכבת המסתורין והאגדות שנקשרו בו. ידוע שסביבו התקבצה האסכולה הפיתגוראית מעין כת פסבדו-מתמטית שהאמינה ש"הכל מספר", או ליתר דיוק הכל ניתן לכימות, וייחסה למספרים משמעויות מיסטיות. ככל הנראה הפיתגוראים ידעו לבנות את הגופים האפלטוניים, הכירו את הממוצע האריתמטי, הממוצע הגאומטרי והממוצע ההרמוני והגיעו להישגים חשובים נוספים. ניתן לומר שהפיתגוראים גילו את היותו של השורש הריבועי של 2, שהוא גם האלכסון בריבוע שאורך צלעותיו 1, אי רציונלי, אך תגליתם הייתה למעשה רק שהקטעים "חסרי מידה משותפת", ומושג המספר האי רציונלי מאוחר יותר.[2] אזכור ראשון לקיומם של קטעים חסרי מידה משותפת מופיע בדיאלוג "תאיטיטוס" של אפלטון, אך רעיון זה היה מוכר עוד קודם לכן, במאה החמישית לפנה"ס להיפאסוס, בן האסכולה הפיתגוראית, ואולי לפיתגורס עצמו.[3]`, - result: `בתקופה מאוחרת יותר, השתמשו היוונים בשיטת סימון מתקדמת יותר, שבה הוצגו המספרים לפי 22 אותיות האלפבית היווני. לסימון המספרים בין 1 ל-9 נקבעו תשע האותיות הראשונות, בתוספת גרש ( ' ) בצד ימין של האות, למעלה; תשע האותיות הבאות ייצגו את העשרות מ-10 עד 90, והבאות את המאות. לסימון הספרות בין 1000 ל-900,000, השתמשו היוונים באותן אותיות, אך הוסיפו לאותיות את הגרש דווקא מצד שמאל של האותיות, למטה. ממיליון ומעלה, כנראה השתמשו היוונים בשני תגים במקום אחד. + result: `בתקופה מאוחרת יותר, השתמשו היוונים בשיטת סימון מתקדמת יותר, שבה הוצגו המספרים לפי 22 אותיות האלפבית היווני. לסימון המספרים בין 1 ל-9 נקבעו תשע האותיות הראשונות, בתוספת גרש ( ' ) בצד ימין של האות, למעלה; תשע האותיות הבאות ייצגו את העשרות מ-10 עד 90, והבאות את המאות. לסימון הספרות בין 1000 ל-900,000, השתמשו היוונים באותן אותיות, אך הוסיפו לאותיות את הגרש דווקא מצד שמאל של האותיות, למטה. ממיליון ומעלה, כנראה השתמשו היוונים בשני תגים במקום אחד. - המתמטיקאי הבולט הראשון ביוון העתיקה, ויש האומרים בתולדות האנושות, הוא תאלס (624 לפנה"ס - 546 לפנה"ס בקירוב).[1] לא יהיה זה משולל יסוד להניח שהוא האדם הראשון שהוכיח משפט מתמטי, ולא רק גילה אותו. תאלס הוכיח שישרים מקבילים חותכים מצד אחד של שוקי זווית קטעים בעלי יחסים שווים (משפט תאלס הראשון), שהזווית המונחת על קוטר במעגל היא זווית ישרה (משפט תאלס השני), שהקוטר מחלק את המעגל לשני חלקים שווים, ושזוויות הבסיס במשולש שווה-שוקיים שוות זו לזו. מיוחסות לו גם שיטות למדידת גובהן של הפירמידות בעזרת מדידת צילן ולקביעת מיקומה של ספינה הנראית מן החוף. + המתמטיקאי הבולט הראשון ביוון העתיקה, ויש האומרים בתולדות האנושות, הוא תאלס (624 לפנה"ס - 546 לפנה"ס בקירוב).[1] לא יהיה זה משולל יסוד להניח שהוא האדם הראשון שהוכיח משפט מתמטי, ולא רק גילה אותו. תאלס הוכיח שישרים מקבילים חותכים מצד אחד של שוקי זווית קטעים בעלי יחסים שווים (משפט תאלס הראשון), שהזווית המונחת על קוטר במעגל היא זווית ישרה (משפט תאלס השני), שהקוטר מחלק את המעגל לשני חלקים שווים, ושזוויות הבסיס במשולש שווה-שוקיים שוות זו לזו. מיוחסות לו גם שיטות למדידת גובהן של הפירמידות בעזרת מדידת צילן ולקביעת מיקומה של ספינה הנראית מן החוף. - בשנים 582 לפנה"ס עד 496 לפנה"ס, בקירוב, חי מתמטיקאי חשוב במיוחד - פיתגורס. המקורות הראשוניים עליו מועטים, וההיסטוריונים מתקשים להפריד את העובדות משכבת המסתורין והאגדות שנקשרו בו. ידוע שסביבו התקבצה האסכולה הפיתגוראית מעין כת פסבדו-מתמטית שהאמינה ש"הכל מספר", או ליתר דיוק הכל ניתן לכימות, וייחסה למספרים משמעויות מיסטיות. ככל הנראה הפיתגוראים ידעו לבנות את הגופים האפלטוניים, הכירו את הממוצע האריתמטי, הממוצע הגאומטרי והממוצע ההרמוני והגיעו להישגים חשובים נוספים. ניתן לומר שהפיתגוראים גילו את היותו של השורש הריבועי של 2, שהוא גם האלכסון בריבוע שאורך צלעותיו 1, אי רציונלי, אך תגליתם הייתה למעשה רק שהקטעים "חסרי מידה משותפת", ומושג המספר האי רציונלי מאוחר יותר.[2] אזכור ראשון לקיומם של קטעים חסרי מידה משותפת מופיע בדיאלוג "תאיטיטוס" של אפלטון, אך רעיון זה היה מוכר עוד קודם לכן, במאה החמישית לפנה"ס להיפאסוס, בן האסכולה הפיתגוראית, ואולי לפיתגורס עצמו.[3]`, - status: EscapeStatus{Escaped: true, HasAmbiguous: true}, + בשנים 582 לפנה"ס עד 496 לפנה"ס, בקירוב, חי מתמטיקאי חשוב במיוחד - פיתגורס. המקורות הראשוניים עליו מועטים, וההיסטוריונים מתקשים להפריד את העובדות משכבת המסתורין והאגדות שנקשרו בו. ידוע שסביבו התקבצה האסכולה הפיתגוראית מעין כת פסבדו-מתמטית שהאמינה ש"הכל מספר", או ליתר דיוק הכל ניתן לכימות, וייחסה למספרים משמעויות מיסטיות. ככל הנראה הפיתגוראים ידעו לבנות את הגופים האפלטוניים, הכירו את הממוצע האריתמטי, הממוצע הגאומטרי והממוצע ההרמוני והגיעו להישגים חשובים נוספים. ניתן לומר שהפיתגוראים גילו את היותו של השורש הריבועי של 2, שהוא גם האלכסון בריבוע שאורך צלעותיו 1, אי רציונלי, אך תגליתם הייתה למעשה רק שהקטעים "חסרי מידה משותפת", ומושג המספר האי רציונלי מאוחר יותר.[2] אזכור ראשון לקיומם של קטעים חסרי מידה משותפת מופיע בדיאלוג "תאיטיטוס" של אפלטון, אך רעיון זה היה מוכר עוד קודם לכן, במאה החמישית לפנה"ס להיפאסוס, בן האסכולה הפיתגוראית, ואולי לפיתגורס עצמו.[3]`, + status: EscapeStatus{}, }, { name: "Mixed RTL+LTR", @@ -111,7 +111,7 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`, { name: "CVE testcase", text: "if access_level != \"user\u202E \u2066// Check if admin\u2069 \u2066\" {", - result: `if access_level != "user` + "\u202e" + ` ` + "\u2066" + `// Check if admin` + "\u2069" + ` ` + "\u2066" + `" {`, + result: `if access_level != "user` + "\u202e" + ` ` + "\u2066" + `// Check if admin` + "\u2069" + ` ` + "\u2066" + `" {`, status: EscapeStatus{Escaped: true, HasInvisible: true}, }, { @@ -123,7 +123,7 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`, result: `Many computer programs fail to display bidirectional text correctly. For example, the Hebrew name Sarah ` + "\u2067" + `שרה` + "\u2066\n" + `sin (ש) (which appears rightmost), then resh (ר), and finally heh (ה) (which should appear leftmost).` + - "\n" + `if access_level != "user` + "\u202e" + ` ` + "\u2066" + `// Check if admin` + "\u2069" + ` ` + "\u2066" + `" {` + "\n", + "\n" + `if access_level != "user` + "\u202e" + ` ` + "\u2066" + `// Check if admin` + "\u2069" + ` ` + "\u2066" + `" {` + "\n", status: EscapeStatus{Escaped: true, HasInvisible: true}, }, { @@ -134,38 +134,22 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`, result: "\xef\xbb\xbftest", status: EscapeStatus{}, }, + { + name: "ambiguous", + text: "O𝐾", + result: `O𝐾`, + status: EscapeStatus{Escaped: true, HasAmbiguous: true}, + }, } func TestEscapeControlReader(t *testing.T) { - // add some control characters to the tests - tests := make([]escapeControlTest, 0, len(escapeControlTests)*3) - copy(tests, escapeControlTests) - - // if there is a BOM, we should keep the BOM - addPrefix := func(prefix, s string) string { - if strings.HasPrefix(s, "\xef\xbb\xbf") { - return s[:3] + prefix + s[3:] - } - return prefix + s - } - for _, test := range escapeControlTests { - test.name += " (+Control)" - test.text = addPrefix("\u001E", test.text) - test.result = addPrefix(``+"\u001e"+``, test.result) - test.status.Escaped = true - test.status.HasInvisible = true - tests = append(tests, test) - } - - re := regexp.MustCompile(`repo.ambiguous_character:\d+,\d+`) // simplify the output for the tests, remove the translation variants - for _, tt := range tests { + for _, tt := range escapeControlTests { t.Run(tt.name, func(t *testing.T) { output := &strings.Builder{} status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{}) assert.NoError(t, err) assert.Equal(t, tt.status, *status) outStr := output.String() - outStr = re.ReplaceAllString(outStr, "repo.ambiguous_character") assert.Equal(t, tt.result, outStr) }) } @@ -179,3 +163,50 @@ func TestSettingAmbiguousUnicodeDetection(t *testing.T) { _, out = EscapeControlHTML("a test", &translation.MockLocale{}) assert.EqualValues(t, `a test`, out) } + +func TestHTMLChunkReader(t *testing.T) { + type textPart struct { + text string + isTag bool + } + testReadChunks := func(t *testing.T, chunkSize int, input string, expected []textPart) { + r := &htmlChunkReader{in: strings.NewReader(input), readBuf: make([]byte, 0, chunkSize)} + var results []textPart + for { + parts, partIsTag, err := r.readRunes() + if err != nil { + break + } + for i, part := range parts { + results = append(results, textPart{string(part), partIsTag[i]}) + } + } + assert.Equal(t, expected, results, "chunk size: %d, input: %s", chunkSize, input) + } + + testReadChunks(t, 10, "abcghi", []textPart{ + {text: "abc", isTag: false}, + {text: "", isTag: true}, + {text: "gh", isTag: false}, + // -- chunk + {text: "i", isTag: false}, + }) + + testReadChunks(t, 10, "ghi", []textPart{ + {text: "", isTag: true}, + {text: "", isTag: true}, + // -- chunk + {text: "ghi", isTag: false}, + }) + + rune1, rune2, rune3, rune4 := "A", "é", "啊", "🌞" + require.Len(t, rune1, 1) + require.Len(t, rune2, 2) + require.Len(t, rune3, 3) + require.Len(t, rune4, 4) + input := "<" + rune1 + rune2 + rune3 + rune4 + ">" + rune1 + rune2 + rune3 + rune4 + testReadChunks(t, 4, input, []textPart{{"", true}, {"Aé", false}, {"啊", false}, {"🌞", false}}) + testReadChunks(t, 5, input, []textPart{{"", true}, {"Aé", false}, {"啊", false}, {"🌞", false}}) + testReadChunks(t, 6, input, []textPart{{"", true}, {"A", false}, {"é啊", false}, {"🌞", false}}) + testReadChunks(t, 7, input, []textPart{{"", true}, {"A", false}, {"é啊", false}, {"🌞", false}}) +} diff --git a/modules/charset/ambiguous/ambiguous.json b/modules/charset/generate/ambiguous.json similarity index 100% rename from modules/charset/ambiguous/ambiguous.json rename to modules/charset/generate/ambiguous.json diff --git a/modules/charset/invisible/generate.go b/modules/charset/generate/generate.go similarity index 57% rename from modules/charset/invisible/generate.go rename to modules/charset/generate/generate.go index bd57dd6c4d..16ea53fda1 100644 --- a/modules/charset/invisible/generate.go +++ b/modules/charset/generate/generate.go @@ -5,37 +5,86 @@ package main import ( "bytes" - "flag" "fmt" "go/format" + "log" "os" + "sort" "text/template" + "unicode" + + "code.gitea.io/gitea/modules/json" "golang.org/x/text/unicode/rangetable" ) +// ambiguous.json provides a one to one mapping of ambiguous characters to other characters +// See https://github.com/hediet/vscode-unicode-data/blob/main/out/ambiguous.json + +type AmbiguousTable struct { + Confusable []rune + With []rune + Locale string + RangeTable *unicode.RangeTable +} + +type RunePair struct { + Confusable rune + With rune +} + // InvisibleRunes these are runes that vscode has assigned to be invisible // See https://github.com/hediet/vscode-unicode-data var InvisibleRunes = []rune{ 9, 10, 11, 12, 13, 32, 127, 160, 173, 847, 1564, 4447, 4448, 6068, 6069, 6155, 6156, 6157, 6158, 7355, 7356, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8203, 8204, 8205, 8206, 8207, 8234, 8235, 8236, 8237, 8238, 8239, 8287, 8288, 8289, 8290, 8291, 8292, 8293, 8294, 8295, 8296, 8297, 8298, 8299, 8300, 8301, 8302, 8303, 10240, 12288, 12644, 65024, 65025, 65026, 65027, 65028, 65029, 65030, 65031, 65032, 65033, 65034, 65035, 65036, 65037, 65038, 65039, 65279, 65440, 65520, 65521, 65522, 65523, 65524, 65525, 65526, 65527, 65528, 65532, 78844, 119155, 119156, 119157, 119158, 119159, 119160, 119161, 119162, 917504, 917505, 917506, 917507, 917508, 917509, 917510, 917511, 917512, 917513, 917514, 917515, 917516, 917517, 917518, 917519, 917520, 917521, 917522, 917523, 917524, 917525, 917526, 917527, 917528, 917529, 917530, 917531, 917532, 917533, 917534, 917535, 917536, 917537, 917538, 917539, 917540, 917541, 917542, 917543, 917544, 917545, 917546, 917547, 917548, 917549, 917550, 917551, 917552, 917553, 917554, 917555, 917556, 917557, 917558, 917559, 917560, 917561, 917562, 917563, 917564, 917565, 917566, 917567, 917568, 917569, 917570, 917571, 917572, 917573, 917574, 917575, 917576, 917577, 917578, 917579, 917580, 917581, 917582, 917583, 917584, 917585, 917586, 917587, 917588, 917589, 917590, 917591, 917592, 917593, 917594, 917595, 917596, 917597, 917598, 917599, 917600, 917601, 917602, 917603, 917604, 917605, 917606, 917607, 917608, 917609, 917610, 917611, 917612, 917613, 917614, 917615, 917616, 917617, 917618, 917619, 917620, 917621, 917622, 917623, 917624, 917625, 917626, 917627, 917628, 917629, 917630, 917631, 917760, 917761, 917762, 917763, 917764, 917765, 917766, 917767, 917768, 917769, 917770, 917771, 917772, 917773, 917774, 917775, 917776, 917777, 917778, 917779, 917780, 917781, 917782, 917783, 917784, 917785, 917786, 917787, 917788, 917789, 917790, 917791, 917792, 917793, 917794, 917795, 917796, 917797, 917798, 917799, 917800, 917801, 917802, 917803, 917804, 917805, 917806, 917807, 917808, 917809, 917810, 917811, 917812, 917813, 917814, 917815, 917816, 917817, 917818, 917819, 917820, 917821, 917822, 917823, 917824, 917825, 917826, 917827, 917828, 917829, 917830, 917831, 917832, 917833, 917834, 917835, 917836, 917837, 917838, 917839, 917840, 917841, 917842, 917843, 917844, 917845, 917846, 917847, 917848, 917849, 917850, 917851, 917852, 917853, 917854, 917855, 917856, 917857, 917858, 917859, 917860, 917861, 917862, 917863, 917864, 917865, 917866, 917867, 917868, 917869, 917870, 917871, 917872, 917873, 917874, 917875, 917876, 917877, 917878, 917879, 917880, 917881, 917882, 917883, 917884, 917885, 917886, 917887, 917888, 917889, 917890, 917891, 917892, 917893, 917894, 917895, 917896, 917897, 917898, 917899, 917900, 917901, 917902, 917903, 917904, 917905, 917906, 917907, 917908, 917909, 917910, 917911, 917912, 917913, 917914, 917915, 917916, 917917, 917918, 917919, 917920, 917921, 917922, 917923, 917924, 917925, 917926, 917927, 917928, 917929, 917930, 917931, 917932, 917933, 917934, 917935, 917936, 917937, 917938, 917939, 917940, 917941, 917942, 917943, 917944, 917945, 917946, 917947, 917948, 917949, 917950, 917951, 917952, 917953, 917954, 917955, 917956, 917957, 917958, 917959, 917960, 917961, 917962, 917963, 917964, 917965, 917966, 917967, 917968, 917969, 917970, 917971, 917972, 917973, 917974, 917975, 917976, 917977, 917978, 917979, 917980, 917981, 917982, 917983, 917984, 917985, 917986, 917987, 917988, 917989, 917990, 917991, 917992, 917993, 917994, 917995, 917996, 917997, 917998, 917999, } -var verbose bool - -func main() { - flag.Usage = func() { - fmt.Fprintf(os.Stderr, `%s: Generate InvisibleRunesRange - -Usage: %[1]s [-v] [-o output.go] -`, os.Args[0]) - flag.PrintDefaults() +func generateAmbiguous() { + bs, err := os.ReadFile("ambiguous.json") + if err != nil { + log.Fatalf("Unable to read, err: %v", err) } - output := "" - flag.BoolVar(&verbose, "v", false, "verbose output") - flag.StringVar(&output, "o", "invisible_gen.go", "file to output to") - flag.Parse() + var unwrapped string + if err := json.Unmarshal(bs, &unwrapped); err != nil { + log.Fatalf("Unable to unwrap content in, err: %v", err) + } + fromJSON := map[string][]uint32{} + if err := json.Unmarshal([]byte(unwrapped), &fromJSON); err != nil { + log.Fatalf("Unable to unmarshal content in, err: %v", err) + } + + tables := make([]*AmbiguousTable, 0, len(fromJSON)) + for locale, chars := range fromJSON { + table := &AmbiguousTable{Locale: locale} + table.Confusable = make([]rune, 0, len(chars)/2) + table.With = make([]rune, 0, len(chars)/2) + pairs := make([]RunePair, len(chars)/2) + for i := 0; i < len(chars); i += 2 { + pairs[i/2].Confusable, pairs[i/2].With = rune(chars[i]), rune(chars[i+1]) + } + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Confusable < pairs[j].Confusable + }) + for _, pair := range pairs { + table.Confusable = append(table.Confusable, pair.Confusable) + table.With = append(table.With, pair.With) + } + table.RangeTable = rangetable.New(table.Confusable...) + tables = append(tables, table) + } + sort.Slice(tables, func(i, j int) bool { + return tables[i].Locale < tables[j].Locale + }) + data := map[string]any{"Tables": tables} + + if err := runTemplate(templateAmbiguous, "../ambiguous_gen.go", &data); err != nil { + log.Fatalf("Unable to run template: %v", err) + } +} + +func generateInvisible() { // First we filter the runes to remove // filtered := make([]rune, 0, len(InvisibleRunes)) @@ -47,8 +96,8 @@ Usage: %[1]s [-v] [-o output.go] } table := rangetable.New(filtered...) - if err := runTemplate(generatorTemplate, output, table); err != nil { - fatalf("Unable to run template: %v", err) + if err := runTemplate(generatorInvisible, "../invisible_gen.go", table); err != nil { + log.Fatalf("Unable to run template: %v", err) } } @@ -59,7 +108,7 @@ func runTemplate(t *template.Template, filename string, data any) error { } bs, err := format.Source(buf.Bytes()) if err != nil { - verbosef("Bad source:\n%s", buf.String()) + log.Printf("Bad source:\n%s", buf.String()) return fmt.Errorf("unable to format source: %w", err) } @@ -85,37 +134,68 @@ func runTemplate(t *template.Template, filename string, data any) error { return nil } -var generatorTemplate = template.Must(template.New("invisibleTemplate").Parse(`// This file is generated by modules/charset/invisible/generate.go DO NOT EDIT -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT +func main() { + generateAmbiguous() + generateInvisible() +} +var templateAmbiguous = template.Must(template.New("ambiguousTemplate").Parse(`// This file is generated by modules/charset/generate/generate.go DO NOT EDIT +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT package charset import "unicode" -var InvisibleRanges = &unicode.RangeTable{ - R16: []unicode.Range16{ -{{range .R16 }} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, -{{end}} }, - R32: []unicode.Range32{ -{{range .R32}} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, -{{end}} }, - LatinOffset: {{.LatinOffset}}, +// This file is generated from https://github.com/hediet/vscode-unicode-data/blob/main/out/ambiguous.json + +// AmbiguousTable matches a confusable rune with its partner for the Locale +type AmbiguousTable struct { + Confusable []rune + With []rune + Locale string + RangeTable *unicode.RangeTable +} + +func newAmbiguousTableMap() map[string]*AmbiguousTable { + return map[string]*AmbiguousTable { + {{- range .Tables}} + {{printf "%q" .Locale}}: { + Confusable: []rune{ {{range .Confusable}}{{.}},{{end}} }, + With: []rune{ {{range .With}}{{.}},{{end}} }, + Locale: {{printf "%q" .Locale}}, + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {{range .RangeTable.R16 }} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, + {{end}} }, + R32: []unicode.Range32{ + {{range .RangeTable.R32}} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, + {{end}} }, + LatinOffset: {{.RangeTable.LatinOffset}}, + }, + }, + {{end}} + } } `)) -func logf(format string, args ...any) { - fmt.Fprintf(os.Stderr, format+"\n", args...) -} +var generatorInvisible = template.Must(template.New("invisibleTemplate").Parse(`// This file is generated by modules/charset/generate/generate.go DO NOT EDIT +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT -func verbosef(format string, args ...any) { - if verbose { - logf(format, args...) +package charset + +import "unicode" + +func newInvisibleRangeTable() *unicode.RangeTable { + return &unicode.RangeTable{ + R16: []unicode.Range16{ +{{range .R16 }} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, + {{end}}}, + R32: []unicode.Range32{ +{{range .R32}} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, + {{end}}}, + LatinOffset: {{.LatinOffset}}, } } - -func fatalf(format string, args ...any) { - logf("fatal: "+format+"\n", args...) - os.Exit(1) -} +`)) diff --git a/modules/charset/htmlstream.go b/modules/charset/htmlstream.go deleted file mode 100644 index 61f29120a6..0000000000 --- a/modules/charset/htmlstream.go +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package charset - -import ( - "fmt" - "io" - - "golang.org/x/net/html" -) - -// HTMLStreamer represents a SAX-like interface for HTML -type HTMLStreamer interface { - Error(err error) error - Doctype(data string) error - Comment(data string) error - StartTag(data string, attrs ...html.Attribute) error - SelfClosingTag(data string, attrs ...html.Attribute) error - EndTag(data string) error - Text(data string) error -} - -// PassthroughHTMLStreamer is a passthrough streamer -type PassthroughHTMLStreamer struct { - next HTMLStreamer -} - -func NewPassthroughStreamer(next HTMLStreamer) *PassthroughHTMLStreamer { - return &PassthroughHTMLStreamer{next: next} -} - -var _ (HTMLStreamer) = &PassthroughHTMLStreamer{} - -// Error tells the next streamer in line that there is an error -func (p *PassthroughHTMLStreamer) Error(err error) error { - return p.next.Error(err) -} - -// Doctype tells the next streamer what the doctype is -func (p *PassthroughHTMLStreamer) Doctype(data string) error { - return p.next.Doctype(data) -} - -// Comment tells the next streamer there is a comment -func (p *PassthroughHTMLStreamer) Comment(data string) error { - return p.next.Comment(data) -} - -// StartTag tells the next streamer there is a starting tag -func (p *PassthroughHTMLStreamer) StartTag(data string, attrs ...html.Attribute) error { - return p.next.StartTag(data, attrs...) -} - -// SelfClosingTag tells the next streamer there is a self-closing tag -func (p *PassthroughHTMLStreamer) SelfClosingTag(data string, attrs ...html.Attribute) error { - return p.next.SelfClosingTag(data, attrs...) -} - -// EndTag tells the next streamer there is a end tag -func (p *PassthroughHTMLStreamer) EndTag(data string) error { - return p.next.EndTag(data) -} - -// Text tells the next streamer there is a text -func (p *PassthroughHTMLStreamer) Text(data string) error { - return p.next.Text(data) -} - -// HTMLStreamWriter acts as a writing sink -type HTMLStreamerWriter struct { - io.Writer - err error -} - -// Write implements io.Writer -func (h *HTMLStreamerWriter) Write(data []byte) (int, error) { - if h.err != nil { - return 0, h.err - } - return h.Writer.Write(data) -} - -// Write implements io.StringWriter -func (h *HTMLStreamerWriter) WriteString(data string) (int, error) { - if h.err != nil { - return 0, h.err - } - return h.Writer.Write([]byte(data)) -} - -// Error tells the next streamer in line that there is an error -func (h *HTMLStreamerWriter) Error(err error) error { - if h.err == nil { - h.err = err - } - return h.err -} - -// Doctype tells the next streamer what the doctype is -func (h *HTMLStreamerWriter) Doctype(data string) error { - _, h.err = h.WriteString("") - return h.err -} - -// Comment tells the next streamer there is a comment -func (h *HTMLStreamerWriter) Comment(data string) error { - _, h.err = h.WriteString("") - return h.err -} - -// StartTag tells the next streamer there is a starting tag -func (h *HTMLStreamerWriter) StartTag(data string, attrs ...html.Attribute) error { - return h.startTag(data, attrs, false) -} - -// SelfClosingTag tells the next streamer there is a self-closing tag -func (h *HTMLStreamerWriter) SelfClosingTag(data string, attrs ...html.Attribute) error { - return h.startTag(data, attrs, true) -} - -func (h *HTMLStreamerWriter) startTag(data string, attrs []html.Attribute, selfclosing bool) error { - if _, h.err = h.WriteString("<" + data); h.err != nil { - return h.err - } - for _, attr := range attrs { - if _, h.err = h.WriteString(" " + attr.Key + "=\"" + html.EscapeString(attr.Val) + "\""); h.err != nil { - return h.err - } - } - if selfclosing { - if _, h.err = h.WriteString("/>"); h.err != nil { - return h.err - } - } else { - if _, h.err = h.WriteString(">"); h.err != nil { - return h.err - } - } - return h.err -} - -// EndTag tells the next streamer there is a end tag -func (h *HTMLStreamerWriter) EndTag(data string) error { - _, h.err = h.WriteString("") - return h.err -} - -// Text tells the next streamer there is a text -func (h *HTMLStreamerWriter) Text(data string) error { - _, h.err = h.WriteString(html.EscapeString(data)) - return h.err -} - -// StreamHTML streams an html to a provided streamer -func StreamHTML(source io.Reader, streamer HTMLStreamer) error { - tokenizer := html.NewTokenizer(source) - for { - tt := tokenizer.Next() - switch tt { - case html.ErrorToken: - if tokenizer.Err() != io.EOF { - return tokenizer.Err() - } - return nil - case html.DoctypeToken: - token := tokenizer.Token() - if err := streamer.Doctype(token.Data); err != nil { - return err - } - case html.CommentToken: - token := tokenizer.Token() - if err := streamer.Comment(token.Data); err != nil { - return err - } - case html.StartTagToken: - token := tokenizer.Token() - if err := streamer.StartTag(token.Data, token.Attr...); err != nil { - return err - } - case html.SelfClosingTagToken: - token := tokenizer.Token() - if err := streamer.StartTag(token.Data, token.Attr...); err != nil { - return err - } - case html.EndTagToken: - token := tokenizer.Token() - if err := streamer.EndTag(token.Data); err != nil { - return err - } - case html.TextToken: - token := tokenizer.Token() - if err := streamer.Text(token.Data); err != nil { - return err - } - default: - return fmt.Errorf("unknown type of token: %d", tt) - } - } -} diff --git a/modules/charset/invisible_gen.go b/modules/charset/invisible_gen.go index 812f0e34b3..ddda875a9f 100644 --- a/modules/charset/invisible_gen.go +++ b/modules/charset/invisible_gen.go @@ -1,36 +1,38 @@ -// This file is generated by modules/charset/invisible/generate.go DO NOT EDIT -// Copyright 2022 The Gitea Authors. All rights reserved. +// This file is generated by modules/charset/generate/generate.go DO NOT EDIT +// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package charset import "unicode" -var InvisibleRanges = &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 11, Hi: 13, Stride: 1}, - {Lo: 127, Hi: 160, Stride: 33}, - {Lo: 173, Hi: 847, Stride: 674}, - {Lo: 1564, Hi: 4447, Stride: 2883}, - {Lo: 4448, Hi: 6068, Stride: 1620}, - {Lo: 6069, Hi: 6155, Stride: 86}, - {Lo: 6156, Hi: 6158, Stride: 1}, - {Lo: 7355, Hi: 7356, Stride: 1}, - {Lo: 8192, Hi: 8207, Stride: 1}, - {Lo: 8234, Hi: 8239, Stride: 1}, - {Lo: 8287, Hi: 8303, Stride: 1}, - {Lo: 10240, Hi: 12288, Stride: 2048}, - {Lo: 12644, Hi: 65024, Stride: 52380}, - {Lo: 65025, Hi: 65039, Stride: 1}, - {Lo: 65279, Hi: 65440, Stride: 161}, - {Lo: 65520, Hi: 65528, Stride: 1}, - {Lo: 65532, Hi: 65532, Stride: 1}, - }, - R32: []unicode.Range32{ - {Lo: 78844, Hi: 119155, Stride: 40311}, - {Lo: 119156, Hi: 119162, Stride: 1}, - {Lo: 917504, Hi: 917631, Stride: 1}, - {Lo: 917760, Hi: 917999, Stride: 1}, - }, - LatinOffset: 2, +func newInvisibleRangeTable() *unicode.RangeTable { + return &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 11, Hi: 13, Stride: 1}, + {Lo: 127, Hi: 160, Stride: 33}, + {Lo: 173, Hi: 847, Stride: 674}, + {Lo: 1564, Hi: 4447, Stride: 2883}, + {Lo: 4448, Hi: 6068, Stride: 1620}, + {Lo: 6069, Hi: 6155, Stride: 86}, + {Lo: 6156, Hi: 6158, Stride: 1}, + {Lo: 7355, Hi: 7356, Stride: 1}, + {Lo: 8192, Hi: 8207, Stride: 1}, + {Lo: 8234, Hi: 8239, Stride: 1}, + {Lo: 8287, Hi: 8303, Stride: 1}, + {Lo: 10240, Hi: 12288, Stride: 2048}, + {Lo: 12644, Hi: 65024, Stride: 52380}, + {Lo: 65025, Hi: 65039, Stride: 1}, + {Lo: 65279, Hi: 65440, Stride: 161}, + {Lo: 65520, Hi: 65528, Stride: 1}, + {Lo: 65532, Hi: 65532, Stride: 1}, + }, + R32: []unicode.Range32{ + {Lo: 78844, Hi: 119155, Stride: 40311}, + {Lo: 119156, Hi: 119162, Stride: 1}, + {Lo: 917504, Hi: 917631, Stride: 1}, + {Lo: 917760, Hi: 917999, Stride: 1}, + }, + LatinOffset: 2, + } } diff --git a/modules/commitstatus/commit_status.go b/modules/commitstatus/commit_status.go index a0ab4e7186..9c6994249b 100644 --- a/modules/commitstatus/commit_status.go +++ b/modules/commitstatus/commit_status.go @@ -61,16 +61,17 @@ type CommitStatusStates []CommitStatusState //nolint:revive // export stutter // According to https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#get-the-combined-status-for-a-specific-reference // > Additionally, a combined state is returned. The state is one of: // > failure if any of the contexts report as error or failure +// > failure if any of the contexts report as warning (Gitea specific behavior) // > pending if there are no statuses or a context is pending // > success if the latest status for all contexts is success func (css CommitStatusStates) Combine() CommitStatusState { successCnt := 0 for _, state := range css { switch { - case state.IsError() || state.IsFailure(): + case state.IsError() || state.IsFailure() || state.IsWarning(): return CommitStatusFailure case state.IsPending(): - case state.IsSuccess() || state.IsWarning() || state.IsSkipped(): + case state.IsSuccess() || state.IsSkipped(): successCnt++ } } diff --git a/modules/commitstatus/commit_status_test.go b/modules/commitstatus/commit_status_test.go index 10d8f20aa4..dc8f9ce1a5 100644 --- a/modules/commitstatus/commit_status_test.go +++ b/modules/commitstatus/commit_status_test.go @@ -41,7 +41,7 @@ func TestCombine(t *testing.T) { { name: "warning", states: CommitStatusStates{CommitStatusWarning}, - expected: CommitStatusSuccess, + expected: CommitStatusFailure, }, // 2 states { @@ -62,7 +62,7 @@ func TestCombine(t *testing.T) { { name: "pending and warning", states: CommitStatusStates{CommitStatusPending, CommitStatusWarning}, - expected: CommitStatusPending, + expected: CommitStatusFailure, }, { name: "success and error", @@ -77,7 +77,7 @@ func TestCombine(t *testing.T) { { name: "success and warning", states: CommitStatusStates{CommitStatusSuccess, CommitStatusWarning}, - expected: CommitStatusSuccess, + expected: CommitStatusFailure, }, { name: "error and failure", @@ -98,7 +98,7 @@ func TestCombine(t *testing.T) { { name: "pending, success and warning", states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusWarning}, - expected: CommitStatusPending, + expected: CommitStatusFailure, }, { name: "pending, success and error", @@ -133,7 +133,7 @@ func TestCombine(t *testing.T) { { name: "success, warning and skipped", states: CommitStatusStates{CommitStatusSuccess, CommitStatusWarning, CommitStatusSkipped}, - expected: CommitStatusSuccess, + expected: CommitStatusFailure, }, // All success { @@ -181,12 +181,12 @@ func TestCombine(t *testing.T) { { name: "mixed states with all success", states: CommitStatusStates{CommitStatusSuccess, CommitStatusSuccess, CommitStatusPending, CommitStatusWarning}, - expected: CommitStatusPending, + expected: CommitStatusFailure, }, { name: "all success with warning", states: CommitStatusStates{CommitStatusSuccess, CommitStatusSuccess, CommitStatusSuccess, CommitStatusWarning}, - expected: CommitStatusSuccess, + expected: CommitStatusFailure, }, } diff --git a/modules/dump/dumper.go b/modules/dump/dumper.go index 02829d6a1e..2f16070704 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 ebcca50903..c72cc466a0 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/generate/generate.go b/modules/generate/generate.go index 2d9a3dd902..9baa057b17 100644 --- a/modules/generate/generate.go +++ b/modules/generate/generate.go @@ -54,21 +54,16 @@ func DecodeJwtSecretBase64(src string) ([]byte, error) { } // NewJwtSecretWithBase64 generates a jwt secret with its base64 encoded value intended to be used for saving into config file -func NewJwtSecretWithBase64() ([]byte, string, error) { +func NewJwtSecretWithBase64() ([]byte, string) { bytes := make([]byte, defaultJwtSecretLen) - _, err := io.ReadFull(rand.Reader, bytes) + _, err := rand.Read(bytes) if err != nil { - return nil, "", err + panic(err) // rand.Read never fails } - return bytes, base64.RawURLEncoding.EncodeToString(bytes), nil + return bytes, base64.RawURLEncoding.EncodeToString(bytes) } // NewSecretKey generate a new value intended to be used by SECRET_KEY. func NewSecretKey() (string, error) { - secretKey, err := util.CryptoRandomString(64) - if err != nil { - return "", err - } - - return secretKey, nil + return util.CryptoRandomString(64), nil } diff --git a/modules/generate/generate_test.go b/modules/generate/generate_test.go index af640a60c1..f9dd20cc7f 100644 --- a/modules/generate/generate_test.go +++ b/modules/generate/generate_test.go @@ -25,10 +25,12 @@ func TestDecodeJwtSecretBase64(t *testing.T) { } func TestNewJwtSecretWithBase64(t *testing.T) { - secret, encoded, err := NewJwtSecretWithBase64() - assert.NoError(t, err) + secret, encoded := NewJwtSecretWithBase64() assert.Len(t, secret, 32) decoded, err := DecodeJwtSecretBase64(encoded) assert.NoError(t, err) assert.Equal(t, secret, decoded) + + secret2, _ := NewJwtSecretWithBase64() + assert.NotEqual(t, secret, secret2) } diff --git a/modules/git/catfile_batch_command.go b/modules/git/catfile_batch_command.go index 710561f045..4e18282bf3 100644 --- a/modules/git/catfile_batch_command.go +++ b/modules/git/catfile_batch_command.go @@ -7,8 +7,10 @@ import ( "context" "os" "path/filepath" + "strings" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -39,6 +41,9 @@ func (b *catFileBatchCommand) getBatch() *catFileBatchCommunicator { } func (b *catFileBatchCommand) QueryContent(obj string) (*CatFileObject, BufferedReader, error) { + if strings.Contains(obj, "\n") { + setting.PanicInDevOrTesting("invalid object name with newline: %q", obj) + } _, err := b.getBatch().reqWriter.Write([]byte("contents " + obj + "\n")) if err != nil { return nil, nil, err @@ -51,6 +56,9 @@ func (b *catFileBatchCommand) QueryContent(obj string) (*CatFileObject, Buffered } func (b *catFileBatchCommand) QueryInfo(obj string) (*CatFileObject, error) { + if strings.Contains(obj, "\n") { + setting.PanicInDevOrTesting("invalid object name with newline: %q", obj) + } _, err := b.getBatch().reqWriter.Write([]byte("info " + obj + "\n")) if err != nil { return nil, err diff --git a/modules/git/catfile_batch_legacy.go b/modules/git/catfile_batch_legacy.go index 795fc4ce3d..595043d1d2 100644 --- a/modules/git/catfile_batch_legacy.go +++ b/modules/git/catfile_batch_legacy.go @@ -8,8 +8,10 @@ import ( "io" "os" "path/filepath" + "strings" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -50,6 +52,9 @@ func (b *catFileBatchLegacy) getBatchCheck() *catFileBatchCommunicator { } func (b *catFileBatchLegacy) QueryContent(obj string) (*CatFileObject, BufferedReader, error) { + if strings.Contains(obj, "\n") { + setting.PanicInDevOrTesting("invalid object name with newline: %q", obj) + } _, err := io.WriteString(b.getBatchContent().reqWriter, obj+"\n") if err != nil { return nil, nil, err @@ -62,6 +67,9 @@ func (b *catFileBatchLegacy) QueryContent(obj string) (*CatFileObject, BufferedR } func (b *catFileBatchLegacy) QueryInfo(obj string) (*CatFileObject, error) { + if strings.Contains(obj, "\n") { + setting.PanicInDevOrTesting("invalid object name with newline: %q", obj) + } _, err := io.WriteString(b.getBatchCheck().reqWriter, obj+"\n") if err != nil { return nil, err diff --git a/modules/git/catfile_batch_reader.go b/modules/git/catfile_batch_reader.go index 8a0b342079..4d77fb03c7 100644 --- a/modules/git/catfile_batch_reader.go +++ b/modules/git/catfile_batch_reader.go @@ -10,58 +10,49 @@ import ( "errors" "io" "math" + "slices" "strconv" "strings" "sync/atomic" - "time" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) -var catFileBatchDebugWaitClose atomic.Int64 - type catFileBatchCommunicator struct { - cancel context.CancelFunc + closeFunc atomic.Pointer[func(err error)] reqWriter io.Writer respReader *bufio.Reader debugGitCmd *gitcmd.Command } -func (b *catFileBatchCommunicator) Close() { - if b.cancel != nil { - b.cancel() - b.cancel = nil +func (b *catFileBatchCommunicator) Close(err ...error) { + if fn := b.closeFunc.Swap(nil); fn != nil { + (*fn)(util.OptionalArg(err)) } } -// newCatFileBatch opens git cat-file --batch in the provided repo and returns a stdin pipe, a stdout reader and cancel function -func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) (ret *catFileBatchCommunicator) { +// newCatFileBatch opens git cat-file --batch/--batch-check/--batch-command command and prepares the stdin/stdout pipes for communication. +func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) *catFileBatchCommunicator { ctx, ctxCancel := context.WithCancelCause(ctx) - - // We often want to feed the commits in order into cat-file --batch, followed by their trees and subtrees as necessary. stdinWriter, stdoutReader, stdPipeClose := cmdCatFile.MakeStdinStdoutPipe() - pipeClose := func() { - if delay := catFileBatchDebugWaitClose.Load(); delay > 0 { - time.Sleep(time.Duration(delay)) // for testing purpose only - } - stdPipeClose() - } - - ret = &catFileBatchCommunicator{ + ret := &catFileBatchCommunicator{ debugGitCmd: cmdCatFile, - cancel: func() { ctxCancel(nil) }, reqWriter: stdinWriter, respReader: bufio.NewReaderSize(stdoutReader, 32*1024), // use a buffered reader for rich operations } + ret.closeFunc.Store(new(func(err error) { + ctxCancel(err) + stdPipeClose() + })) err := cmdCatFile.WithDir(repoPath).StartWithStderr(ctx) if err != nil { log.Error("Unable to start git command %v: %v", cmdCatFile.LogString(), err) // ideally here it should return the error, but it would require refactoring all callers // so just return a dummy communicator that does nothing, almost the same behavior as before, not bad - ctxCancel(err) - pipeClose() + ret.Close(err) return ret } @@ -70,13 +61,33 @@ func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Co if err != nil && !errors.Is(err, context.Canceled) { log.Error("cat-file --batch command failed in repo %s, error: %v", repoPath, err) } - ctxCancel(err) - pipeClose() + ret.Close(err) }() return ret } +func (b *catFileBatchCommunicator) debugKill() (ret struct { + beforeClose chan struct{} + blockClose chan struct{} + afterClose chan struct{} +}, +) { + ret.beforeClose = make(chan struct{}) + ret.blockClose = make(chan struct{}) + ret.afterClose = make(chan struct{}) + oldCloseFunc := b.closeFunc.Load() + b.closeFunc.Store(new(func(err error) { + b.closeFunc.Store(nil) + close(ret.beforeClose) + <-ret.blockClose + (*oldCloseFunc)(err) + close(ret.afterClose) + })) + b.debugGitCmd.DebugKill() + return ret +} + // catFileBatchParseInfoLine reads the header line from cat-file --batch // We expect: SP SP LF // then leaving the rest of the stream " LF" to be read @@ -162,77 +173,46 @@ headerLoop: return id, DiscardFull(rd, size-n+1) } -// git tree files are a list: -// SP NUL -// -// Unfortunately this 20-byte notation is somewhat in conflict to all other git tools -// Therefore we need some method to convert these binary hashes to hex hashes - // ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream -// This carefully avoids allocations - except where fnameBuf is too small. -// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations -// -// Each line is composed of: -// SP NUL -// -// We don't attempt to convert the raw HASH to save a lot of time -func ParseCatFileTreeLine(objectFormat ObjectFormat, rd BufferedReader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { - var readBytes []byte - - // Read the Mode & fname - readBytes, err = rd.ReadSlice('\x00') - if err != nil { - return mode, fname, sha, n, err - } - idx := bytes.IndexByte(readBytes, ' ') - if idx < 0 { - log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes) - return mode, fname, sha, n, &ErrNotExist{} - } - - n += idx + 1 - copy(modeBuf, readBytes[:idx]) - if len(modeBuf) >= idx { - modeBuf = modeBuf[:idx] - } else { - modeBuf = append(modeBuf, readBytes[len(modeBuf):idx]...) - } - mode = modeBuf - - readBytes = readBytes[idx+1:] - - // Deal with the fname - copy(fnameBuf, readBytes) - if len(fnameBuf) > len(readBytes) { - fnameBuf = fnameBuf[:len(readBytes)] - } else { - fnameBuf = append(fnameBuf, readBytes[len(fnameBuf):]...) - } - for err == bufio.ErrBufferFull { - readBytes, err = rd.ReadSlice('\x00') - fnameBuf = append(fnameBuf, readBytes...) - } - n += len(fnameBuf) - if err != nil { - return mode, fname, sha, n, err - } - fnameBuf = fnameBuf[:len(fnameBuf)-1] - fname = fnameBuf - - // Deal with the binary hash - idx = 0 - length := objectFormat.FullLength() / 2 - for idx < length { - var read int - read, err = rd.Read(shaBuf[idx:length]) - n += read - if err != nil { - return mode, fname, sha, n, err +// Each entry is composed of: +// SP NUL +func ParseCatFileTreeLine(objectFormat ObjectFormat, rd BufferedReader) (mode EntryMode, name string, objID ObjectID, n int, err error) { + // use the in-buffer memory as much as possible to avoid extra allocations + bufBytes, err := rd.ReadSlice('\x00') + const maxEntryInfoBytes = 1024 * 1024 + if errors.Is(err, bufio.ErrBufferFull) { + bufBytes = slices.Clone(bufBytes) + for len(bufBytes) < maxEntryInfoBytes && errors.Is(err, bufio.ErrBufferFull) { + var tmp []byte + tmp, err = rd.ReadSlice('\x00') + bufBytes = append(bufBytes, tmp...) } - idx += read } - sha = shaBuf - return mode, fname, sha, n, err + if err != nil { + return mode, name, objID, len(bufBytes), err + } + + idx := bytes.IndexByte(bufBytes, ' ') + if idx < 0 { + return mode, name, objID, len(bufBytes), errors.New("invalid CatFileTreeLine output") + } + + mode = ParseEntryMode(util.UnsafeBytesToString(bufBytes[:idx])) + name = string(bufBytes[idx+1 : len(bufBytes)-1]) // trim the NUL terminator, it needs a copy because the bufBytes will be reused by the reader + if mode == EntryModeNoEntry { + return mode, name, objID, len(bufBytes), errors.New("invalid entry mode: " + string(bufBytes[:idx])) + } + + switch objectFormat { + case Sha1ObjectFormat: + objID = &Sha1Hash{} + case Sha256ObjectFormat: + objID = &Sha256Hash{} + default: + panic("unsupported object format: " + objectFormat.Name()) + } + readIDLen, err := io.ReadFull(rd, objID.RawValue()) + return mode, name, objID, len(bufBytes) + readIDLen, err } func DiscardFull(rd BufferedReader, discard int64) error { diff --git a/modules/git/catfile_batch_test.go b/modules/git/catfile_batch_test.go index 69662ffc1a..782d34d249 100644 --- a/modules/git/catfile_batch_test.go +++ b/modules/git/catfile_batch_test.go @@ -7,9 +7,7 @@ import ( "io" "os" "path/filepath" - "sync" "testing" - "time" "code.gitea.io/gitea/modules/test" @@ -39,13 +37,22 @@ func testCatFileBatch(t *testing.T) { require.Error(t, err) }) - simulateQueryTerminated := func(pipeCloseDelay, pipeReadDelay time.Duration) (errRead error) { - catFileBatchDebugWaitClose.Store(int64(pipeCloseDelay)) - defer catFileBatchDebugWaitClose.Store(0) + simulateQueryTerminated := func(t *testing.T, errBeforePipeClose, errAfterPipeClose error) { + readError := func(t *testing.T, r io.Reader, expectedErr error) { + if expectedErr == nil { + return // expectedErr == nil means this read should be skipped + } + n, err := r.Read(make([]byte, 100)) + assert.Zero(t, n) + assert.ErrorIs(t, err, expectedErr) + } + batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare")) require.NoError(t, err) defer batch.Close() - _, _ = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449") + _, err = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449") + require.NoError(t, err) + var c *catFileBatchCommunicator switch b := batch.(type) { case *catFileBatchLegacy: @@ -58,24 +65,18 @@ func testCatFileBatch(t *testing.T) { t.FailNow() } - wg := sync.WaitGroup{} - wg.Go(func() { - time.Sleep(pipeReadDelay) - var n int - n, errRead = c.respReader.Read(make([]byte, 100)) - assert.Zero(t, n) - }) - time.Sleep(10 * time.Millisecond) - c.debugGitCmd.DebugKill() - wg.Wait() - return errRead - } + require.NotEqual(t, errBeforePipeClose == nil, errAfterPipeClose == nil, "must set exactly one of the expected errors") + inceptor := c.debugKill() + <-inceptor.beforeClose // wait for the command's Close to be called, the pipe is not closed yet + readError(t, c.respReader, errBeforePipeClose) // then caller will read on an open pipe which will be closed soon + close(inceptor.blockClose) // continue to close the pipe + <-inceptor.afterClose // wait for the pipe to be closed + readError(t, c.respReader, errAfterPipeClose) // then caller will read on a closed pipe + } t.Run("QueryTerminated", func(t *testing.T) { - err := simulateQueryTerminated(0, 20*time.Millisecond) - assert.ErrorIs(t, err, os.ErrClosed) // pipes are closed faster - err = simulateQueryTerminated(40*time.Millisecond, 20*time.Millisecond) - assert.ErrorIs(t, err, io.EOF) // reader is faster + simulateQueryTerminated(t, io.EOF, nil) // reader is faster + simulateQueryTerminated(t, nil, os.ErrClosed) // pipes are closed faster }) batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare")) diff --git a/modules/git/commit.go b/modules/git/commit.go index dfecfe6057..b576451db8 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -11,19 +11,28 @@ import ( "os/exec" "strings" + "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/util" ) +type CommitMessage struct { + MessageRaw string + messageUTF8 *string + messageTitle *string + messageBody *string +} + // Commit represents a git commit. type Commit struct { Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache" - ID ObjectID - Author *Signature // never nil - Committer *Signature // never nil - CommitMessage string - Signature *CommitSignature + CommitMessage + + ID ObjectID + Author *Signature // never nil + Committer *Signature // never nil + Signature *CommitSignature Parents []ObjectID // ID strings submoduleCache *ObjectCache[*SubModule] @@ -35,19 +44,28 @@ type CommitSignature struct { Payload string } -// Message returns the commit message. Same as retrieving CommitMessage directly. -func (c *Commit) Message() string { - // FIXME: GIT-COMMIT-MESSAGE-ENCODING: this logic is not right - // * When need to use commit message in templates/database, it should be valid UTF-8 - // * When need to get the original commit message, it should just use "c.CommitMessage" - // It's not easy to refactor at the moment, many templates need to be updated and tested - return c.CommitMessage +func (c *CommitMessage) MessageUTF8() string { + if c.messageUTF8 == nil { + bs := charset.ToUTF8(util.UnsafeStringToBytes(c.MessageRaw), charset.ConvertOpts{ErrorReplacement: []byte{'?'}}) + c.messageUTF8 = new(util.UnsafeBytesToString(bs)) + } + return *c.messageUTF8 } -// Summary returns first line of commit message. -// The string is forced to be valid UTF8 -func (c *Commit) Summary() string { - return strings.ToValidUTF8(strings.Split(strings.TrimSpace(c.CommitMessage), "\n")[0], "?") +func (c *CommitMessage) MessageTitle() string { + if c.messageTitle == nil { + s, _, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n") + c.messageTitle = new(strings.TrimSpace(s)) + } + return *c.messageTitle +} + +func (c *CommitMessage) MessageBody() string { + if c.messageBody == nil { + _, s, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n") + c.messageBody = new(strings.TrimSpace(s)) + } + return *c.messageBody } // ParentID returns oid of n-th parent (0-based index). @@ -247,27 +265,6 @@ func (c *Commit) GetFileContent(filename string, limit int) (string, error) { return string(bytes), nil } -// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') -func (c *Commit) GetBranchName() (string, error) { - cmd := gitcmd.NewCommand("name-rev") - if DefaultFeatures().CheckVersionAtLeast("2.13.0") { - cmd.AddArguments("--exclude", "refs/tags/*") - } - cmd.AddArguments("--name-only", "--no-undefined").AddDynamicArguments(c.ID.String()) - data, _, err := cmd.WithDir(c.repo.Path).RunStdString(c.repo.Ctx) - if err != nil { - // handle special case where git can not describe commit - if strings.Contains(err.Error(), "cannot describe") { - return "", nil - } - - return "", err - } - - // name-rev commitID output will be "master" or "master~12" - return strings.SplitN(strings.TrimSpace(data), "~", 2)[0], nil -} - // GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository. func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) { commitID, _, err := gitcmd.NewCommand("rev-parse"). diff --git a/modules/git/commit_convert_gogit.go b/modules/git/commit_convert_gogit.go index d7b945ed6b..92ad1d21fd 100644 --- a/modules/git/commit_convert_gogit.go +++ b/modules/git/commit_convert_gogit.go @@ -66,7 +66,7 @@ func convertPGPSignature(c *object.Commit) *CommitSignature { func convertCommit(c *object.Commit) *Commit { return &Commit{ ID: ParseGogitHash(c.Hash), - CommitMessage: c.Message, + CommitMessage: CommitMessage{MessageRaw: c.Message}, Committer: &c.Committer, Author: &c.Author, Signature: convertPGPSignature(c), diff --git a/modules/git/commit_info_gogit.go b/modules/git/commit_info_gogit.go index 73227347bc..6174f46f07 100644 --- a/modules/git/commit_info_gogit.go +++ b/modules/git/commit_info_gogit.go @@ -7,6 +7,7 @@ package git import ( "context" + "maps" "path" "github.com/emirpasic/gods/trees/binaryheap" @@ -47,9 +48,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit * return nil, nil, err } - for k, v := range revs2 { - revs[k] = v - } + maps.Copy(revs, revs2) } } else { revs, err = GetLastCommitForPaths(ctx, nil, c, treePath, entryPaths) @@ -201,7 +200,7 @@ heaploop: // Load the parent commits for the one we are currently examining numParents := current.commit.NumParents() var parents []cgobject.CommitNode - for i := 0; i < numParents; i++ { + for i := range numParents { parent, err := current.commit.ParentNode(i) if err != nil { break @@ -273,9 +272,8 @@ heaploop: if len(newRemainingPaths) == 0 { break - } else { - remainingPaths = newRemainingPaths } + remainingPaths = newRemainingPaths } } } diff --git a/modules/git/commit_reader.go b/modules/git/commit_reader.go index eb8f4c6322..ce48d6f319 100644 --- a/modules/git/commit_reader.go +++ b/modules/git/commit_reader.go @@ -92,7 +92,7 @@ func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader) } } - commit.CommitMessage = messageSB.String() + commit.MessageRaw = messageSB.String() if commit.Signature != nil { commit.Signature.Payload = payloadSB.String() } diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go index 0aefb30c95..9fb1539dc9 100644 --- a/modules/git/commit_sha256_test.go +++ b/modules/git/commit_sha256_test.go @@ -95,7 +95,7 @@ signed commit`, commitFromReader.Signature.Payload) commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) assert.NoError(t, err) - commitFromReader.CommitMessage += "\n\n" + commitFromReader.CommitMessage.MessageRaw += "\n\n" commitFromReader.Signature.Payload += "\n\n" assert.Equal(t, commitFromReader, commitFromReader2) } diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index de7b7455eb..a7668e4deb 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -91,7 +91,7 @@ empty commit`, commitFromReader.Signature.Payload) commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) assert.NoError(t, err) - commitFromReader.CommitMessage += "\n\n" + commitFromReader.CommitMessage.MessageRaw += "\n\n" commitFromReader.Signature.Payload += "\n\n" assert.Equal(t, commitFromReader, commitFromReader2) } @@ -154,11 +154,20 @@ ISO-8859-1`, commitFromReader.Signature.Payload) commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) assert.NoError(t, err) - commitFromReader.CommitMessage += "\n\n" + commitFromReader.CommitMessage.MessageRaw += "\n\n" commitFromReader.Signature.Payload += "\n\n" assert.Equal(t, commitFromReader, commitFromReader2) } +func TestCommitMessageSanitizesInvalidUTF8(t *testing.T) { + commit := &Commit{ + CommitMessage: CommitMessage{MessageRaw: "title \xff\n\n\n\nbody \xff\n\n\n"}, + } + assert.Equal(t, "title ÿ", commit.MessageTitle()) + assert.Equal(t, "body ÿ", commit.MessageBody()) + assert.Equal(t, "title ÿ\n\n\n\nbody ÿ\n\n\n", commit.MessageUTF8()) +} + func TestHasPreviousCommit(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") diff --git a/modules/git/foreachref/parser.go b/modules/git/foreachref/parser.go index 91868076b4..874b3744c3 100644 --- a/modules/git/foreachref/parser.go +++ b/modules/git/foreachref/parser.go @@ -111,8 +111,6 @@ func (p *Parser) parseRef(refBlock string) (map[string]string, error) { len(fields), len(p.format.fieldNames)) } for i, field := range fields { - field = strings.TrimSpace(field) - var fieldKey string var fieldVal string before, after, ok := strings.Cut(field, " ") diff --git a/modules/git/foreachref/parser_test.go b/modules/git/foreachref/parser_test.go index 7a37ced356..fc1c8e7cf6 100644 --- a/modules/git/foreachref/parser_test.go +++ b/modules/git/foreachref/parser_test.go @@ -116,12 +116,12 @@ func TestParser(t *testing.T) { }, { "refname:short": "v0.0.2", - "contents": "Update CI config (#651)", + "contents": "Update CI config (#651)\n\n", "author": "John Doe 1521643174 +0000", }, { "refname:short": "v0.0.3", - "contents": "Fixed code sample for bash completion (#687)", + "contents": "Fixed code sample for bash completion (#687)\n\n", "author": "Foo Baz 1524836750 +0200", }, }, diff --git a/modules/git/git.go b/modules/git/git.go index 2df83f9843..69eb07d1f0 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.go b/modules/git/gitcmd/command.go index e0393799a2..ca74c3380a 100644 --- a/modules/git/gitcmd/command.go +++ b/modules/git/gitcmd/command.go @@ -57,14 +57,12 @@ type Command struct { } func logArgSanitize(arg string) string { - if strings.Contains(arg, "://") && strings.Contains(arg, "@") { - return util.SanitizeCredentialURLs(arg) - } else if filepath.IsAbs(arg) { + if filepath.IsAbs(arg) { base := filepath.Base(arg) dir := filepath.Dir(arg) return ".../" + filepath.Join(filepath.Base(dir), base) } - return arg + return util.SanitizeCredentialURLs(arg) } func (c *Command) LogString() string { diff --git a/modules/git/gitcmd/command_test.go b/modules/git/gitcmd/command_test.go index 6e4214d995..1a9bfe7d75 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() @@ -109,7 +109,10 @@ func TestCommandString(t *testing.T) { assert.Equal(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString()) cmd = NewCommand("url: https://a:b@c/", "/root/dir-a/dir-b") - assert.Equal(t, cmd.prog+` "url: https://sanitized-credential@c/" .../dir-a/dir-b`, cmd.LogString()) + assert.Equal(t, cmd.prog+` "url: https://(masked)@c/" .../dir-a/dir-b`, cmd.LogString()) + + cmd = NewCommand("url: a:b@c/", "/root/dir-a/dir-b") + assert.Equal(t, cmd.prog+` "url: (masked)@c/" .../dir-a/dir-b`, cmd.LogString()) } func TestRunStdError(t *testing.T) { diff --git a/modules/git/gitcmd/error.go b/modules/git/gitcmd/error.go index b674068c40..436f4e18ae 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/git/notes_gogit.go b/modules/git/notes_gogit.go index 340f4d5ccc..b3638820dd 100644 --- a/modules/git/notes_gogit.go +++ b/modules/git/notes_gogit.go @@ -9,6 +9,7 @@ import ( "context" "fmt" "io" + "strings" "code.gitea.io/gitea/modules/log" @@ -30,7 +31,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) } remainingCommitID := commitID - path := "" + var path strings.Builder currentTree, err := notes.Tree.gogitTreeObject() if err != nil { return fmt.Errorf("unable to get tree object for notes commit %q: %w", notes.ID.String(), err) @@ -41,17 +42,17 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) for len(remainingCommitID) > 2 { file, err = currentTree.File(remainingCommitID) if err == nil { - path += remainingCommitID + path.WriteString(remainingCommitID) break } if err == object.ErrFileNotFound { currentTree, err = currentTree.Tree(remainingCommitID[0:2]) - path += remainingCommitID[0:2] + "/" + path.WriteString(remainingCommitID[0:2] + "/") remainingCommitID = remainingCommitID[2:] } if err != nil { if err == object.ErrDirectoryNotFound { - return ErrNotExist{ID: remainingCommitID, RelPath: path} + return ErrNotExist{ID: remainingCommitID, RelPath: path.String()} } log.Error("Unable to find git note corresponding to the commit %q. Error: %v", commitID, err) return err @@ -83,12 +84,12 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) return err } - lastCommits, err := GetLastCommitForPaths(ctx, nil, commitNode, "", []string{path}) + lastCommits, err := GetLastCommitForPaths(ctx, nil, commitNode, "", []string{path.String()}) if err != nil { - log.Error("Unable to get the commit for the path %q. Error: %v", path, err) + log.Error("Unable to get the commit for the path %q. Error: %v", path.String(), err) return err } - note.Commit = lastCommits[path] + note.Commit = lastCommits[path.String()] return nil } diff --git a/modules/git/object_id_test.go b/modules/git/object_id_test.go index 03d0c85d87..213a0cd341 100644 --- a/modules/git/object_id_test.go +++ b/modules/git/object_id_test.go @@ -22,4 +22,6 @@ func TestIsValidSHAPattern(t *testing.T) { assert.Equal(t, "2e65efe2a145dda7ee51d1741299f848e5bf752e", ComputeBlobHash(Sha1ObjectFormat, []byte("a")).String()) assert.Equal(t, "473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813", ComputeBlobHash(Sha256ObjectFormat, nil).String()) assert.Equal(t, "eb337bcee2061c5313c9a1392116b6c76039e9e30d71467ae359b36277e17dc7", ComputeBlobHash(Sha256ObjectFormat, []byte("a")).String()) + assert.True(t, IsEmptyCommitID("")) + assert.True(t, IsEmptyCommitID("0000000000000000000000000000000000000000")) } diff --git a/modules/git/parse_treeentry.go b/modules/git/parse_treeentry.go index d46cd3344d..4a00cc1fb1 100644 --- a/modules/git/parse_treeentry.go +++ b/modules/git/parse_treeentry.go @@ -5,10 +5,7 @@ package git import ( "bytes" - "fmt" "io" - - "code.gitea.io/gitea/modules/log" ) // ParseTreeEntries parses the output of a `git ls-tree -l` command. @@ -46,15 +43,14 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { return entries, nil } +var _ = catBatchParseTreeEntries // bypass "unused" lint because it is only used by "nogogit" + func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd BufferedReader, sz int64) ([]*TreeEntry, error) { - fnameBuf := make([]byte, 4096) - modeBuf := make([]byte, 40) - shaBuf := make([]byte, objectFormat.FullLength()) entries := make([]*TreeEntry, 0, 10) loop: for sz > 0 { - mode, fname, sha, count, err := ParseCatFileTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf) + mode, fname, objID, count, err := ParseCatFileTreeLine(objectFormat, rd) if err != nil { if err == io.EOF { break loop @@ -64,25 +60,9 @@ loop: sz -= int64(count) entry := new(TreeEntry) entry.ptree = ptree - - switch string(mode) { - case "100644": - entry.entryMode = EntryModeBlob - case "100755": - entry.entryMode = EntryModeExec - case "120000": - entry.entryMode = EntryModeSymlink - case "160000": - entry.entryMode = EntryModeCommit - case "40000", "40755": // git uses 40000 for tree object, but some users may get 40755 for unknown reasons - entry.entryMode = EntryModeTree - default: - log.Debug("Unknown mode: %v", string(mode)) - return nil, fmt.Errorf("unknown mode: %v", string(mode)) - } - - entry.ID = objectFormat.MustID(sha) - entry.name = string(fname) + entry.entryMode = mode + entry.ID = objID + entry.name = fname entries = append(entries, entry) } if _, err := rd.Discard(1); err != nil { diff --git a/modules/git/parse_treeentry_test.go b/modules/git/parse_treeentry_test.go index 4223cbb3d7..5b81b49edd 100644 --- a/modules/git/parse_treeentry_test.go +++ b/modules/git/parse_treeentry_test.go @@ -4,6 +4,9 @@ package git import ( + "bufio" + "io" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -100,3 +103,31 @@ func TestParseTreeEntriesInvalid(t *testing.T) { assert.Error(t, err) assert.Empty(t, entries) } + +func TestParseCatFileTreeLine(t *testing.T) { + input := "100644 looooooooooooooooooooooooong-file-name.txt\x0012345678901234567890" + input += "40755 some-directory\x00abcdefg123abcdefg123" + + var readCount int + + buf := bufio.NewReaderSize(strings.NewReader(input), 20) // NewReaderSize has a limit: min buffer size = 16 + mode, name, objID, n, err := ParseCatFileTreeLine(Sha1ObjectFormat, buf) + readCount += n + assert.NoError(t, err) + assert.Equal(t, EntryModeBlob, mode) + assert.Equal(t, "looooooooooooooooooooooooong-file-name.txt", name) + assert.Equal(t, "12345678901234567890", string(objID.RawValue())) + + mode, name, objID, n, err = ParseCatFileTreeLine(Sha1ObjectFormat, buf) + readCount += n + assert.NoError(t, err) + assert.Equal(t, EntryModeTree, mode) + assert.Equal(t, "some-directory", name) + assert.Equal(t, "abcdefg123abcdefg123", string(objID.RawValue())) + + assert.Equal(t, len(input), readCount) + + _, _, _, n, err = ParseCatFileTreeLine(Sha1ObjectFormat, buf) + assert.ErrorIs(t, err, io.EOF) + assert.Zero(t, n) +} diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go index 91bda0d0e5..b4a4aff4ed 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -8,10 +8,8 @@ package pipeline import ( "bufio" "bytes" - "encoding/hex" "io" "sort" - "strings" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" @@ -46,10 +44,6 @@ func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader trees := []string{} paths := []string{} - fnameBuf := make([]byte, 4096) - modeBuf := make([]byte, 40) - workingShaBuf := make([]byte, objectID.Type().FullLength()/2) - for scan.Scan() { // Get the next commit ID commitID := scan.Text() @@ -93,23 +87,23 @@ func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader case "tree": var n int64 for n < info.Size { - mode, fname, binObjectID, count, err := git.ParseCatFileTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf) + mode, fname, shaID, count, err := git.ParseCatFileTreeLine(objectID.Type(), batchReader) if err != nil { return nil, err } n += int64(count) - if bytes.Equal(binObjectID, objectID.RawValue()) { + if bytes.Equal(shaID.RawValue(), objectID.RawValue()) { result := LFSResult{ - Name: curPath + string(fname), + Name: curPath + fname, SHA: curCommit.ID.String(), - Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0], + Summary: curCommit.MessageTitle(), When: curCommit.Author.When, ParentHashes: curCommit.Parents, } - resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result - } else if string(mode) == git.EntryModeTree.String() { - trees = append(trees, hex.EncodeToString(binObjectID)) - paths = append(paths, curPath+string(fname)+"/") + resultsMap[curCommit.ID.String()+":"+curPath+fname] = &result + } else if mode == git.EntryModeTree { + trees = append(trees, shaID.String()) + paths = append(paths, curPath+fname+"/") } } if _, err := batchReader.Discard(1); err != nil { diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index c10f73690c..8ba3f83386 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -362,39 +362,6 @@ func (repo *Repository) CommitsBetweenLimit(last, before *Commit, limit, skip in return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) } -// CommitsBetweenNotBase returns a list that contains commits between [before, last), excluding commits in baseBranch. -// If before is detached (removed by reset + push) it is not included. -func (repo *Repository) CommitsBetweenNotBase(last, before *Commit, baseBranch string) ([]*Commit, error) { - var stdout []byte - var err error - if before == nil { - stdout, _, err = gitcmd.NewCommand("rev-list"). - AddDynamicArguments(last.ID.String()). - AddOptionValues("--not", baseBranch). - WithDir(repo.Path). - RunStdBytes(repo.Ctx) - } else { - stdout, _, err = gitcmd.NewCommand("rev-list"). - AddDynamicArguments(before.ID.String()+".."+last.ID.String()). - AddOptionValues("--not", baseBranch). - WithDir(repo.Path). - RunStdBytes(repo.Ctx) - 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... - stdout, _, err = gitcmd.NewCommand("rev-list"). - AddDynamicArguments(before.ID.String(), last.ID.String()). - AddOptionValues("--not", baseBranch). - WithDir(repo.Path). - RunStdBytes(repo.Ctx) - } - } - if err != nil { - return nil, err - } - return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) -} - // CommitsBetweenIDs return commits between twoe commits func (repo *Repository) CommitsBetweenIDs(last, before string) ([]*Commit, error) { lastCommit, err := repo.GetCommit(last) diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 3f7883ab14..ea56a1375d 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -54,7 +54,7 @@ func TestGetTagCommitWithSignature(t *testing.T) { assert.NotNil(t, commit) assert.NotNil(t, commit.Signature) // test that signature is not in message - assert.Equal(t, "signed-commit\n", commit.CommitMessage) + assert.Equal(t, "signed-commit\n", commit.CommitMessage.MessageRaw) } func TestGetCommitWithBadCommitID(t *testing.T) { diff --git a/modules/git/repo_commitgraph_gogit.go b/modules/git/repo_commitgraph_gogit.go index c0082b62c8..aebb9981e2 100644 --- a/modules/git/repo_commitgraph_gogit.go +++ b/modules/git/repo_commitgraph_gogit.go @@ -17,21 +17,21 @@ import ( ) // CommitNodeIndex returns the index for walking commit graph -func (r *Repository) CommitNodeIndex() (cgobject.CommitNodeIndex, *os.File) { - indexPath := filepath.Join(r.Path, "objects", "info", "commit-graph") +func (repo *Repository) CommitNodeIndex() (cgobject.CommitNodeIndex, *os.File) { + indexPath := filepath.Join(repo.Path, "objects", "info", "commit-graph") file, err := os.Open(indexPath) if err == nil { var index commitgraph.Index index, err = commitgraph.OpenFileIndex(file) if err == nil { - return cgobject.NewGraphCommitNodeIndex(index, r.gogitRepo.Storer), file + return cgobject.NewGraphCommitNodeIndex(index, repo.gogitRepo.Storer), file } } if !os.IsNotExist(err) { - gitealog.Warn("Unable to read commit-graph for %s: %v", r.Path, err) + gitealog.Warn("Unable to read commit-graph for %s: %v", repo.Path, err) } - return cgobject.NewObjectCommitNodeIndex(r.gogitRepo.Storer), nil + return cgobject.NewObjectCommitNodeIndex(repo.gogitRepo.Storer), nil } diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 2599236ae0..862b370890 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -176,15 +176,14 @@ func parseTagRef(ref map[string]string) (tag *Tag, err error) { } tag.Tagger = parseSignatureFromCommitLine(ref["creator"]) - tag.Message = ref["contents"] + tag.MessageRaw = ref["contents"] // strip any signature if present in contents field - _, tag.Message, _ = parsePayloadSignature(util.UnsafeStringToBytes(tag.Message), 0) + _, tag.MessageRaw, _ = parsePayloadSignature(util.UnsafeStringToBytes(tag.MessageRaw), 0) // annotated tag with GPG signature if tag.Type == "tag" && ref["contents:signature"] != "" { - payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n", - tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message)) + payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s", tag.Object, tag.Name, ref["creator"], tag.MessageRaw) tag.Signature = &CommitSignature{ Signature: ref["contents:signature"], Payload: payload, diff --git a/modules/git/repo_tag_gogit.go b/modules/git/repo_tag_gogit.go index 878ab55bf2..a3e25f3899 100644 --- a/modules/git/repo_tag_gogit.go +++ b/modules/git/repo_tag_gogit.go @@ -64,12 +64,12 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { return nil, err } tag := &Tag{ - Name: name, - ID: tagID, - Object: commitID, - Type: tp, - Tagger: commit.Committer, - Message: commit.Message(), + Name: name, + ID: tagID, + Object: commitID, + Type: tp, + Tagger: commit.Committer, + CommitMessage: CommitMessage{MessageRaw: commit.CommitMessage.MessageRaw}, } repo.tagCache.Set(tagID.String(), tag) @@ -86,12 +86,12 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { } tag := &Tag{ - Name: name, - ID: tagID, - Object: commitID.Type().MustID(gogitTag.Target[:]), - Type: tp, - Tagger: &gogitTag.Tagger, - Message: gogitTag.Message, + Name: name, + ID: tagID, + Object: commitID.Type().MustID(gogitTag.Target[:]), + Type: tp, + Tagger: &gogitTag.Tagger, + CommitMessage: CommitMessage{MessageRaw: gogitTag.Message}, } repo.tagCache.Set(tagID.String(), tag) diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go index a9ac040821..053c633662 100644 --- a/modules/git/repo_tag_nogogit.go +++ b/modules/git/repo_tag_nogogit.go @@ -71,12 +71,12 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { return nil, err } tag := &Tag{ - Name: name, - ID: tagID, - Object: commitID, - Type: tp, - Tagger: commit.Committer, - Message: commit.Message(), + Name: name, + ID: tagID, + Object: commitID, + Type: tp, + Tagger: commit.Committer, + CommitMessage: commit.CommitMessage, } repo.tagCache.Set(tagID.String(), tag) diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index e6f8e75a0e..50c6ab5036 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -211,13 +211,13 @@ func TestRepository_parseTagRef(t *testing.T) { }, want: &Tag{ - Name: "v1.9.1", - ID: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"), - Object: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"), - Type: "commit", - Tagger: parseSignatureFromCommitLine("Foo Bar 1565789218 +0300"), - Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n", - Signature: nil, + Name: "v1.9.1", + ID: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"), + Object: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"), + Type: "commit", + Tagger: parseSignatureFromCommitLine("Foo Bar 1565789218 +0300"), + CommitMessage: CommitMessage{MessageRaw: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n"}, + Signature: nil, }, }, @@ -240,13 +240,13 @@ func TestRepository_parseTagRef(t *testing.T) { }, want: &Tag{ - Name: "v0.0.1", - ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"), - Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"), - Type: "tag", - Tagger: parseSignatureFromCommitLine("Foo Bar 1565789218 +0300"), - Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n", - Signature: nil, + Name: "v0.0.1", + ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"), + Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"), + Type: "tag", + Tagger: parseSignatureFromCommitLine("Foo Bar 1565789218 +0300"), + CommitMessage: CommitMessage{MessageRaw: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n"}, + Signature: nil, }, }, @@ -263,6 +263,7 @@ func TestRepository_parseTagRef(t *testing.T) { * add changelog of v1.9.1 * Update CHANGELOG.md + -----BEGIN PGP SIGNATURE----- aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3 @@ -298,12 +299,12 @@ qbHDASXl }, want: &Tag{ - Name: "v0.0.1", - ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"), - Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"), - Type: "tag", - Tagger: parseSignatureFromCommitLine("Foo Bar 1565789218 +0300"), - Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md", + Name: "v0.0.1", + ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"), + Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"), + Type: "tag", + Tagger: parseSignatureFromCommitLine("Foo Bar 1565789218 +0300"), + CommitMessage: CommitMessage{MessageRaw: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n"}, Signature: &CommitSignature{ Signature: `-----BEGIN PGP SIGNATURE----- diff --git a/modules/git/tag.go b/modules/git/tag.go index 8bf3658d62..232dda7fd9 100644 --- a/modules/git/tag.go +++ b/modules/git/tag.go @@ -12,12 +12,13 @@ import ( // Tag represents a Git tag. type Tag struct { + CommitMessage + Name string ID ObjectID Object ObjectID // The id of this commit object Type string Tagger *Signature - Message string Signature *CommitSignature } @@ -87,7 +88,7 @@ func parseTagData(objectFormat ObjectFormat, data []byte) (*Tag, error) { pos += eol + 1 } payload, msg, sign := parsePayloadSignature(data, pos) - tag.Message = msg + tag.MessageRaw = msg if len(sign) > 0 { tag.Signature = &CommitSignature{Signature: sign, Payload: payload} } diff --git a/modules/git/tag_test.go b/modules/git/tag_test.go index ba02c28946..6a65e359a2 100644 --- a/modules/git/tag_test.go +++ b/modules/git/tag_test.go @@ -28,7 +28,6 @@ tagger Lucas Michot 1484491741 +0100 Object: MustIDFromString("3b114ab800c6432ad42387ccf6bc8d4388a2885a"), Type: "commit", Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))}, - Message: "", Signature: nil, }, }, @@ -43,13 +42,13 @@ o ono`, expected: Tag{ - Name: "", - ID: Sha1ObjectFormat.EmptyObjectID(), - Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc"), - Type: "commit", - Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0).In(time.FixedZone("", 3600))}, - Message: "test message\no\n\nono", - Signature: nil, + Name: "", + ID: Sha1ObjectFormat.EmptyObjectID(), + Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc"), + Type: "commit", + Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0).In(time.FixedZone("", 3600))}, + CommitMessage: CommitMessage{MessageRaw: "test message\no\n\nono"}, + Signature: nil, }, }, { @@ -64,12 +63,12 @@ dummy signature -----END SSH SIGNATURE----- `, expected: Tag{ - Name: "", - ID: Sha1ObjectFormat.EmptyObjectID(), - Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa"), - Type: "commit", - Tagger: &Signature{Name: "dummy user", Email: "dummy-email@example.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))}, - Message: "dummy message", + Name: "", + ID: Sha1ObjectFormat.EmptyObjectID(), + Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa"), + Type: "commit", + Tagger: &Signature{Name: "dummy user", Email: "dummy-email@example.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))}, + CommitMessage: CommitMessage{MessageRaw: "dummy message"}, Signature: &CommitSignature{ Signature: `-----BEGIN SSH SIGNATURE----- dummy signature @@ -93,5 +92,5 @@ dummy message`, tag, err := parseTagData(Sha1ObjectFormat, []byte("type commit\n\nfoo\n-----BEGIN SSH SIGNATURE-----\ncorrupted...")) assert.NoError(t, err) - assert.Equal(t, "foo\n-----BEGIN SSH SIGNATURE-----\ncorrupted...", tag.Message) + assert.Equal(t, "foo\n-----BEGIN SSH SIGNATURE-----\ncorrupted...", tag.CommitMessage.MessageRaw) } diff --git a/modules/git/tree_entry_gogit_test.go b/modules/git/tree_entry_gogit_test.go index ed14b45e9e..7727b08cb9 100644 --- a/modules/git/tree_entry_gogit_test.go +++ b/modules/git/tree_entry_gogit_test.go @@ -21,7 +21,7 @@ func TestEntryGogit(t *testing.T) { EntryModeTree: filemode.Dir, } for emode, fmode := range cases { - assert.EqualValues(t, fmode, entryModeToGogitFileMode(emode)) - assert.EqualValues(t, emode, gogitFileModeToEntryMode(fmode)) + assert.Equal(t, fmode, entryModeToGogitFileMode(emode)) + assert.Equal(t, emode, gogitFileModeToEntryMode(fmode)) } } diff --git a/modules/git/tree_entry_mode.go b/modules/git/tree_entry_mode.go index 2ceba11374..f80f6bdc75 100644 --- a/modules/git/tree_entry_mode.go +++ b/modules/git/tree_entry_mode.go @@ -66,9 +66,10 @@ func ParseEntryMode(mode string) EntryMode { return EntryModeSymlink case "160000": return EntryModeCommit - case "040000": + case "040000", "40000": // leading-zero is optional return EntryModeTree default: + // if the faster path didn't work, try parsing the mode as an integer and masking off the file type bits // git uses 040000 for tree object, but some users may get 040755 from non-standard git implementations m, _ := strconv.ParseInt(mode, 8, 32) modeInt := EntryMode(m) diff --git a/modules/git/tree_entry_test.go b/modules/git/tree_entry_test.go index 3df6eeab68..bd6a5783b2 100644 --- a/modules/git/tree_entry_test.go +++ b/modules/git/tree_entry_test.go @@ -46,7 +46,9 @@ func TestParseEntryMode(t *testing.T) { {"160755", EntryModeCommit}, {"040000", EntryModeTree}, + {"40000", EntryModeTree}, {"040755", EntryModeTree}, + {"40755", EntryModeTree}, {"777777", EntryModeNoEntry}, // invalid mode } diff --git a/modules/gitrepo/commit.go b/modules/gitrepo/commit.go index 0ab17862fe..0a8cb0544c 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/compare.go b/modules/gitrepo/compare.go index 06cf880d99..7e38d33e6f 100644 --- a/modules/gitrepo/compare.go +++ b/modules/gitrepo/compare.go @@ -42,3 +42,30 @@ func GetDivergingCommits(ctx context.Context, repo Repository, baseBranch, targe } return &DivergeObject{Ahead: ahead, Behind: behind}, nil } + +// GetCommitIDsBetweenReverse returns the last commit IDs between two commits in reverse order (from old to new) with limit. +// If the result exceeds the limit, the old commits IDs will be ignored +func GetCommitIDsBetweenReverse(ctx context.Context, repo Repository, startRef, endRef, notRef string, limit int) ([]string, error) { + genCmd := func(reversions ...string) *gitcmd.Command { + cmd := gitcmd.NewCommand("rev-list", "--reverse"). + AddArguments("-n").AddDynamicArguments(strconv.Itoa(limit)). + AddDynamicArguments(reversions...) + if notRef != "" { // --not should be kept as the last parameter of git command, otherwise the result will be wrong + cmd.AddOptionValues("--not", notRef) + } + return cmd + } + stdout, _, err := RunCmdString(ctx, repo, genCmd(startRef+".."+endRef)) + // example git error message: fatal: origin/main..HEAD: no merge base + if err != nil && strings.Contains(err.Stderr(), "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... + stdout, _, err = RunCmdString(ctx, repo, genCmd(startRef, endRef)) + } + if err != nil { + return nil, err + } + + commitIDs := strings.Fields(strings.TrimSpace(stdout)) + return commitIDs, nil +} diff --git a/modules/gitrepo/compare_test.go b/modules/gitrepo/compare_test.go index f8661d9412..91ee32bed5 100644 --- a/modules/gitrepo/compare_test.go +++ b/modules/gitrepo/compare_test.go @@ -4,9 +4,15 @@ package gitrepo import ( + "path/filepath" + "strings" "testing" + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type mockRepository struct { @@ -17,6 +23,32 @@ func (r *mockRepository) RelativePath() string { return r.path } +func TestMergeBaseNoCommonHistory(t *testing.T) { + repoDir := filepath.Join(t.TempDir(), "repo.git") + require.NoError(t, gitcmd.NewCommand("init").AddDynamicArguments(repoDir).Run(t.Context())) + _, _, runErr := gitcmd.NewCommand("fast-import").WithDir(repoDir).WithStdinBytes([]byte(strings.TrimSpace(` +commit refs/heads/branch1 +committer User 1714310400 +0000 +data 12 +First commit +M 100644 inline file1.txt +data 12 +Hello from 1 + +commit refs/heads/branch2 +committer User 1714310400 +0000 +data 13 +Second commit +M 100644 inline file2.txt +data 12 +Hello from 2 +`))).RunStdString(t.Context()) + require.NoError(t, runErr) + mergeBase, err := MergeBase(t.Context(), &mockRepository{path: repoDir}, "branch1", "branch2") + assert.Empty(t, mergeBase) + assert.ErrorIs(t, err, util.ErrNotExist) +} + func TestRepoGetDivergingCommits(t *testing.T) { repo := &mockRepository{path: "repo1_bare"} do, err := GetDivergingCommits(t.Context(), repo, "master", "branch2") @@ -40,3 +72,75 @@ func TestRepoGetDivergingCommits(t *testing.T) { Behind: 2, }, do) } + +func TestGetCommitIDsBetweenReverse(t *testing.T) { + repo := &mockRepository{path: "repo1_bare"} + + // tests raw commit IDs + commitIDs, err := GetCommitIDsBetweenReverse(t.Context(), repo, + "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + "ce064814f4a0d337b333e646ece456cd39fab612", + "", + 100, + ) + assert.NoError(t, err) + assert.Equal(t, []string{ + "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", + "6fbd69e9823458e6c4a2fc5c0f6bc022b2f2acd1", + "37991dec2c8e592043f47155ce4808d4580f9123", + "feaf4ba6bc635fec442f46ddd4512416ec43c2c2", + "ce064814f4a0d337b333e646ece456cd39fab612", + }, commitIDs) + + commitIDs, err = GetCommitIDsBetweenReverse(t.Context(), repo, + "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + "ce064814f4a0d337b333e646ece456cd39fab612", + "6fbd69e9823458e6c4a2fc5c0f6bc022b2f2acd1", + 100, + ) + assert.NoError(t, err) + assert.Equal(t, []string{ + "37991dec2c8e592043f47155ce4808d4580f9123", + "feaf4ba6bc635fec442f46ddd4512416ec43c2c2", + "ce064814f4a0d337b333e646ece456cd39fab612", + }, commitIDs) + + commitIDs, err = GetCommitIDsBetweenReverse(t.Context(), repo, + "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + "ce064814f4a0d337b333e646ece456cd39fab612", + "", + 3, + ) + assert.NoError(t, err) + assert.Equal(t, []string{ + "37991dec2c8e592043f47155ce4808d4580f9123", + "feaf4ba6bc635fec442f46ddd4512416ec43c2c2", + "ce064814f4a0d337b333e646ece456cd39fab612", + }, commitIDs) + + // test branch names instead of raw commit IDs. + commitIDs, err = GetCommitIDsBetweenReverse(t.Context(), repo, + "test", + "master", + "", + 100, + ) + assert.NoError(t, err) + assert.Equal(t, []string{ + "feaf4ba6bc635fec442f46ddd4512416ec43c2c2", + "ce064814f4a0d337b333e646ece456cd39fab612", + }, commitIDs) + + // add notref to exclude test + commitIDs, err = GetCommitIDsBetweenReverse(t.Context(), repo, + "test", + "master", + "test", + 100, + ) + assert.NoError(t, err) + assert.Equal(t, []string{ + "feaf4ba6bc635fec442f46ddd4512416ec43c2c2", + "ce064814f4a0d337b333e646ece456cd39fab612", + }, commitIDs) +} diff --git a/modules/gitrepo/config.go b/modules/gitrepo/config.go index 9be3ef94ae..4940f1c078 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/gitrepo/merge.go b/modules/gitrepo/merge.go index 8d58e21c8d..5198392949 100644 --- a/modules/gitrepo/merge.go +++ b/modules/gitrepo/merge.go @@ -9,13 +9,17 @@ import ( "strings" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/util" ) // MergeBase checks and returns merge base of two commits. func MergeBase(ctx context.Context, repo Repository, baseCommitID, headCommitID string) (string, error) { - mergeBase, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("merge-base"). + mergeBase, stderr, err := RunCmdString(ctx, repo, gitcmd.NewCommand("merge-base"). AddDashesAndList(baseCommitID, headCommitID)) if err != nil { + if gitcmd.IsErrorExitCode(err, 1) && strings.TrimSpace(stderr) == "" { + return "", util.NewNotExistErrorf("merge-base for %s and %s doesn't exist", baseCommitID, headCommitID) + } return "", fmt.Errorf("get merge-base of %s and %s failed: %w", baseCommitID, headCommitID, err) } return strings.TrimSpace(mergeBase), nil diff --git a/modules/glob/glob.go b/modules/glob/glob.go index d4ca77e2ee..387a305af3 100644 --- a/modules/glob/glob.go +++ b/modules/glob/glob.go @@ -7,6 +7,8 @@ import ( "errors" "fmt" "regexp" + "slices" + "strings" "code.gitea.io/gitea/modules/util" ) @@ -18,19 +20,27 @@ type Glob interface { } type globCompiler struct { + regexpQuestion bool + regexpPlus bool + superWildcardRight bool + supportNegative bool + + separators []rune nonSeparatorChars string globPattern []rune regexpPattern string regexp *regexp.Regexp + builder *strings.Builder pos int + negativeFlip bool } // compileChars compiles character class patterns like [abc] or [!abc] -func (g *globCompiler) compileChars() (string, error) { - result := "" +func (g *globCompiler) compileChars() error { + g.builder.WriteByte('[') if g.pos < len(g.globPattern) && g.globPattern[g.pos] == '!' { g.pos++ - result += "^" + g.builder.WriteByte('^') } for g.pos < len(g.globPattern) { @@ -38,81 +48,111 @@ func (g *globCompiler) compileChars() (string, error) { g.pos++ if c == ']' { - return "[" + result + "]", nil + g.builder.WriteByte(']') + return nil } if c == '\\' { if g.pos >= len(g.globPattern) { - return "", errors.New("unterminated character class escape") + return errors.New("unterminated character class escape") } - result += "\\" + string(g.globPattern[g.pos]) + g.builder.WriteByte('\\') + g.builder.WriteRune(g.globPattern[g.pos]) g.pos++ } else { - result += string(c) + g.builder.WriteRune(c) } } - return "", errors.New("unterminated character class") + return errors.New("unterminated character class") } // compile compiles the glob pattern into a regular expression -func (g *globCompiler) compile(subPattern bool) (string, error) { - result := "" - +func (g *globCompiler) compile(subPattern bool) error { for g.pos < len(g.globPattern) { c := g.globPattern[g.pos] g.pos++ if subPattern && c == '}' { - return "(" + result + ")", nil + g.builder.WriteByte(')') + return nil } switch c { case '*': if g.pos < len(g.globPattern) && g.globPattern[g.pos] == '*' { - g.pos++ - result += ".*" // match any sequence of characters + var matchRightSep bool + if g.superWildcardRight { + // check "**/" pattern, then the wildcards should also match the right separator + // e.g.: "**/docs" should match "docs" + var rightRune rune + if g.pos+1 < len(g.globPattern) { + rightRune = g.globPattern[g.pos+1] + } + if slices.Contains(g.separators, rightRune) { + matchRightSep = g.pos-2 < 0 || g.globPattern[g.pos-2] == rightRune + } + } + if matchRightSep { + g.pos += 2 + } else { + g.pos++ + } + g.builder.WriteString(".*") // match any sequence of characters } else { - result += g.nonSeparatorChars + "*" // match any sequence of non-separator characters + g.builder.WriteString(g.nonSeparatorChars) + g.builder.WriteByte('*') // match any sequence of non-separator characters } case '?': - result += g.nonSeparatorChars // match any single non-separator character + if g.regexpQuestion { + g.builder.WriteByte('?') + } else { + g.builder.WriteString(g.nonSeparatorChars) // match any single non-separator character + } + case '+': + if g.regexpPlus { + g.builder.WriteByte('+') + } else { + g.builder.WriteByte('\\') + g.builder.WriteRune(c) + } case '[': - chars, err := g.compileChars() - if err != nil { - return "", err + if err := g.compileChars(); err != nil { + return err } - result += chars case '{': - subResult, err := g.compile(true) - if err != nil { - return "", err + g.builder.WriteByte('(') + if err := g.compile(true); err != nil { + return err } - result += subResult case ',': if subPattern { - result += "|" + g.builder.WriteByte('|') } else { - result += "," + g.builder.WriteByte(',') } case '\\': if g.pos >= len(g.globPattern) { - return "", errors.New("no character to escape") + return errors.New("no character to escape") } - result += "\\" + string(g.globPattern[g.pos]) + g.builder.WriteByte('\\') + g.builder.WriteRune(g.globPattern[g.pos]) g.pos++ - case '.', '+', '^', '$', '(', ')', '|': - result += "\\" + string(c) // escape regexp special characters + case '.', '^', '$', '(', ')', '|': + g.builder.WriteByte('\\') + g.builder.WriteRune(c) // escape regexp special characters default: - result += string(c) + g.builder.WriteRune(c) } } - return result, nil + return nil } -func newGlobCompiler(pattern string, separators ...rune) (Glob, error) { - g := &globCompiler{globPattern: []rune(pattern)} +func initGlobCompiler(g *globCompiler, pattern string, separators []rune) (Glob, error) { + g.globPattern = []rune(pattern) + g.separators = separators + g.builder = new(strings.Builder) // Escape separators for use in character class escapedSeparators := regexp.QuoteMeta(string(separators)) @@ -122,12 +162,18 @@ func newGlobCompiler(pattern string, separators ...rune) (Glob, error) { g.nonSeparatorChars = "." } - compiled, err := g.compile(false) - if err != nil { - return nil, err + if g.supportNegative && len(g.globPattern) > 0 && g.globPattern[0] == '!' { + g.negativeFlip = true + g.pos++ } - g.regexpPattern = "^" + compiled + "$" + g.builder.WriteByte('^') + if err := g.compile(false); err != nil { + return nil, err + } + g.builder.WriteByte('$') + g.regexpPattern = g.builder.String() + g.builder = nil regex, err := regexp.Compile(g.regexpPattern) if err != nil { @@ -139,11 +185,24 @@ func newGlobCompiler(pattern string, separators ...rune) (Glob, error) { } func (g *globCompiler) Match(s string) bool { - return g.regexp.MatchString(s) + ret := g.regexp.MatchString(s) + if g.negativeFlip { + ret = !ret + } + return ret } func Compile(pattern string, separators ...rune) (Glob, error) { - return newGlobCompiler(pattern, separators...) + return initGlobCompiler(&globCompiler{}, pattern, separators) +} + +func CompileWorkflow(pattern string) (Glob, error) { + return initGlobCompiler(&globCompiler{ + regexpQuestion: true, + regexpPlus: true, + superWildcardRight: true, + supportNegative: true, + }, pattern, []rune{'/'}) } func MustCompile(pattern string, separators ...rune) Glob { diff --git a/modules/globallock/globallock.go b/modules/globallock/globallock.go index 24e91881bb..575d41446a 100644 --- a/modules/globallock/globallock.go +++ b/modules/globallock/globallock.go @@ -6,31 +6,39 @@ package globallock import ( "context" "sync" + "sync/atomic" "code.gitea.io/gitea/modules/setting" ) var ( - defaultLocker Locker - initOnce sync.Once - initFunc = func() { - switch setting.GlobalLock.ServiceType { - case "redis": - defaultLocker = NewRedisLocker(setting.GlobalLock.ServiceConnStr) - case "memory": - fallthrough - default: - defaultLocker = NewMemoryLocker() - } - } // define initFunc as a variable to make it possible to change it in tests + defaultLocker atomic.Pointer[Locker] + defaultMutex sync.Mutex ) +func initDefaultLocker() Locker { + switch setting.GlobalLock.ServiceType { + case "redis": + return NewRedisLocker(setting.GlobalLock.ServiceConnStr) + default: // "memory" + return NewMemoryLocker() + } +} + // DefaultLocker returns the default locker. func DefaultLocker() Locker { - initOnce.Do(func() { - initFunc() - }) - return defaultLocker + ptr := defaultLocker.Load() + if ptr == nil { + defaultMutex.Lock() + ptr = defaultLocker.Load() + if ptr == nil { + ptr = new(initDefaultLocker()) + defaultLocker.Store(ptr) + } + defaultMutex.Unlock() + ptr = defaultLocker.Load() + } + return *ptr } // Lock tries to acquire a lock for the given key, it uses the default locker. diff --git a/modules/globallock/globallock_test.go b/modules/globallock/globallock_test.go index 8d55d9f699..7bee4409da 100644 --- a/modules/globallock/globallock_test.go +++ b/modules/globallock/globallock_test.go @@ -5,7 +5,6 @@ package globallock import ( "context" - "os" "sync" "testing" @@ -15,50 +14,13 @@ import ( func TestLockAndDo(t *testing.T) { t.Run("redis", func(t *testing.T) { - url := "redis://127.0.0.1:6379/0" - if os.Getenv("CI") == "" { - // Make it possible to run tests against a local redis instance - url = os.Getenv("TEST_REDIS_URL") - if url == "" { - t.Skip("TEST_REDIS_URL not set and not running in CI") - return - } - } - - oldDefaultLocker := defaultLocker - oldInitFunc := initFunc - defer func() { - defaultLocker = oldDefaultLocker - initFunc = oldInitFunc - if defaultLocker == nil { - initOnce = sync.Once{} - } - }() - - initOnce = sync.Once{} - initFunc = func() { - defaultLocker = NewRedisLocker(url) - } - + locker := newTestRedisLocker(t) + defaultLocker.Store(new(locker)) testLockAndDo(t) - require.NoError(t, defaultLocker.(*redisLocker).Close()) + require.NoError(t, locker.(*redisLocker).Close()) }) t.Run("memory", func(t *testing.T) { - oldDefaultLocker := defaultLocker - oldInitFunc := initFunc - defer func() { - defaultLocker = oldDefaultLocker - initFunc = oldInitFunc - if defaultLocker == nil { - initOnce = sync.Once{} - } - }() - - initOnce = sync.Once{} - initFunc = func() { - defaultLocker = NewMemoryLocker() - } - + defaultLocker.Store(new(NewMemoryLocker())) testLockAndDo(t) }) } @@ -69,13 +31,10 @@ func testLockAndDo(t *testing.T) { ctx := t.Context() count := 0 wg := sync.WaitGroup{} - wg.Add(concurrency) for range concurrency { - go func() { - defer wg.Done() + wg.Go(func() { err := LockAndDo(ctx, "test", func(ctx context.Context) error { count++ - // It's impossible to acquire the lock inner the function ok, err := TryLockAndDo(ctx, "test", func(ctx context.Context) error { assert.Fail(t, "should not acquire the lock") @@ -83,13 +42,11 @@ func testLockAndDo(t *testing.T) { }) assert.False(t, ok) assert.NoError(t, err) - return nil }) - require.NoError(t, err) - }() + assert.NoError(t, err) + }) } - wg.Wait() assert.Equal(t, concurrency, count) diff --git a/modules/globallock/locker_test.go b/modules/globallock/locker_test.go index 14cb0ec388..44c0c0de5e 100644 --- a/modules/globallock/locker_test.go +++ b/modules/globallock/locker_test.go @@ -10,29 +10,30 @@ import ( "testing" "time" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/util" + "github.com/go-redsync/redsync/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func newTestRedisLocker(t *testing.T) Locker { + t.Helper() + redisURL := util.IfZero(os.Getenv("TEST_REDIS_URL"), "redis://127.0.0.1:6379/0") + rl := NewRedisLocker(redisURL).(*redisLocker) + err := rl.conn.Ping(t.Context()).Err() + if err != nil && test.AllowSkipExternalService() { + t.Skip("no redis server for testing, skipped") + } + require.NoError(t, err, "redis error for testing: %v", err) + return rl +} + func TestLocker(t *testing.T) { t.Run("redis", func(t *testing.T) { - url := "redis://127.0.0.1:6379/0" - if os.Getenv("CI") == "" { - // Make it possible to run tests against a local redis instance - url = os.Getenv("TEST_REDIS_URL") - if url == "" { - t.Skip("TEST_REDIS_URL not set and not running in CI") - return - } - } - oldExpiry := redisLockExpiry - redisLockExpiry = 5 * time.Second // make it shorter for testing - defer func() { - redisLockExpiry = oldExpiry - }() - - locker := NewRedisLocker(url) + defer test.MockVariableValue(&redisLockExpiry, 5*time.Second)() // make it shorter for testing + locker := newTestRedisLocker(t) testLocker(t, locker) testRedisLocker(t, locker.(*redisLocker)) require.NoError(t, locker.(*redisLocker).Close()) diff --git a/modules/globallock/redis_locker.go b/modules/globallock/redis_locker.go index 45dc769fd4..057848106f 100644 --- a/modules/globallock/redis_locker.go +++ b/modules/globallock/redis_locker.go @@ -14,6 +14,7 @@ import ( "github.com/go-redsync/redsync/v4" "github.com/go-redsync/redsync/v4/redis/goredis/v9" + "github.com/redis/go-redis/v9" ) const redisLockKeyPrefix = "gitea:globallock:" @@ -23,7 +24,8 @@ const redisLockKeyPrefix = "gitea:globallock:" var redisLockExpiry = 30 * time.Second type redisLocker struct { - rs *redsync.Redsync + conn redis.UniversalClient + rs *redsync.Redsync mutexM sync.Map closed atomic.Bool @@ -33,17 +35,13 @@ type redisLocker struct { var _ Locker = &redisLocker{} func NewRedisLocker(connection string) Locker { + conn := nosql.GetManager().GetRedisClient(connection) l := &redisLocker{ - rs: redsync.New( - goredis.NewPool( - nosql.GetManager().GetRedisClient(connection), - ), - ), + conn: conn, + rs: redsync.New(goredis.NewPool(conn)), } - l.extendWg.Add(1) l.startExtend() - return l } diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go index 51bd5a2334..243026ab90 100644 --- a/modules/graceful/manager.go +++ b/modules/graceful/manager.go @@ -98,7 +98,7 @@ func (g *Manager) RunAtTerminate(terminate func()) { defer g.terminateWaitGroup.Done() defer func() { if err := recover(); err != nil { - log.Critical("PANIC during RunAtTerminate: %v\nStacktrace: %s", err, log.Stack(2)) + log.Error("PANIC during RunAtTerminate: %v\nStacktrace: %s", err, log.Stack(2)) } }() terminate() @@ -113,7 +113,7 @@ func (g *Manager) RunAtShutdown(ctx context.Context, shutdown func()) { func() { defer func() { if err := recover(); err != nil { - log.Critical("PANIC during RunAtShutdown: %v\nStacktrace: %s", err, log.Stack(2)) + log.Error("PANIC during RunAtShutdown: %v\nStacktrace: %s", err, log.Stack(2)) } }() select { diff --git a/modules/graceful/manager_windows.go b/modules/graceful/manager_windows.go index 457768d6ca..9592dd6b39 100644 --- a/modules/graceful/manager_windows.go +++ b/modules/graceful/manager_windows.go @@ -1,5 +1,6 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT + // This code is heavily inspired by the archived gofacebook/gracenet/net.go handler //go:build windows diff --git a/modules/graceful/server_http.go b/modules/graceful/server_http.go index 7c855ac64e..77a2c3b6f8 100644 --- a/modules/graceful/server_http.go +++ b/modules/graceful/server_http.go @@ -12,7 +12,12 @@ import ( func newHTTPServer(network, address, name string, handler http.Handler) (*Server, ServeFunction) { server := NewServer(network, address, name) + protocols := http.Protocols{} + protocols.SetHTTP1(true) + protocols.SetHTTP2(true) // HTTP/2 can only be used when Gitea is configured to use TLS + protocols.SetUnencryptedHTTP2(true) // Allow HTTP/2 without TLS, in case Gitea is behind a reverse proxy httpServer := http.Server{ + Protocols: &protocols, Handler: handler, BaseContext: func(net.Listener) context.Context { return GetManager().HammerContext() }, } diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go index c7416c7a10..dca28588e4 100644 --- a/modules/highlight/highlight.go +++ b/modules/highlight/highlight.go @@ -5,13 +5,9 @@ package highlight import ( - "bufio" "bytes" - "fmt" gohtml "html" "html/template" - "io" - "strings" "sync" "code.gitea.io/gitea/modules/log" @@ -19,11 +15,11 @@ import ( "code.gitea.io/gitea/modules/util" "github.com/alecthomas/chroma/v2" - "github.com/alecthomas/chroma/v2/formatters/html" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/alecthomas/chroma/v2/styles" ) -// don't index files larger than this many bytes for performance purposes +// don't highlight files larger than this many bytes for performance purposes const sizeLimit = 1024 * 1024 type globalVarsType struct { @@ -80,6 +76,10 @@ func UnsafeSplitHighlightedLines(code template.HTML) (ret [][]byte) { } } +func htmlEscape(code string) template.HTML { + return template.HTML(gohtml.EscapeString(code)) +} + // RenderCodeSlowGuess tries to get a lexer by file name and language first, // if not found, it will try to guess the lexer by code content, which is slow (more than several hundreds of milliseconds). func RenderCodeSlowGuess(fileName, language, code string) (output template.HTML, lexer chroma.Lexer, lexerDisplayName string) { @@ -90,7 +90,7 @@ func RenderCodeSlowGuess(fileName, language, code string) (output template.HTML, } if len(code) > sizeLimit { - return template.HTML(template.HTMLEscapeString(code)), nil, "" + return htmlEscape(code), nil, "" } lexer = detectChromaLexerWithAnalyze(fileName, language, util.UnsafeStringToBytes(code)) // it is also slow @@ -99,91 +99,65 @@ func RenderCodeSlowGuess(fileName, language, code string) (output template.HTML, // RenderCodeByLexer returns a HTML version of code string with chroma syntax highlighting classes func RenderCodeByLexer(lexer chroma.Lexer, code string) template.HTML { - formatter := html.New(html.WithClasses(true), - html.WithLineNumbers(false), - html.PreventSurroundingPre(true), + formatter := chromahtml.New(chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(false), + chromahtml.PreventSurroundingPre(true), ) - htmlbuf := bytes.Buffer{} - htmlw := bufio.NewWriter(&htmlbuf) - iterator, err := lexer.Tokenise(nil, code) if err != nil { log.Error("Can't tokenize code: %v", err) - return template.HTML(template.HTMLEscapeString(code)) - } - // style not used for live site but need to pass something - err = formatter.Format(htmlw, globalVars().githubStyles, iterator) - if err != nil { - log.Error("Can't format code: %v", err) - return template.HTML(template.HTMLEscapeString(code)) + return htmlEscape(code) } - _ = htmlw.Flush() - // Chroma will add newlines for certain lexers in order to highlight them properly - // Once highlighted, strip them here, so they don't cause copy/paste trouble in HTML output - return template.HTML(strings.TrimSuffix(htmlbuf.String(), "\n")) + htmlBuf := &bytes.Buffer{} + // style not used for live site but need to pass something + err = formatter.Format(htmlBuf, globalVars().githubStyles, iterator) + if err != nil { + log.Error("Can't format code: %v", err) + return htmlEscape(code) + } + return template.HTML(util.UnsafeBytesToString(htmlBuf.Bytes())) } // RenderFullFile returns a slice of chroma syntax highlighted HTML lines of code and the matched lexer name -func RenderFullFile(fileName, language string, code []byte) ([]template.HTML, string, error) { - if len(code) > sizeLimit { - return RenderPlainText(code), "", nil +func RenderFullFile(fileName, language string, code []byte) ([]template.HTML, string) { + if language == LanguagePlaintext || len(code) > sizeLimit { + return renderPlainText(code), formatLexerName(LanguagePlaintext) } - - formatter := html.New(html.WithClasses(true), - html.WithLineNumbers(false), - html.PreventSurroundingPre(true), - ) - lexer := detectChromaLexerWithAnalyze(fileName, language, code) lexerName := formatLexerName(lexer.Config().Name) - - iterator, err := lexer.Tokenise(nil, string(code)) - if err != nil { - return nil, "", fmt.Errorf("can't tokenize code: %w", err) + rendered := RenderCodeByLexer(lexer, util.UnsafeBytesToString(code)) + unsafeLines := UnsafeSplitHighlightedLines(rendered) + lines := make([]template.HTML, len(unsafeLines)) + for idx, lineBytes := range unsafeLines { + lines[idx] = template.HTML(util.UnsafeBytesToString(lineBytes)) } - - tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens()) - htmlBuf := &bytes.Buffer{} - - lines := make([]template.HTML, 0, len(tokensLines)) - for _, tokens := range tokensLines { - iterator = chroma.Literator(tokens...) - err = formatter.Format(htmlBuf, globalVars().githubStyles, iterator) - if err != nil { - return nil, "", fmt.Errorf("can't format code: %w", err) - } - lines = append(lines, template.HTML(htmlBuf.String())) - htmlBuf.Reset() - } - - return lines, lexerName, nil + return lines, lexerName } -// RenderPlainText returns non-highlighted HTML for code -func RenderPlainText(code []byte) []template.HTML { - r := bufio.NewReader(bytes.NewReader(code)) - m := make([]template.HTML, 0, bytes.Count(code, []byte{'\n'})+1) - for { - content, err := r.ReadString('\n') - if err != nil && err != io.EOF { - log.Error("failed to read string from buffer: %v", err) - break +// renderPlainText returns non-highlighted HTML for code +func renderPlainText(code []byte) []template.HTML { + lines := make([]template.HTML, 0, bytes.Count(code, []byte{'\n'})+1) + pos := 0 + for pos < len(code) { + var content []byte + nextPos := bytes.IndexByte(code[pos:], '\n') + if nextPos == -1 { + content = code[pos:] + pos = len(code) + } else { + content = code[pos : pos+nextPos+1] + pos += nextPos + 1 } - if content == "" && err == io.EOF { - break - } - s := template.HTML(gohtml.EscapeString(content)) - m = append(m, s) + lines = append(lines, htmlEscape(util.UnsafeBytesToString(content))) } - return m + return lines } func formatLexerName(name string) string { - if name == "fallback" { + if name == LanguagePlaintext || name == chromaLexerFallback { return "Plaintext" } - return util.ToTitleCaseNoLower(name) } diff --git a/modules/highlight/highlight_test.go b/modules/highlight/highlight_test.go index d026210475..211132b255 100644 --- a/modules/highlight/highlight_test.go +++ b/modules/highlight/highlight_test.go @@ -118,8 +118,7 @@ c=2 for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - out, lexerName, err := RenderFullFile(tt.name, "", []byte(tt.code)) - assert.NoError(t, err) + out, lexerName := RenderFullFile(tt.name, "", []byte(tt.code)) assert.Equal(t, tt.want, out) assert.Equal(t, tt.lexerName, lexerName) }) @@ -182,7 +181,7 @@ c=2`), for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - out := RenderPlainText([]byte(tt.code)) + out := renderPlainText([]byte(tt.code)) assert.Equal(t, tt.want, out) }) } diff --git a/modules/highlight/lexerdetect.go b/modules/highlight/lexerdetect.go index 5d98578f35..5b3348761c 100644 --- a/modules/highlight/lexerdetect.go +++ b/modules/highlight/lexerdetect.go @@ -16,7 +16,11 @@ import ( "github.com/go-enry/go-enry/v2" ) -const mapKeyLowerPrefix = "lower/" +const ( + mapKeyLowerPrefix = "lower/" + LanguagePlaintext = "plaintext" + chromaLexerFallback = "fallback" +) // chromaLexers is fully managed by us to do fast lookup for chroma lexers by file name or language name // Don't use lexers.Get because it is very slow in many cases (iterate all rules, filepath glob match, etc.) @@ -54,6 +58,7 @@ var chromaLexers = sync.OnceValue(func() (ret struct { ".inc": "PHP", // ObjectPascal, POVRay, SourcePawn, PHTML ".m": "Objective-C", // Matlab, Mathematica, Mason ".mc": "Mason", // MonkeyC + ".mod": "AMPL", // Modula-2 ".network": "SYSTEMD", // INI ".php": "PHP", // PHTML ".php3": "PHP", // PHTML diff --git a/modules/hostmatcher/hostmatcher.go b/modules/hostmatcher/hostmatcher.go index 15c6371422..044dba679a 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/htmlutil/html.go b/modules/htmlutil/html.go index 8dbfe0c22e..5db1d6404a 100644 --- a/modules/htmlutil/html.go +++ b/modules/htmlutil/html.go @@ -83,3 +83,34 @@ func HTMLPrintTag(w io.Writer, tag template.HTML, attrs map[string]string) (writ written += n return written, err } + +func EscapeString(s string) template.HTML { + return template.HTML(template.HTMLEscapeString(s)) +} + +type HTMLBuilder struct { + sb strings.Builder +} + +func (b *HTMLBuilder) WriteString(s string) *HTMLBuilder { + b.sb.WriteString(template.HTMLEscapeString(s)) + return b +} + +func (b *HTMLBuilder) WriteHTML(s template.HTML) *HTMLBuilder { + b.sb.WriteString(string(s)) + return b +} + +func (b *HTMLBuilder) WriteFormat(fmt template.HTML, args ...any) *HTMLBuilder { + _, _ = HTMLPrintf(&b.sb, fmt, args...) + return b +} + +func (b *HTMLBuilder) HTMLString() template.HTML { + return template.HTML(b.sb.String()) +} + +func (b *HTMLBuilder) String() string { + return b.sb.String() +} diff --git a/modules/htmlutil/html_test.go b/modules/htmlutil/html_test.go index 22258ce59d..a1ab0a6a49 100644 --- a/modules/htmlutil/html_test.go +++ b/modules/htmlutil/html_test.go @@ -22,3 +22,10 @@ func TestHTMLFormat(t *testing.T) { assert.Equal(t, template.HTML("<>"), HTMLFormat("%s", template.URL("<>"))) assert.Equal(t, template.HTML("&StringMethod &StringMethod"), HTMLFormat("%s %s", testStringer{}, &testStringer{})) } + +func TestHTMLBuilder(t *testing.T) { + b := &HTMLBuilder{} + b.WriteString("<").WriteHTML("
").WriteFormat("%s%s", ">", EscapeString(">")) + assert.Equal(t, "<
>>", b.String()) + assert.Equal(t, template.HTML("<
>>"), b.HTMLString()) +} diff --git a/modules/httplib/content_disposition.go b/modules/httplib/content_disposition.go new file mode 100644 index 0000000000..da23dae221 --- /dev/null +++ b/modules/httplib/content_disposition.go @@ -0,0 +1,65 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package httplib + +import ( + "mime" + "strings" + + "code.gitea.io/gitea/modules/setting" +) + +type ContentDispositionType string + +const ( + ContentDispositionInline ContentDispositionType = "inline" + ContentDispositionAttachment ContentDispositionType = "attachment" +) + +func needsEncodingRune(b rune) bool { + return (b < ' ' || b > '~') && b != '\t' +} + +// getSafeName replaces all invalid chars in the filename field by underscore +func getSafeName(s string) (_ string, needsEncoding bool) { + var out strings.Builder + for _, b := range s { + if needsEncodingRune(b) { + needsEncoding = true + out.WriteRune('_') + } else { + out.WriteRune(b) + } + } + return out.String(), needsEncoding +} + +func EncodeContentDispositionAttachment(filename string) string { + return encodeContentDisposition(ContentDispositionAttachment, filename) +} + +func EncodeContentDispositionInline(filename string) string { + return encodeContentDisposition(ContentDispositionInline, filename) +} + +// encodeContentDisposition encodes a correct Content-Disposition Header +func encodeContentDisposition(t ContentDispositionType, filename string) string { + safeFilename, needsEncoding := getSafeName(filename) + result := mime.FormatMediaType(string(t), map[string]string{"filename": safeFilename}) + // No need for the utf8 encoding + if !needsEncoding { + return result + } + utf8Result := mime.FormatMediaType(string(t), map[string]string{"filename": filename}) + + // The mime package might have unexpected results in other go versions + // Make tests instance fail, otherwise use the default behavior of the go mime package + if !strings.HasPrefix(result, string(t)+"; filename=") || !strings.HasPrefix(utf8Result, string(t)+"; filename*=") { + setting.PanicInDevOrTesting("Unexpected mime package result %s", result) + return utf8Result + } + + encodedFileName := strings.TrimPrefix(utf8Result, string(t)) + return result + encodedFileName +} diff --git a/modules/httplib/content_disposition_test.go b/modules/httplib/content_disposition_test.go new file mode 100644 index 0000000000..bf5040e107 --- /dev/null +++ b/modules/httplib/content_disposition_test.go @@ -0,0 +1,64 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package httplib + +import ( + "mime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestContentDisposition(t *testing.T) { + type testEntry struct { + disposition ContentDispositionType + filename string + header string + } + table := []testEntry{ + {disposition: ContentDispositionInline, filename: "test.txt", header: "inline; filename=test.txt"}, + {disposition: ContentDispositionInline, filename: "test❌.txt", header: "inline; filename=test_.txt; filename*=utf-8''test%E2%9D%8C.txt"}, + {disposition: ContentDispositionInline, filename: "test ❌.txt", header: "inline; filename=\"test _.txt\"; filename*=utf-8''test%20%E2%9D%8C.txt"}, + {disposition: ContentDispositionInline, filename: "\"test.txt", header: "inline; filename=\"\\\"test.txt\""}, + {disposition: ContentDispositionInline, filename: "hello\tworld.txt", header: "inline; filename=\"hello\tworld.txt\""}, + {disposition: ContentDispositionAttachment, filename: "hello\tworld.txt", header: "attachment; filename=\"hello\tworld.txt\""}, + {disposition: ContentDispositionAttachment, filename: "hello\nworld.txt", header: "attachment; filename=hello_world.txt; filename*=utf-8''hello%0Aworld.txt"}, + {disposition: ContentDispositionAttachment, filename: "hello\rworld.txt", header: "attachment; filename=hello_world.txt; filename*=utf-8''hello%0Dworld.txt"}, + } + + // Check the needsEncodingRune replacer ranges except tab that is checked above + // Any change in behavior should fail here + for c := ' '; !needsEncodingRune(c); c++ { + var header string + switch { + case strings.ContainsAny(string(c), ` (),/:;<=>?@[]`): + header = "inline; filename=\"hello" + string(c) + "world.txt\"" + case strings.ContainsAny(string(c), `"\`): + // This document advises against for backslash in quoted form: + // https://datatracker.ietf.org/doc/html/rfc6266#appendix-D + // However the mime package is not generating the filename* in this scenario + header = "inline; filename=\"hello\\" + string(c) + "world.txt\"" + default: + header = "inline; filename=hello" + string(c) + "world.txt" + } + table = append(table, testEntry{ + disposition: ContentDispositionInline, + filename: "hello" + string(c) + "world.txt", + header: header, + }) + } + + for _, entry := range table { + t.Run(string(entry.disposition)+"_"+entry.filename, func(t *testing.T) { + encoded := encodeContentDisposition(entry.disposition, entry.filename) + assert.Equal(t, entry.header, encoded) + disposition, params, err := mime.ParseMediaType(encoded) + require.NoError(t, err) + assert.Equal(t, string(entry.disposition), disposition) + assert.Equal(t, entry.filename, params["filename"]) + }) + } +} diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index fc7edc36c4..d51c938bf0 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -8,10 +8,9 @@ import ( "errors" "fmt" "io" + "io/fs" "net/http" - "net/url" "path" - "path/filepath" "strconv" "strings" "time" @@ -27,18 +26,55 @@ import ( ) type ServeHeaderOptions struct { - ContentType string // defaults to "application/octet-stream" - ContentTypeCharset string - ContentLength *int64 - Disposition string // defaults to "attachment" + ContentType string // defaults to "application/octet-stream" + ContentLength *int64 + Filename string - CacheIsPublic bool - CacheDuration time.Duration // defaults to 5 minutes - LastModified time.Time + ContentDisposition ContentDispositionType + + CacheIsPublic bool + CacheDuration time.Duration // defaults to 5 minutes + LastModified time.Time +} + +const ( + // Disable JS execution on the same origin, since we serve the file from the same origin as Gitea server. + // This rule can be relaxed in the future as long as it is properly sandboxed. + // "style-src" is for SVG inline styles (from Display SVG files as images instead of text #14101) + serveHeaderCspDefault = "default-src 'none'; style-src 'unsafe-inline'; sandbox" + + // No sandbox attribute for PDF as it breaks rendering in at least Safari. + // This should generally be safe as scripts inside PDF can not escape the PDF document. + // See https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion. + // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context + serveHeaderCspPdf = "default-src 'none'; style-src 'unsafe-inline'" + + // For audios and videos, actually it doesn't really need CSP (just like Gitea <= 1.25) + serveHeaderCspAudioVideo = "" +) + +func serveSetHeaderContentRelated(w http.ResponseWriter, contentType string) { + header := w.Header() + contentType = util.IfZero(contentType, typesniffer.MimeTypeApplicationOctetStream) + header.Set("Content-Type", contentType) + header.Set("X-Content-Type-Options", "nosniff") + + csp := serveHeaderCspDefault + if strings.HasPrefix(contentType, "application/pdf") { + csp = serveHeaderCspPdf + } + if strings.HasPrefix(contentType, "video/") || strings.HasPrefix(contentType, "audio/") { + csp = serveHeaderCspAudioVideo + } + if csp != "" { + header.Set("Content-Security-Policy", csp) + } else { + header.Del("Content-Security-Policy") + } } // ServeSetHeaders sets necessary content serve headers -func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { +func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) { header := w.Header() skipCompressionExts := container.SetOf(".gz", ".bz2", ".zip", ".xz", ".zst", ".deb", ".apk", ".jar", ".png", ".jpg", ".webp") @@ -46,29 +82,14 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { w.Header().Add(gzhttp.HeaderNoCompression, "1") } - contentType := typesniffer.MimeTypeApplicationOctetStream - if opts.ContentType != "" { - if opts.ContentTypeCharset != "" { - contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset) - } else { - contentType = opts.ContentType - } - } - header.Set("Content-Type", contentType) - header.Set("X-Content-Type-Options", "nosniff") + serveSetHeaderContentRelated(w, opts.ContentType) if opts.ContentLength != nil { header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10)) } - if opts.Filename != "" { - disposition := opts.Disposition - if disposition == "" { - disposition = "attachment" - } - - backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \" - header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename))) + contentDisposition := util.IfZero(opts.ContentDisposition, ContentDispositionAttachment) + header.Set("Content-Disposition", encodeContentDisposition(contentDisposition, path.Base(opts.Filename))) header.Set("Access-Control-Expose-Headers", "Content-Disposition") } @@ -84,49 +105,40 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { } } -// ServeData download file from io.Reader -func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byte, opts *ServeHeaderOptions) { - // do not set "Content-Length", because the length could only be set by callers, and it needs to support range requests - sniffedType := typesniffer.DetectContentType(mineBuf) - - // the "render" parameter came from year 2016: 638dd24c, it doesn't have clear meaning, so I think it could be removed later - isPlain := sniffedType.IsText() || r.FormValue("render") != "" +func serveSetHeadersByUserContent(w http.ResponseWriter, contentPrefetchBuf []byte, opts ServeHeaderOptions) { + var detectCharset bool if setting.MimeTypeMap.Enabled { - fileExtension := strings.ToLower(filepath.Ext(opts.Filename)) + fileExtension := strings.ToLower(path.Ext(opts.Filename)) opts.ContentType = setting.MimeTypeMap.Map[fileExtension] + detectCharset = strings.HasPrefix(opts.ContentType, "text/") && !strings.Contains(opts.ContentType, "charset=") } if opts.ContentType == "" { + sniffedType := typesniffer.DetectContentType(contentPrefetchBuf) if sniffedType.IsBrowsableBinaryType() { opts.ContentType = sniffedType.GetMimeType() - } else if isPlain { + } else if sniffedType.IsText() { + // intentionally do not render user's HTML content as a page, for safety, and avoid content spamming & abusing opts.ContentType = "text/plain" + detectCharset = true } else { opts.ContentType = typesniffer.MimeTypeApplicationOctetStream } } - if isPlain { - charset, _ := charsetModule.DetectEncoding(mineBuf) - opts.ContentTypeCharset = strings.ToLower(charset) + if detectCharset { + if charset, _ := charsetModule.DetectEncoding(contentPrefetchBuf); charset != "" { + opts.ContentType += "; charset=" + strings.ToLower(charset) + } } - // serve types that can present a security risk with CSP - w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox") - - if sniffedType.IsPDF() { - // no sandbox attribute for PDF as it breaks rendering in at least safari. this - // should generally be safe as scripts inside PDF can not escape the PDF document - // see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion - // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context - w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") - } - - // TODO: UNIFY-CONTENT-DISPOSITION-FROM-STORAGE - opts.Disposition = "inline" - if sniffedType.IsSvgImage() && !setting.UI.SVG.Enabled { - opts.Disposition = "attachment" + if opts.ContentDisposition == "" { + sniffedType := typesniffer.FromContentType(opts.ContentType) + opts.ContentDisposition = ContentDispositionInline + if sniffedType.IsSvgImage() && !setting.UI.SVG.Enabled { + opts.ContentDisposition = ContentDispositionAttachment + } } ServeSetHeaders(w, opts) @@ -134,7 +146,10 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byt const mimeDetectionBufferLen = 1024 -func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, reader io.Reader, opts *ServeHeaderOptions) { +func ServeUserContentByReader(r *http.Request, w http.ResponseWriter, size int64, reader io.Reader, opts ServeHeaderOptions) { + if opts.ContentLength != nil { + panic("do not set ContentLength, use size argument instead") + } buf := make([]byte, mimeDetectionBufferLen) n, err := util.ReadAtMost(reader, buf) if err != nil { @@ -144,7 +159,7 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, re if n >= 0 { buf = buf[:n] } - setServeHeadersByFile(r, w, buf, opts) + serveSetHeadersByUserContent(w, buf, opts) // reset the reader to the beginning reader = io.MultiReader(bytes.NewReader(buf), reader) @@ -198,32 +213,29 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, re partialLength := end - start + 1 w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size)) w.Header().Set("Content-Length", strconv.FormatInt(partialLength, 10)) - if _, err = io.CopyN(io.Discard, reader, start); err != nil { - http.Error(w, "serve content: unable to skip", http.StatusInternalServerError) - return + + if seeker, ok := reader.(io.Seeker); ok { + if _, err = seeker.Seek(start, io.SeekStart); err != nil { + http.Error(w, "serve content: unable to seek", http.StatusInternalServerError) + return + } + } else { + if _, err = io.CopyN(io.Discard, reader, start); err != nil { + http.Error(w, "serve content: unable to skip", http.StatusInternalServerError) + return + } } w.WriteHeader(http.StatusPartialContent) _, _ = io.CopyN(w, reader, partialLength) // just like http.ServeContent, not necessary to handle the error } -func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, modTime *time.Time, reader io.ReadSeeker, opts *ServeHeaderOptions) { - buf := make([]byte, mimeDetectionBufferLen) - n, err := util.ReadAtMost(reader, buf) +func ServeUserContentByFile(r *http.Request, w http.ResponseWriter, file fs.File, opts ServeHeaderOptions) { + info, err := file.Stat() if err != nil { - http.Error(w, "serve content: unable to read", http.StatusInternalServerError) + http.Error(w, "unable to serve file, stat error", http.StatusInternalServerError) return } - if _, err = reader.Seek(0, io.SeekStart); err != nil { - http.Error(w, "serve content: unable to seek", http.StatusInternalServerError) - return - } - if n >= 0 { - buf = buf[:n] - } - setServeHeadersByFile(r, w, buf, opts) - if modTime == nil { - modTime = &time.Time{} - } - http.ServeContent(w, r, opts.Filename, *modTime, reader) + opts.LastModified = info.ModTime() + ServeUserContentByReader(r, w, info.Size(), file, opts) } diff --git a/modules/httplib/serve_test.go b/modules/httplib/serve_test.go index 78b88c9b5f..419085237c 100644 --- a/modules/httplib/serve_test.go +++ b/modules/httplib/serve_test.go @@ -12,11 +12,13 @@ import ( "strings" "testing" + "code.gitea.io/gitea/modules/typesniffer" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestServeContentByReader(t *testing.T) { +func TestServeUserContentByReader(t *testing.T) { data := "0123456789abcdef" test := func(t *testing.T, expectedStatusCode int, expectedContent string) { @@ -27,7 +29,7 @@ func TestServeContentByReader(t *testing.T) { } reader := strings.NewReader(data) w := httptest.NewRecorder() - ServeContentByReader(r, w, int64(len(data)), reader, &ServeHeaderOptions{}) + ServeUserContentByReader(r, w, int64(len(data)), reader, ServeHeaderOptions{}) assert.Equal(t, expectedStatusCode, w.Code) if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK { assert.Equal(t, strconv.Itoa(len(expectedContent)), w.Header().Get("Content-Length")) @@ -58,7 +60,7 @@ func TestServeContentByReader(t *testing.T) { }) } -func TestServeContentByReadSeeker(t *testing.T) { +func TestServeUserContentByFile(t *testing.T) { data := "0123456789abcdef" tmpFile := t.TempDir() + "/test" err := os.WriteFile(tmpFile, []byte(data), 0o644) @@ -76,7 +78,7 @@ func TestServeContentByReadSeeker(t *testing.T) { defer seekReader.Close() w := httptest.NewRecorder() - ServeContentByReadSeeker(r, w, nil, seekReader, &ServeHeaderOptions{}) + ServeUserContentByFile(r, w, seekReader, ServeHeaderOptions{}) assert.Equal(t, expectedStatusCode, w.Code) if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK { assert.Equal(t, strconv.Itoa(len(expectedContent)), w.Header().Get("Content-Length")) @@ -106,3 +108,36 @@ func TestServeContentByReadSeeker(t *testing.T) { test(t, http.StatusPartialContent, data[1:]) }) } + +func TestServeSetHeaderContentRelated(t *testing.T) { + cases := []struct { + contentType string + csp string + }{ + {"", serveHeaderCspDefault}, + {"any", serveHeaderCspDefault}, + {"application/pdf", serveHeaderCspPdf}, + {"application/pdf; other", serveHeaderCspPdf}, + {"audio/mp4", serveHeaderCspAudioVideo}, + {"video/ogg; other", serveHeaderCspAudioVideo}, + {typesniffer.MimeTypeImageSvg, serveHeaderCspDefault}, + } + for _, c := range cases { + w := httptest.NewRecorder() + serveSetHeaderContentRelated(w, c.contentType) + csp := w.Header().Get("Content-Security-Policy") + assert.Equal(t, c.csp, csp, "content-type: %s", c.contentType) + assert.Equal(t, "nosniff", w.Header().Get("X-Content-Type-Options")) // it should always be there + } + + // make sure sandboxed + require.Contains(t, serveHeaderCspDefault, "; sandbox") +} + +func TestServeSetHeaders(t *testing.T) { + w := httptest.NewRecorder() + ServeSetHeaders(w, ServeHeaderOptions{Filename: "foo.zip"}) + assert.Equal(t, "attachment; filename=foo.zip", w.Header().Get("Content-Disposition")) + ServeSetHeaders(w, ServeHeaderOptions{Filename: "foo.zip", ContentDisposition: ContentDispositionInline}) + assert.Equal(t, "inline; filename=foo.zip", w.Header().Get("Content-Disposition")) +} diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go index 9d170528ad..f9a3d4156d 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -18,8 +18,7 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/indexer" "code.gitea.io/gitea/modules/indexer/code/internal" - indexer_internal "code.gitea.io/gitea/modules/indexer/internal" - inner_elasticsearch "code.gitea.io/gitea/modules/indexer/internal/elasticsearch" + es "code.gitea.io/gitea/modules/indexer/internal/elasticsearch" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -28,23 +27,15 @@ import ( "code.gitea.io/gitea/modules/util" "github.com/go-enry/go-enry/v2" - "github.com/olivere/elastic/v7" ) -const ( - esRepoIndexerLatestVersion = 3 - // multi-match-types, currently only 2 types are used - // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types - esMultiMatchTypeBestFields = "best_fields" - esMultiMatchTypePhrasePrefix = "phrase_prefix" -) +const esRepoIndexerLatestVersion = 3 var _ internal.Indexer = &Indexer{} // Indexer implements Indexer interface type Indexer struct { - inner *inner_elasticsearch.Indexer - indexer_internal.Indexer // do not composite inner_elasticsearch.Indexer directly to avoid exposing too much + *es.Indexer } func (b *Indexer) SupportedSearchModes() []indexer.SearchMode { @@ -53,12 +44,7 @@ func (b *Indexer) SupportedSearchModes() []indexer.SearchMode { // NewIndexer creates a new elasticsearch indexer func NewIndexer(url, indexerName string) *Indexer { - inner := inner_elasticsearch.NewIndexer(url, indexerName, esRepoIndexerLatestVersion, defaultMapping) - indexer := &Indexer{ - inner: inner, - Indexer: inner, - } - return indexer + return &Indexer{Indexer: es.NewIndexer(url, indexerName, esRepoIndexerLatestVersion, defaultMapping)} } const ( @@ -138,7 +124,7 @@ const ( }` ) -func (b *Indexer) addUpdate(ctx context.Context, catFileBatch git.CatFileBatch, sha string, update internal.FileUpdate, repo *repo_model.Repository) ([]elastic.BulkableRequest, error) { +func (b *Indexer) addUpdate(ctx context.Context, catFileBatch git.CatFileBatch, sha string, update internal.FileUpdate, repo *repo_model.Repository) ([]es.BulkOp, error) { // Ignore vendored files in code search if setting.Indexer.ExcludeVendored && analyze.IsVendor(update.Filename) { return nil, nil @@ -157,8 +143,9 @@ func (b *Indexer) addUpdate(ctx context.Context, catFileBatch git.CatFileBatch, } } + id := internal.FilenameIndexerID(repo.ID, update.Filename) if size > setting.Indexer.MaxIndexerFileSize { - return []elastic.BulkableRequest{b.addDelete(update.Filename, repo)}, nil + return []es.BulkOp{es.DeleteOp(id)}, nil } info, batchReader, err := catFileBatch.QueryContent(update.BlobSha) @@ -177,33 +164,24 @@ func (b *Indexer) addUpdate(ctx context.Context, catFileBatch git.CatFileBatch, if _, err = batchReader.Discard(1); err != nil { return nil, err } - id := internal.FilenameIndexerID(repo.ID, update.Filename) - return []elastic.BulkableRequest{ - elastic.NewBulkIndexRequest(). - Index(b.inner.VersionedIndexName()). - Id(id). - Doc(map[string]any{ - "repo_id": repo.ID, - "filename": update.Filename, - "content": string(charset.ToUTF8DropErrors(fileContents)), - "commit_id": sha, - "language": analyze.GetCodeLanguage(update.Filename, fileContents), - "updated_at": timeutil.TimeStampNow(), - }), - }, nil + return []es.BulkOp{es.IndexOp(id, map[string]any{ + "repo_id": repo.ID, + "filename": update.Filename, + "content": string(charset.ToUTF8DropErrors(fileContents)), + "commit_id": sha, + "language": analyze.GetCodeLanguage(update.Filename, fileContents), + "updated_at": timeutil.TimeStampNow(), + })}, nil } -func (b *Indexer) addDelete(filename string, repo *repo_model.Repository) elastic.BulkableRequest { - id := internal.FilenameIndexerID(repo.ID, filename) - return elastic.NewBulkDeleteRequest(). - Index(b.inner.VersionedIndexName()). - Id(id) +func (b *Indexer) addDelete(filename string, repo *repo_model.Repository) es.BulkOp { + return es.DeleteOp(internal.FilenameIndexerID(repo.ID, filename)) } // Index will save the index data func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { - reqs := make([]elastic.BulkableRequest, 0) + ops := make([]es.BulkOp, 0) if len(changes.Updates) > 0 { batch, err := gitrepo.NewBatch(ctx, repo) if err != nil { @@ -212,29 +190,25 @@ func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha st defer batch.Close() for _, update := range changes.Updates { - updateReqs, err := b.addUpdate(ctx, batch, sha, update, repo) + updateOps, err := b.addUpdate(ctx, batch, sha, update, repo) if err != nil { return err } - if len(updateReqs) > 0 { - reqs = append(reqs, updateReqs...) + if len(updateOps) > 0 { + ops = append(ops, updateOps...) } } } for _, filename := range changes.RemovedFilenames { - reqs = append(reqs, b.addDelete(filename, repo)) + ops = append(ops, b.addDelete(filename, repo)) } - if len(reqs) > 0 { + if len(ops) > 0 { esBatchSize := 50 - for i := 0; i < len(reqs); i += esBatchSize { - _, err := b.inner.Client.Bulk(). - Index(b.inner.VersionedIndexName()). - Add(reqs[i:min(i+esBatchSize, len(reqs))]...). - Do(ctx) - if err != nil { + for i := 0; i < len(ops); i += esBatchSize { + if err := b.Bulk(ctx, ops[i:min(i+esBatchSize, len(ops))]); err != nil { return err } } @@ -246,33 +220,21 @@ func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha st func (b *Indexer) Delete(ctx context.Context, repoID int64) error { if err := b.doDelete(ctx, repoID); err != nil { // Maybe there is a conflict during the delete operation, so we should retry after a refresh - log.Warn("Deletion of entries of repo %v within index %v was erroneous. Trying to refresh index before trying again", repoID, b.inner.VersionedIndexName(), err) - if err := b.refreshIndex(ctx); err != nil { + log.Warn("Deletion of entries of repo %v within index %v was erroneous: %v. Trying to refresh index before trying again", repoID, b.VersionedIndexName(), err) + if err := b.Refresh(ctx); err != nil { return err } if err := b.doDelete(ctx, repoID); err != nil { - log.Error("Could not delete entries of repo %v within index %v", repoID, b.inner.VersionedIndexName()) + log.Error("Could not delete entries of repo %v within index %v", repoID, b.VersionedIndexName()) return err } } return nil } -func (b *Indexer) refreshIndex(ctx context.Context) error { - if _, err := b.inner.Client.Refresh(b.inner.VersionedIndexName()).Do(ctx); err != nil { - log.Error("Error while trying to refresh index %v", b.inner.VersionedIndexName(), err) - return err - } - - return nil -} - // Delete entries by repoId func (b *Indexer) doDelete(ctx context.Context, repoID int64) error { - _, err := b.inner.Client.DeleteByQuery(b.inner.VersionedIndexName()). - Query(elastic.NewTermsQuery("repo_id", repoID)). - Do(ctx) - return err + return b.DeleteByQuery(ctx, es.TermsQuery("repo_id", repoID)) } // contentMatchIndexPos find words positions for start and the following end on content. It will @@ -291,10 +253,10 @@ func contentMatchIndexPos(content, start, end string) (int, int) { return startIdx, (startIdx + len(start) + endIdx + len(end)) - 9 // remove the length since we give Content the original data } -func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { +func convertResult(searchResult *es.SearchResponse, kw string, pageSize int) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { hits := make([]*internal.SearchResult, 0, pageSize) - for _, hit := range searchResult.Hits.Hits { - repoID, fileName := internal.ParseIndexerID(hit.Id) + for _, hit := range searchResult.Hits { + repoID, fileName := internal.ParseIndexerID(hit.ID) res := make(map[string]any) if err := json.Unmarshal(hit.Source, &res); err != nil { return 0, nil, nil, err @@ -333,111 +295,111 @@ func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) }) } - return searchResult.TotalHits(), hits, extractAggs(searchResult), nil + return searchResult.Total, hits, extractAggs(searchResult), nil } -func extractAggs(searchResult *elastic.SearchResult) []*internal.SearchResultLanguages { - var searchResultLanguages []*internal.SearchResultLanguages - agg, found := searchResult.Aggregations.Terms("language") - if found { - searchResultLanguages = make([]*internal.SearchResultLanguages, 0, 10) - - for _, bucket := range agg.Buckets { - searchResultLanguages = append(searchResultLanguages, &internal.SearchResultLanguages{ - Language: bucket.Key.(string), - Color: enry.GetColor(bucket.Key.(string)), - Count: int(bucket.DocCount), - }) +func extractAggs(searchResult *es.SearchResponse) []*internal.SearchResultLanguages { + buckets, found := searchResult.Aggregations["language"] + if !found { + return nil + } + searchResultLanguages := make([]*internal.SearchResultLanguages, 0, 10) + for _, bucket := range buckets { + // language is mapped as keyword so the key is always a string; if the + // mapping ever changes, skip rather than emit an empty-language bucket. + key, ok := bucket.Key.(string) + if !ok { + continue } + searchResultLanguages = append(searchResultLanguages, &internal.SearchResultLanguages{ + Language: key, + Color: enry.GetColor(key), + Count: int(bucket.DocCount), + }) } return searchResultLanguages } // Search searches for codes and language stats by given conditions. func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { - var contentQuery elastic.Query searchMode := util.IfZero(opts.SearchMode, b.SupportedSearchModes()[0].ModeValue) + contentQuery := es.Query(es.NewMultiMatchQuery(opts.Keyword, "content").Type(es.MultiMatchTypeBestFields).Operator("and")) if searchMode == indexer.SearchModeExact { - // 1.21 used NewMultiMatchQuery().Type(esMultiMatchTypePhrasePrefix), but later releases changed to NewMatchPhraseQuery - contentQuery = elastic.NewMatchPhraseQuery("content", opts.Keyword) - } else /* words */ { - contentQuery = elastic.NewMultiMatchQuery("content", opts.Keyword).Type(esMultiMatchTypeBestFields).Operator("and") + contentQuery = es.MatchPhraseQuery("content", opts.Keyword) } - kwQuery := elastic.NewBoolQuery().Should( + kwQuery := es.NewBoolQuery().Should( contentQuery, - elastic.NewMultiMatchQuery(opts.Keyword, "filename^10").Type(esMultiMatchTypePhrasePrefix), + es.NewMultiMatchQuery(opts.Keyword, "filename^10").Type(es.MultiMatchTypePhrasePrefix), ) - query := elastic.NewBoolQuery() - query = query.Must(kwQuery) + query := es.NewBoolQuery().Must(kwQuery) if len(opts.RepoIDs) > 0 { - repoStrs := make([]any, 0, len(opts.RepoIDs)) - for _, repoID := range opts.RepoIDs { - repoStrs = append(repoStrs, repoID) - } - repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...) - query = query.Must(repoQuery) + query.Must(es.TermsQuery("repo_id", es.ToAnySlice(opts.RepoIDs)...)) } - var ( - start, pageSize = opts.GetSkipTake() - kw = "" + opts.Keyword + "" - aggregation = elastic.NewTermsAggregation().Field("language").Size(10).OrderByCountDesc() - ) + start, pageSize := opts.GetSkipTake() + kw := "" + opts.Keyword + "" + languageAggs := map[string]any{ + "language": map[string]any{ + "terms": map[string]any{ + "field": "language", + "size": 10, + "order": map[string]any{"_count": "desc"}, + }, + }, + } + // number_of_fragments=0 returns the full highlighted content (no fragmentation). + highlight := map[string]any{ + "fields": map[string]any{ + "content": map[string]any{}, + "filename": map[string]any{}, + }, + "number_of_fragments": 0, + "type": "fvh", + } + sort := []es.SortField{ + {Field: "_score", Desc: true}, + {Field: "updated_at", Desc: false}, + } if len(opts.Language) == 0 { - searchResult, err := b.inner.Client.Search(). - Index(b.inner.VersionedIndexName()). - Aggregation("language", aggregation). - Query(query). - Highlight( - elastic.NewHighlight(). - Field("content"). - Field("filename"). - NumOfFragments(0). // return all highlighting content on fragments - HighlighterType("fvh"), - ). - Sort("_score", false). - Sort("updated_at", true). - From(start).Size(pageSize). - Do(ctx) + resp, err := b.Indexer.Search(ctx, es.SearchRequest{ + Query: query, + Sort: sort, + From: start, + Size: pageSize, + TrackTotal: true, + Aggregations: languageAggs, + Highlight: highlight, + }) if err != nil { return 0, nil, nil, err } - - return convertResult(searchResult, kw, pageSize) + return convertResult(resp, kw, pageSize) } - langQuery := elastic.NewMatchQuery("language", opts.Language) - countResult, err := b.inner.Client.Search(). - Index(b.inner.VersionedIndexName()). - Aggregation("language", aggregation). - Query(query). - Size(0). // We only need stats information - Do(ctx) + countResp, err := b.Indexer.Search(ctx, es.SearchRequest{ + Query: query, + Size: 0, // stats only + TrackTotal: true, + Aggregations: languageAggs, + }) if err != nil { return 0, nil, nil, err } - query = query.Must(langQuery) - searchResult, err := b.inner.Client.Search(). - Index(b.inner.VersionedIndexName()). - Query(query). - Highlight( - elastic.NewHighlight(). - Field("content"). - Field("filename"). - NumOfFragments(0). // return all highlighting content on fragments - HighlighterType("fvh"), - ). - Sort("_score", false). - Sort("updated_at", true). - From(start).Size(pageSize). - Do(ctx) + query.Must(es.MatchQuery("language", opts.Language)) + resp, err := b.Indexer.Search(ctx, es.SearchRequest{ + Query: query, + Sort: sort, + From: start, + Size: pageSize, + TrackTotal: true, + Highlight: highlight, + }) if err != nil { return 0, nil, nil, err } - total, hits, _, err := convertResult(searchResult, kw, pageSize) - - return total, hits, extractAggs(countResult), err + total, hits, _, err := convertResult(resp, kw, pageSize) + return total, hits, extractAggs(countResp), err } diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index a884ab733a..a8baf44edc 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -8,6 +8,7 @@ import ( "os" "slices" "testing" + "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" @@ -39,6 +40,16 @@ func TestMain(m *testing.M) { func testIndexer(name string, t *testing.T, indexer internal.Indexer) { t.Run(name, func(t *testing.T) { assert.NoError(t, setupRepositoryIndexes(t.Context(), indexer)) + // Wait for the index to catch up: ES/OpenSearch make writes visible + // only after a refresh (default interval: 1s). Bleve is synchronous + // and passes on the first iteration. + require.Eventually(t, func() bool { + total, _, _, err := indexer.Search(t.Context(), &internal.SearchOptions{ + Keyword: "Description", + Paginator: &db.ListOptions{Page: 1, PageSize: 1}, + }) + return err == nil && total > 0 + }, 10*time.Second, 100*time.Millisecond, "index did not become searchable") keywords := []struct { RepoIDs []int64 diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index 009d659d76..b5eb5116b0 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -74,7 +74,7 @@ func HighlightSearchResultCode(filename, language string, lineNums []int, code s // we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting lexer := highlight.DetectChromaLexerByFileName(filename, language) hl := highlight.RenderCodeByLexer(lexer, code) - highlightedLines := strings.Split(string(hl), "\n") + highlightedLines := highlight.UnsafeSplitHighlightedLines(hl) // The lineNums outputted by render might not match the original lineNums, because "highlight" removes the last `\n` lines := make([]*ResultLine, min(len(highlightedLines), len(lineNums))) diff --git a/modules/indexer/internal/elasticsearch/indexer.go b/modules/indexer/internal/elasticsearch/indexer.go index 265ce26585..eaeb89efaa 100644 --- a/modules/indexer/internal/elasticsearch/indexer.go +++ b/modules/indexer/internal/elasticsearch/indexer.go @@ -4,52 +4,80 @@ package elasticsearch import ( + "bytes" "context" - "errors" "fmt" + "io" + "net" + "net/http" + "net/url" + "slices" + "strconv" + "strings" + "time" "code.gitea.io/gitea/modules/indexer/internal" - - "github.com/olivere/elastic/v7" + "code.gitea.io/gitea/modules/json" ) var _ internal.Indexer = &Indexer{} -// Indexer represents a basic elasticsearch indexer implementation +// Indexer is a narrow wrapper around an Elasticsearch/OpenSearch cluster. +// It targets the REST subset shared by Elasticsearch 7/8/9 and OpenSearch 3. type Indexer struct { - Client *elastic.Client + client *http.Client + base string // base URL with trailing slash, no userinfo + user string + pass string - url string indexName string version int mapping string } -func NewIndexer(url, indexName string, version int, mapping string) *Indexer { +// NewIndexer builds an Indexer. The connection is opened by Init. +func NewIndexer(rawURL, indexName string, version int, mapping string) *Indexer { return &Indexer{ - url: url, + base: rawURL, indexName: indexName, version: version, mapping: mapping, } } -// Init initializes the indexer +// Init connects and creates the versioned index if missing, returning true if it already existed. func (i *Indexer) Init(ctx context.Context) (bool, error) { - if i == nil { - return false, errors.New("cannot init nil indexer") - } - if i.Client != nil { - return false, errors.New("indexer is already initialized") - } - - client, err := i.initClient() + parsed, err := url.Parse(i.base) if err != nil { - return false, err + return false, fmt.Errorf("parse elasticsearch url: %w", err) + } + if parsed.User != nil { + i.user = parsed.User.Username() + i.pass, _ = parsed.User.Password() + parsed.User = nil + } + base := parsed.String() + if !strings.HasSuffix(base, "/") { + base += "/" + } + i.base = base + // No client-level Timeout: bulk/_delete_by_query can legitimately run for + // minutes on large repos. Per-request deadlines come from the caller's ctx; + // transport-level timeouts cover stalled connects/handshakes/headers so a + // half-open server cannot wedge the indexer indefinitely. + i.client = &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext, + TLSHandshakeTimeout: 10 * time.Second, + ResponseHeaderTimeout: 30 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + IdleConnTimeout: 90 * time.Second, + MaxIdleConns: 100, + }, } - i.Client = client - exists, err := i.Client.IndexExists(i.VersionedIndexName()).Do(ctx) + exists, err := i.indexExists(ctx, i.VersionedIndexName()) if err != nil { return false, err } @@ -61,34 +89,321 @@ func (i *Indexer) Init(ctx context.Context) (bool, error) { return false, err } - return exists, nil + return false, nil } -// Ping checks if the indexer is available +// Ping returns an error when the cluster is unusable (status != green/yellow). func (i *Indexer) Ping(ctx context.Context) error { - if i == nil { - return errors.New("cannot ping nil indexer") + var body struct { + Status string `json:"status"` } - if i.Client == nil { - return errors.New("indexer is not initialized") - } - - resp, err := i.Client.ClusterHealth().Do(ctx) - if err != nil { + if err := i.doJSON(ctx, http.MethodGet, "_cluster/health", nil, &body); err != nil { return err } - if resp.Status != "green" && resp.Status != "yellow" { - // It's healthy if the status is green, and it's available if the status is yellow, - // see https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html - return fmt.Errorf("status of elasticsearch cluster is %s", resp.Status) + // Healthy = green; usable = yellow. Red is unusable. + // https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html + if body.Status != "green" && body.Status != "yellow" { + return fmt.Errorf("status of elasticsearch cluster is %s", body.Status) } return nil } -// Close closes the indexer +// Close releases idle HTTP connections held by the client. func (i *Indexer) Close() { - if i == nil { + if i == nil || i.client == nil { return } - i.Client = nil + i.client.CloseIdleConnections() + i.client = nil +} + +// Bulk submits index/delete ops. Returns the first item-level failure, if any. +func (i *Indexer) Bulk(ctx context.Context, ops []BulkOp) error { + if len(ops) == 0 { + return nil + } + + index := i.VersionedIndexName() + var buf bytes.Buffer + buf.Grow(len(ops) * 256) + for _, op := range ops { + meta := map[string]any{op.action: map[string]any{"_index": index, "_id": op.id}} + if err := writeJSONLine(&buf, meta); err != nil { + return err + } + if op.action == bulkActionIndex { + if err := writeJSONLine(&buf, op.doc); err != nil { + return err + } + } + } + + res, err := i.do(ctx, http.MethodPost, urlPath(index, "_bulk"), "application/x-ndjson", bytes.NewReader(buf.Bytes())) + if err != nil { + return err + } + defer drainAndClose(res) + + var body struct { + Errors bool `json:"errors"` + Items []map[string]struct { + Status int `json:"status"` + Error json.Value `json:"error"` + } `json:"items"` + } + if err := json.NewDecoder(res.Body).Decode(&body); err != nil { + return err + } + if !body.Errors { + return nil + } + return firstBulkError(body.Items) +} + +// firstBulkError returns the first item-level failure in a bulk response. +// Each items entry is a single-key map ({"index": {...}} or {"delete": {...}}). +// Delete-of-missing (404) is idempotent and not reported. +func firstBulkError(items []map[string]struct { + Status int `json:"status"` + Error json.Value `json:"error"` +}, +) error { + for _, item := range items { + for action, result := range item { + if action == bulkActionDelete && result.Status == http.StatusNotFound { + continue + } + if result.Status >= 300 { + return fmt.Errorf("bulk %s failed (status %d): %s", action, result.Status, string(result.Error)) + } + } + } + return nil +} + +// Index writes a single document. +func (i *Indexer) Index(ctx context.Context, id string, doc any) error { + body, err := json.Marshal(doc) + if err != nil { + return err + } + return i.doJSON(ctx, http.MethodPut, urlPath(i.VersionedIndexName(), "_doc", id), bytes.NewReader(body), nil) +} + +// Delete removes a single document by id. Missing ids are not an error. +func (i *Indexer) Delete(ctx context.Context, id string) error { + res, err := i.do(ctx, http.MethodDelete, urlPath(i.VersionedIndexName(), "_doc", id), "", nil, http.StatusNotFound) + if err != nil { + return err + } + drainAndClose(res) + return nil +} + +// DeleteByQuery removes every document matching the query. +func (i *Indexer) DeleteByQuery(ctx context.Context, query Query) error { + body, err := json.Marshal(map[string]any{"query": query.querySource()}) + if err != nil { + return err + } + return i.doJSON(ctx, http.MethodPost, urlPath(i.VersionedIndexName(), "_delete_by_query"), bytes.NewReader(body), nil) +} + +// Refresh forces a refresh so recent writes are searchable. +func (i *Indexer) Refresh(ctx context.Context) error { + return i.doJSON(ctx, http.MethodPost, urlPath(i.VersionedIndexName(), "_refresh"), nil, nil) +} + +// Search runs a search request and decodes the reply. +func (i *Indexer) Search(ctx context.Context, req SearchRequest) (*SearchResponse, error) { + body := map[string]any{} + if req.Query != nil { + body["query"] = req.Query.querySource() + } + if len(req.Sort) > 0 { + sorts := make([]map[string]any, len(req.Sort)) + for idx, s := range req.Sort { + sorts[idx] = s.source() + } + body["sort"] = sorts + } + if req.From > 0 { + body["from"] = req.From + } + body["size"] = req.Size + if len(req.Aggregations) > 0 { + body["aggs"] = req.Aggregations + } + if len(req.Highlight) > 0 { + body["highlight"] = req.Highlight + } + + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + + // Default track_total_hits is 10000 (capped count); send it explicitly so + // callers can choose between exact totals (true) and skipping counting (false). + path := urlPath(i.VersionedIndexName(), "_search") + "?track_total_hits=" + strconv.FormatBool(req.TrackTotal) + res, err := i.do(ctx, http.MethodPost, path, "application/json", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + defer drainAndClose(res) + return decodeSearchResponse(res.Body) +} + +func (i *Indexer) indexExists(ctx context.Context, name string) (bool, error) { + res, err := i.do(ctx, http.MethodHead, urlPath(name), "", nil, http.StatusNotFound) + if err != nil { + return false, err + } + drainAndClose(res) + return res.StatusCode == http.StatusOK, nil +} + +func (i *Indexer) createIndex(ctx context.Context) error { + var body struct { + Acknowledged bool `json:"acknowledged"` + } + if err := i.doJSON(ctx, http.MethodPut, urlPath(i.VersionedIndexName()), bytes.NewBufferString(i.mapping), &body); err != nil { + return fmt.Errorf("create index %s: %w", i.VersionedIndexName(), err) + } + if !body.Acknowledged { + return fmt.Errorf("create index %s not acknowledged", i.VersionedIndexName()) + } + + i.checkOldIndexes(ctx) + return nil +} + +// do sends a request and returns the response. Status >= 300 is turned into +// an error unless the status appears in okStatus. The caller closes Body. +func (i *Indexer) do(ctx context.Context, method, path, contentType string, body io.Reader, okStatus ...int) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, method, i.base+path, body) + if err != nil { + return nil, err + } + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + if i.user != "" || i.pass != "" { + req.SetBasicAuth(i.user, i.pass) + } + res, err := i.client.Do(req) + if err != nil { + return nil, err + } + if res.StatusCode >= 300 && !slices.Contains(okStatus, res.StatusCode) { + msg := readErrBody(res) + res.Body.Close() + return nil, fmt.Errorf("%s %s: %s", method, path, msg) + } + return res, nil +} + +// doJSON sends a request with a JSON body and, when out is non-nil, decodes +// the JSON response into it. +func (i *Indexer) doJSON(ctx context.Context, method, path string, body io.Reader, out any) error { + contentType := "" + if body != nil { + contentType = "application/json" + } + res, err := i.do(ctx, method, path, contentType, body) + if err != nil { + return err + } + defer drainAndClose(res) + if out == nil { + return nil + } + return json.NewDecoder(res.Body).Decode(out) +} + +// drainAndClose discards any unread response body before closing so the +// underlying TCP connection can be reused for keep-alive. +func drainAndClose(res *http.Response) { + _, _ = io.Copy(io.Discard, res.Body) + res.Body.Close() +} + +func writeJSONLine(buf *bytes.Buffer, v any) error { + enc, err := json.Marshal(v) + if err != nil { + return err + } + buf.Write(enc) + buf.WriteByte('\n') + return nil +} + +// readErrBody reads up to 4 KiB of an error response and drains the rest so +// the underlying connection can be reused (keep-alive needs Body fully read). +func readErrBody(res *http.Response) string { + const limit = 4 << 10 + b, _ := io.ReadAll(io.LimitReader(res.Body, limit)) + _, _ = io.Copy(io.Discard, res.Body) + return fmt.Sprintf("status %d: %s", res.StatusCode, bytes.TrimSpace(b)) +} + +func decodeSearchResponse(r io.Reader) (*SearchResponse, error) { + var raw struct { + Hits struct { + Total struct { + Value int64 `json:"value"` + } `json:"total"` + Hits []struct { + ID string `json:"_id"` + Score float64 `json:"_score"` + Source json.Value `json:"_source"` + Highlight map[string][]string `json:"highlight"` + } `json:"hits"` + } `json:"hits"` + Aggregations map[string]struct { + Buckets []struct { + Key any `json:"key"` + DocCount int64 `json:"doc_count"` + } `json:"buckets"` + } `json:"aggregations"` + } + if err := json.NewDecoder(r).Decode(&raw); err != nil { + return nil, err + } + + resp := &SearchResponse{ + Total: raw.Hits.Total.Value, + Hits: make([]SearchHit, 0, len(raw.Hits.Hits)), + } + for _, h := range raw.Hits.Hits { + resp.Hits = append(resp.Hits, SearchHit{ + ID: h.ID, + Score: h.Score, + Source: h.Source, + Highlight: h.Highlight, + }) + } + if len(raw.Aggregations) > 0 { + resp.Aggregations = make(map[string][]AggBucket, len(raw.Aggregations)) + for name, agg := range raw.Aggregations { + buckets := make([]AggBucket, len(agg.Buckets)) + for idx, b := range agg.Buckets { + buckets[idx] = AggBucket{Key: b.Key, DocCount: b.DocCount} + } + resp.Aggregations[name] = buckets + } + } + return resp, nil +} + +// urlPath joins path segments with `/` and percent-escapes each. +func urlPath(segments ...string) string { + var b bytes.Buffer + for idx, s := range segments { + if idx > 0 { + b.WriteByte('/') + } + b.WriteString(url.PathEscape(s)) + } + return b.String() } diff --git a/modules/indexer/internal/elasticsearch/indexer_test.go b/modules/indexer/internal/elasticsearch/indexer_test.go new file mode 100644 index 0000000000..4db9270733 --- /dev/null +++ b/modules/indexer/internal/elasticsearch/indexer_test.go @@ -0,0 +1,39 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package elasticsearch + +import ( + "strings" + "testing" + + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/require" +) + +func newRealIndexer(t *testing.T) *Indexer { + t.Helper() + esURL := test.ExternalServiceHTTP(t, "TEST_ELASTICSEARCH_URL", "http://elasticsearch:9200") + indexName := "gitea_test_" + strings.ReplaceAll(strings.ToLower(t.Name()), "/", "_") + ix := NewIndexer(esURL, indexName, 1, `{"mappings":{"properties":{"x":{"type":"keyword"}}}}`) + _, err := ix.Init(t.Context()) + require.NoError(t, err) + t.Cleanup(ix.Close) + return ix +} + +func TestPing(t *testing.T) { + ix := newRealIndexer(t) + require.NoError(t, ix.Ping(t.Context())) +} + +func TestDeleteSwallows404(t *testing.T) { + ix := newRealIndexer(t) + require.NoError(t, ix.Delete(t.Context(), "missing-id")) +} + +func TestBulkAcceptsDelete404(t *testing.T) { + ix := newRealIndexer(t) + require.NoError(t, ix.Bulk(t.Context(), []BulkOp{DeleteOp("missing-id")})) +} diff --git a/modules/indexer/internal/elasticsearch/query.go b/modules/indexer/internal/elasticsearch/query.go new file mode 100644 index 0000000000..8f8cf74303 --- /dev/null +++ b/modules/indexer/internal/elasticsearch/query.go @@ -0,0 +1,132 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package elasticsearch + +// MultiMatch types used by the call sites. See +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#multi-match-types +const ( + MultiMatchTypeBestFields = "best_fields" + MultiMatchTypePhrasePrefix = "phrase_prefix" +) + +// ToAnySlice converts []T to []any for variadic query args like TermsQuery. +func ToAnySlice[T any](s []T) []any { + out := make([]any, len(s)) + for idx, v := range s { + out[idx] = v + } + return out +} + +// Query is an Elasticsearch query DSL node. It marshals to the JSON +// object expected by the ES query API. +type Query interface { + querySource() map[string]any +} + +type rawQuery map[string]any + +func (q rawQuery) querySource() map[string]any { return q } + +// TermQuery matches documents whose `field` exactly equals `value`. +func TermQuery(field string, value any) Query { + return rawQuery{"term": map[string]any{field: value}} +} + +// TermsQuery matches documents whose `field` equals any of `values`. +func TermsQuery(field string, values ...any) Query { + return rawQuery{"terms": map[string]any{field: values}} +} + +// MatchQuery is a full-text match on a single field. +func MatchQuery(field string, value any) Query { + return rawQuery{"match": map[string]any{field: value}} +} + +// MatchPhraseQuery matches the exact phrase on `field`. +func MatchPhraseQuery(field, value string) Query { + return rawQuery{"match_phrase": map[string]any{field: value}} +} + +// MultiMatchQuery is the fluent builder for a multi_match query. +type MultiMatchQuery struct { + query any + fields []string + typ string + operator string +} + +// NewMultiMatchQuery creates a multi_match query over the given fields. +func NewMultiMatchQuery(query any, fields ...string) *MultiMatchQuery { + return &MultiMatchQuery{query: query, fields: fields} +} + +func (m *MultiMatchQuery) Type(t string) *MultiMatchQuery { m.typ = t; return m } +func (m *MultiMatchQuery) Operator(op string) *MultiMatchQuery { m.operator = op; return m } + +func (m *MultiMatchQuery) querySource() map[string]any { + body := map[string]any{"query": m.query} + if len(m.fields) > 0 { + body["fields"] = m.fields + } + if m.typ != "" { + body["type"] = m.typ + } + if m.operator != "" { + body["operator"] = m.operator + } + return map[string]any{"multi_match": body} +} + +// RangeQuery is the fluent builder for a range query. +type RangeQuery struct { + field string + body map[string]any +} + +func NewRangeQuery(field string) *RangeQuery { + return &RangeQuery{field: field, body: map[string]any{}} +} + +func (r *RangeQuery) Gte(v any) *RangeQuery { r.body["gte"] = v; return r } +func (r *RangeQuery) Lte(v any) *RangeQuery { r.body["lte"] = v; return r } + +func (r *RangeQuery) querySource() map[string]any { + return map[string]any{"range": map[string]any{r.field: r.body}} +} + +// BoolQuery is the fluent builder for a bool query. +type BoolQuery struct { + must []Query + should []Query + mustNot []Query +} + +func NewBoolQuery() *BoolQuery { return &BoolQuery{} } + +func (b *BoolQuery) Must(q ...Query) *BoolQuery { b.must = append(b.must, q...); return b } +func (b *BoolQuery) Should(q ...Query) *BoolQuery { b.should = append(b.should, q...); return b } +func (b *BoolQuery) MustNot(q ...Query) *BoolQuery { b.mustNot = append(b.mustNot, q...); return b } + +func (b *BoolQuery) querySource() map[string]any { + body := map[string]any{} + if len(b.must) > 0 { + body["must"] = querySlice(b.must) + } + if len(b.should) > 0 { + body["should"] = querySlice(b.should) + } + if len(b.mustNot) > 0 { + body["must_not"] = querySlice(b.mustNot) + } + return map[string]any{"bool": body} +} + +func querySlice(queries []Query) []map[string]any { + out := make([]map[string]any, len(queries)) + for idx, q := range queries { + out[idx] = q.querySource() + } + return out +} diff --git a/modules/indexer/internal/elasticsearch/types.go b/modules/indexer/internal/elasticsearch/types.go new file mode 100644 index 0000000000..106f2faa8e --- /dev/null +++ b/modules/indexer/internal/elasticsearch/types.go @@ -0,0 +1,76 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package elasticsearch + +import "code.gitea.io/gitea/modules/json" + +const ( + bulkActionIndex = "index" + bulkActionDelete = "delete" +) + +// BulkOp is a single write inside a Bulk call. Construct with IndexOp or DeleteOp. +type BulkOp struct { + action string + id string + doc any +} + +// IndexOp builds a bulk index operation. +func IndexOp(id string, doc any) BulkOp { + return BulkOp{action: bulkActionIndex, id: id, doc: doc} +} + +// DeleteOp builds a bulk delete operation. +func DeleteOp(id string) BulkOp { + return BulkOp{action: bulkActionDelete, id: id} +} + +// SortField is one entry of the search sort array. +type SortField struct { + Field string + Desc bool +} + +func (s SortField) source() map[string]any { + order := "asc" + if s.Desc { + order = "desc" + } + return map[string]any{s.Field: map[string]any{"order": order}} +} + +// SearchRequest captures everything Gitea sends to the _search endpoint. +// Aggregations and Highlight are raw ES JSON bodies — callers write them as +// map[string]any since each has exactly one call site with a fixed shape. +type SearchRequest struct { + Query Query + Sort []SortField + From int + Size int + TrackTotal bool + Aggregations map[string]any + Highlight map[string]any +} + +// SearchHit is a single result row. +type SearchHit struct { + ID string + Score float64 + Source json.Value + Highlight map[string][]string +} + +// AggBucket is a terms-aggregation bucket. +type AggBucket struct { + Key any + DocCount int64 +} + +// SearchResponse is Gitea's decoded view of the search reply. +type SearchResponse struct { + Total int64 + Hits []SearchHit + Aggregations map[string][]AggBucket +} diff --git a/modules/indexer/internal/elasticsearch/util.go b/modules/indexer/internal/elasticsearch/util.go index 9e034bd553..2e96d4220a 100644 --- a/modules/indexer/internal/elasticsearch/util.go +++ b/modules/indexer/internal/elasticsearch/util.go @@ -6,14 +6,11 @@ package elasticsearch import ( "context" "fmt" - "time" "code.gitea.io/gitea/modules/log" - - "github.com/olivere/elastic/v7" ) -// VersionedIndexName returns the full index name with version +// VersionedIndexName returns the full index name with version suffix. func (i *Indexer) VersionedIndexName() string { return versionedIndexName(i.indexName, i.version) } @@ -26,41 +23,10 @@ func versionedIndexName(indexName string, version int) string { return fmt.Sprintf("%s.v%d", indexName, version) } -func (i *Indexer) createIndex(ctx context.Context) error { - createIndex, err := i.Client.CreateIndex(i.VersionedIndexName()).BodyString(i.mapping).Do(ctx) - if err != nil { - return err - } - if !createIndex.Acknowledged { - return fmt.Errorf("create index %s with %s failed", i.VersionedIndexName(), i.mapping) - } - - i.checkOldIndexes(ctx) - - return nil -} - -func (i *Indexer) initClient() (*elastic.Client, error) { - opts := []elastic.ClientOptionFunc{ - elastic.SetURL(i.url), - elastic.SetSniff(false), - elastic.SetHealthcheckInterval(10 * time.Second), - elastic.SetGzip(false), - } - - logger := log.GetLogger(log.DEFAULT) - - opts = append(opts, elastic.SetTraceLog(&log.PrintfLogger{Logf: logger.Trace})) - opts = append(opts, elastic.SetInfoLog(&log.PrintfLogger{Logf: logger.Info})) - opts = append(opts, elastic.SetErrorLog(&log.PrintfLogger{Logf: logger.Error})) - - return elastic.NewClient(opts...) -} - func (i *Indexer) checkOldIndexes(ctx context.Context) { - for v := 0; v < i.version; v++ { + for v := range i.version { indexName := versionedIndexName(i.indexName, v) - exists, err := i.Client.IndexExists(indexName).Do(ctx) + exists, err := i.indexExists(ctx, indexName) if err == nil && exists { log.Warn("Found older elasticsearch index named %q, Gitea will keep the old NOT DELETED. You can delete the old version after the upgrade succeed.", indexName) } diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index 39d96cab98..219d4163d6 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -27,7 +27,7 @@ import ( const ( issueIndexerAnalyzer = "issueIndexer" issueIndexerDocType = "issueIndexerDocType" - issueIndexerLatestVersion = 5 + issueIndexerLatestVersion = 6 ) const unicodeNormalizeName = "unicodeNormalize" @@ -83,8 +83,8 @@ func generateIssueIndexMapping() (mapping.IndexMapping, error) { docMapping.AddFieldMappingsAt("label_ids", numberFieldMapping) docMapping.AddFieldMappingsAt("no_label", boolFieldMapping) docMapping.AddFieldMappingsAt("milestone_id", numberFieldMapping) - docMapping.AddFieldMappingsAt("project_id", numberFieldMapping) - docMapping.AddFieldMappingsAt("project_board_id", numberFieldMapping) + docMapping.AddFieldMappingsAt("project_ids", numberFieldMapping) + docMapping.AddFieldMappingsAt("no_project", boolFieldMapping) docMapping.AddFieldMappingsAt("poster_id", numberFieldMapping) docMapping.AddFieldMappingsAt("assignee_id", numberFieldMapping) docMapping.AddFieldMappingsAt("mention_ids", numberFieldMapping) @@ -241,11 +241,15 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( queries = append(queries, bleve.NewDisjunctionQuery(milestoneQueries...)) } - if options.ProjectID.Has() { - queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id")) - } - if options.ProjectColumnID.Has() { - queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id")) + if options.NoProjectOnly { + queries = append(queries, inner_bleve.BoolFieldQuery(true, "no_project")) + } else if len(options.ProjectIDs) > 0 { + var projectQueries []query.Query + for _, projectID := range options.ProjectIDs { + projectQueries = append(projectQueries, inner_bleve.NumericEqualityQuery(projectID, "project_ids")) + } + // FIXME: ISSUE-MULTIPLE-PROJECTS-FILTER: this logic is not right, it should use "AND" but not "OR" + queries = append(queries, bleve.NewDisjunctionQuery(projectQueries...)) } if options.PosterID != "" { diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index 380a25dc23..7a66efe791 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/indexer/issues/internal" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/util" ) func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_model.IssuesOptions, error) { @@ -65,8 +66,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m ReviewRequestedID: convertID(options.ReviewRequestedID), ReviewedID: convertID(options.ReviewedID), SubscriberID: convertID(options.SubscriberID), - ProjectID: convertID(options.ProjectID), - ProjectColumnID: convertID(options.ProjectColumnID), + ProjectIDs: util.Iif(options.NoProjectOnly, []int64{db.NoConditionID}, options.ProjectIDs), IsClosed: options.IsClosed, IsPull: options.IsPull, IncludedLabelNames: nil, diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index f17724664d..f4582d38dd 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -46,10 +46,10 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp searchOpt.MilestoneIDs = opts.MilestoneIDs } - if opts.ProjectID > 0 { - searchOpt.ProjectID = optional.Some(opts.ProjectID) - } else if opts.ProjectID == db.NoConditionID { // FIXME: this is inconsistent from other places - searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0) + if len(opts.ProjectIDs) == 1 && opts.ProjectIDs[0] == db.NoConditionID { + searchOpt.NoProjectOnly = true + } else { + searchOpt.ProjectIDs = opts.ProjectIDs } searchOpt.AssigneeID = opts.AssigneeID @@ -65,7 +65,6 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp return nil } - searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID) searchOpt.PosterID = opts.PosterID searchOpt.MentionID = convertID(opts.MentionedID) searchOpt.ReviewedID = convertID(opts.ReviewedID) diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index 9d627466ef..8286e72f49 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -11,27 +11,18 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/indexer" indexer_internal "code.gitea.io/gitea/modules/indexer/internal" - inner_elasticsearch "code.gitea.io/gitea/modules/indexer/internal/elasticsearch" + es "code.gitea.io/gitea/modules/indexer/internal/elasticsearch" "code.gitea.io/gitea/modules/indexer/issues/internal" "code.gitea.io/gitea/modules/util" - - "github.com/olivere/elastic/v7" ) -const ( - issueIndexerLatestVersion = 2 - // multi-match-types, currently only 2 types are used - // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types - esMultiMatchTypeBestFields = "best_fields" - esMultiMatchTypePhrasePrefix = "phrase_prefix" -) +const issueIndexerLatestVersion = 3 var _ internal.Indexer = &Indexer{} // Indexer implements Indexer interface type Indexer struct { - inner *inner_elasticsearch.Indexer - indexer_internal.Indexer // do not composite inner_elasticsearch.Indexer directly to avoid exposing too much + *es.Indexer } func (b *Indexer) SupportedSearchModes() []indexer.SearchMode { @@ -41,12 +32,7 @@ func (b *Indexer) SupportedSearchModes() []indexer.SearchMode { // NewIndexer creates a new elasticsearch indexer func NewIndexer(url, indexerName string) *Indexer { - inner := inner_elasticsearch.NewIndexer(url, indexerName, issueIndexerLatestVersion, defaultMapping) - indexer := &Indexer{ - inner: inner, - Indexer: inner, - } - return indexer + return &Indexer{Indexer: es.NewIndexer(url, indexerName, issueIndexerLatestVersion, defaultMapping)} } const ( @@ -68,8 +54,8 @@ const ( "label_ids": { "type": "integer", "index": true }, "no_label": { "type": "boolean", "index": true }, "milestone_id": { "type": "integer", "index": true }, - "project_id": { "type": "integer", "index": true }, - "project_board_id": { "type": "integer", "index": true }, + "project_ids": { "type": "integer", "index": true }, + "no_project": { "type": "boolean", "index": true }, "poster_id": { "type": "integer", "index": true }, "assignee_id": { "type": "integer", "index": true }, "mention_ids": { "type": "integer", "index": true }, @@ -93,29 +79,14 @@ func (b *Indexer) Index(ctx context.Context, issues ...*internal.IndexerData) er return nil } else if len(issues) == 1 { issue := issues[0] - _, err := b.inner.Client.Index(). - Index(b.inner.VersionedIndexName()). - Id(strconv.FormatInt(issue.ID, 10)). - BodyJson(issue). - Do(ctx) - return err + return b.Indexer.Index(ctx, strconv.FormatInt(issue.ID, 10), issue) } - reqs := make([]elastic.BulkableRequest, 0) + ops := make([]es.BulkOp, 0, len(issues)) for _, issue := range issues { - reqs = append(reqs, - elastic.NewBulkIndexRequest(). - Index(b.inner.VersionedIndexName()). - Id(strconv.FormatInt(issue.ID, 10)). - Doc(issue), - ) + ops = append(ops, es.IndexOp(strconv.FormatInt(issue.ID, 10), issue)) } - - _, err := b.inner.Client.Bulk(). - Index(b.inner.VersionedIndexName()). - Add(reqs...). - Do(graceful.GetManager().HammerContext()) - return err + return b.Bulk(graceful.GetManager().HammerContext(), ops) } // Delete deletes indexes by ids @@ -123,129 +94,116 @@ func (b *Indexer) Delete(ctx context.Context, ids ...int64) error { if len(ids) == 0 { return nil } else if len(ids) == 1 { - _, err := b.inner.Client.Delete(). - Index(b.inner.VersionedIndexName()). - Id(strconv.FormatInt(ids[0], 10)). - Do(ctx) - return err + return b.Indexer.Delete(ctx, strconv.FormatInt(ids[0], 10)) } - reqs := make([]elastic.BulkableRequest, 0) + ops := make([]es.BulkOp, 0, len(ids)) for _, id := range ids { - reqs = append(reqs, - elastic.NewBulkDeleteRequest(). - Index(b.inner.VersionedIndexName()). - Id(strconv.FormatInt(id, 10)), - ) + ops = append(ops, es.DeleteOp(strconv.FormatInt(id, 10))) } - - _, err := b.inner.Client.Bulk(). - Index(b.inner.VersionedIndexName()). - Add(reqs...). - Do(graceful.GetManager().HammerContext()) - return err + return b.Bulk(graceful.GetManager().HammerContext(), ops) } // Search searches for issues by given conditions. // Returns the matching issue IDs func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (*internal.SearchResult, error) { - query := elastic.NewBoolQuery() + query := es.NewBoolQuery() if options.Keyword != "" { searchMode := util.IfZero(options.SearchMode, b.SupportedSearchModes()[0].ModeValue) + mm := es.NewMultiMatchQuery(options.Keyword, "title", "content", "comments") if searchMode == indexer.SearchModeExact { - query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments").Type(esMultiMatchTypePhrasePrefix)) - } else /* words */ { - query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments").Type(esMultiMatchTypeBestFields).Operator("and")) + mm = mm.Type(es.MultiMatchTypePhrasePrefix) + } else { + mm = mm.Type(es.MultiMatchTypeBestFields).Operator("and") } + query.Must(mm) } if len(options.RepoIDs) > 0 { - q := elastic.NewBoolQuery() - q.Should(elastic.NewTermsQuery("repo_id", toAnySlice(options.RepoIDs)...)) + q := es.NewBoolQuery() + q.Should(es.TermsQuery("repo_id", es.ToAnySlice(options.RepoIDs)...)) if options.AllPublic { - q.Should(elastic.NewTermQuery("is_public", true)) + q.Should(es.TermQuery("is_public", true)) } query.Must(q) } if options.IsPull.Has() { - query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value())) + query.Must(es.TermQuery("is_pull", options.IsPull.Value())) } if options.IsClosed.Has() { - query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.Value())) + query.Must(es.TermQuery("is_closed", options.IsClosed.Value())) } if options.IsArchived.Has() { - query.Must(elastic.NewTermQuery("is_archived", options.IsArchived.Value())) + query.Must(es.TermQuery("is_archived", options.IsArchived.Value())) } if options.NoLabelOnly { - query.Must(elastic.NewTermQuery("no_label", true)) + query.Must(es.TermQuery("no_label", true)) } else { if len(options.IncludedLabelIDs) > 0 { - q := elastic.NewBoolQuery() + q := es.NewBoolQuery() for _, labelID := range options.IncludedLabelIDs { - q.Must(elastic.NewTermQuery("label_ids", labelID)) + q.Must(es.TermQuery("label_ids", labelID)) } query.Must(q) } else if len(options.IncludedAnyLabelIDs) > 0 { - query.Must(elastic.NewTermsQuery("label_ids", toAnySlice(options.IncludedAnyLabelIDs)...)) + query.Must(es.TermsQuery("label_ids", es.ToAnySlice(options.IncludedAnyLabelIDs)...)) } if len(options.ExcludedLabelIDs) > 0 { - q := elastic.NewBoolQuery() + q := es.NewBoolQuery() for _, labelID := range options.ExcludedLabelIDs { - q.MustNot(elastic.NewTermQuery("label_ids", labelID)) + q.MustNot(es.TermQuery("label_ids", labelID)) } query.Must(q) } } if len(options.MilestoneIDs) > 0 { - query.Must(elastic.NewTermsQuery("milestone_id", toAnySlice(options.MilestoneIDs)...)) + query.Must(es.TermsQuery("milestone_id", es.ToAnySlice(options.MilestoneIDs)...)) } - if options.ProjectID.Has() { - query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value())) - } - if options.ProjectColumnID.Has() { - query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value())) + if options.NoProjectOnly { + query.Must(es.TermQuery("no_project", true)) + } else if len(options.ProjectIDs) > 0 { + // FIXME: ISSUE-MULTIPLE-PROJECTS-FILTER: this logic is not right, it should use "AND" but not "OR" + query.Must(es.TermsQuery("project_ids", es.ToAnySlice(options.ProjectIDs)...)) } if options.PosterID != "" { // "(none)" becomes 0, it means no poster posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64) - query.Must(elastic.NewTermQuery("poster_id", posterIDInt64)) + query.Must(es.TermQuery("poster_id", posterIDInt64)) } if options.AssigneeID != "" { if options.AssigneeID == "(any)" { - q := elastic.NewRangeQuery("assignee_id") - q.Gte(1) - query.Must(q) + query.Must(es.NewRangeQuery("assignee_id").Gte(1)) } else { // "(none)" becomes 0, it means no assignee assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64) - query.Must(elastic.NewTermQuery("assignee_id", assigneeIDInt64)) + query.Must(es.TermQuery("assignee_id", assigneeIDInt64)) } } if options.MentionID.Has() { - query.Must(elastic.NewTermQuery("mention_ids", options.MentionID.Value())) + query.Must(es.TermQuery("mention_ids", options.MentionID.Value())) } if options.ReviewedID.Has() { - query.Must(elastic.NewTermQuery("reviewed_ids", options.ReviewedID.Value())) + query.Must(es.TermQuery("reviewed_ids", options.ReviewedID.Value())) } if options.ReviewRequestedID.Has() { - query.Must(elastic.NewTermQuery("review_requested_ids", options.ReviewRequestedID.Value())) + query.Must(es.TermQuery("review_requested_ids", options.ReviewRequestedID.Value())) } if options.SubscriberID.Has() { - query.Must(elastic.NewTermQuery("subscriber_ids", options.SubscriberID.Value())) + query.Must(es.TermQuery("subscriber_ids", options.SubscriberID.Value())) } if options.UpdatedAfterUnix.Has() || options.UpdatedBeforeUnix.Has() { - q := elastic.NewRangeQuery("updated_unix") + q := es.NewRangeQuery("updated_unix") if options.UpdatedAfterUnix.Has() { q.Gte(options.UpdatedAfterUnix.Value()) } @@ -258,9 +216,9 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( if options.SortBy == "" { options.SortBy = internal.SortByCreatedAsc } - sortBy := []elastic.Sorter{ + sortBy := []es.SortField{ parseSortBy(options.SortBy), - elastic.NewFieldSort("id").Desc(), + {Field: "id", Desc: true}, } // See https://stackoverflow.com/questions/35206409/elasticsearch-2-1-result-window-is-too-large-index-max-result-window/35221900 @@ -268,43 +226,30 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( const maxPageSize = 10000 skip, limit := indexer_internal.ParsePaginator(options.Paginator, maxPageSize) - searchResult, err := b.inner.Client.Search(). - Index(b.inner.VersionedIndexName()). - Query(query). - SortBy(sortBy...). - From(skip).Size(limit). - Do(ctx) + resp, err := b.Indexer.Search(ctx, es.SearchRequest{ + Query: query, + Sort: sortBy, + From: skip, + Size: limit, + TrackTotal: true, + }) if err != nil { return nil, err } - hits := make([]internal.Match, 0, limit) - for _, hit := range searchResult.Hits.Hits { - id, _ := strconv.ParseInt(hit.Id, 10, 64) - hits = append(hits, internal.Match{ - ID: id, - }) + hits := make([]internal.Match, 0, len(resp.Hits)) + for _, hit := range resp.Hits { + id, _ := strconv.ParseInt(hit.ID, 10, 64) + hits = append(hits, internal.Match{ID: id}) } return &internal.SearchResult{ - Total: searchResult.TotalHits(), + Total: resp.Total, Hits: hits, }, nil } -func toAnySlice[T any](s []T) []any { - ret := make([]any, 0, len(s)) - for _, item := range s { - ret = append(ret, item) - } - return ret -} - -func parseSortBy(sortBy internal.SortBy) elastic.Sorter { - field := strings.TrimPrefix(string(sortBy), "-") - ret := elastic.NewFieldSort(field) - if strings.HasPrefix(string(sortBy), "-") { - ret.Desc() - } - return ret +func parseSortBy(sortBy internal.SortBy) es.SortField { + field, desc := strings.CutPrefix(string(sortBy), "-") + return es.SortField{Field: field, Desc: desc} } diff --git a/modules/indexer/issues/elasticsearch/elasticsearch_test.go b/modules/indexer/issues/elasticsearch/elasticsearch_test.go index cb9ed3889d..e4f23f3417 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch_test.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch_test.go @@ -6,30 +6,39 @@ package elasticsearch import ( "fmt" "net/http" - "os" + "net/url" "testing" "time" "code.gitea.io/gitea/modules/indexer/issues/internal/tests" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/require" ) func TestElasticsearchIndexer(t *testing.T) { // The elasticsearch instance started by pull-db-tests.yml > test-unit > services > elasticsearch - url := "http://elastic:changeme@elasticsearch:9200" + rawURL := test.ExternalServiceHTTP(t, "TEST_ELASTICSEARCH_URL", "http://elastic:changeme@elasticsearch:9200") - if os.Getenv("CI") == "" { - // Make it possible to run tests against a local elasticsearch instance - url = os.Getenv("TEST_ELASTICSEARCH_URL") - if url == "" { - t.Skip("TEST_ELASTICSEARCH_URL not set and not running in CI") - return - } - } + // Go's net/http does not auto-attach URL userinfo as Basic Auth, so extract + // it and set the header explicitly; otherwise auth-enforced clusters answer + // 401 and the probe never reports ready. + parsed, err := url.Parse(rawURL) + require.NoError(t, err) + user := parsed.User + parsed.User = nil + probeURL := parsed.String() require.Eventually(t, func() bool { - resp, err := http.Get(url) + req, err := http.NewRequest(http.MethodGet, probeURL, nil) + if err != nil { + return false + } + if user != nil { + pass, _ := user.Password() + req.SetBasicAuth(user.Username(), pass) + } + resp, err := http.DefaultClient.Do(req) if err != nil { return false } @@ -37,7 +46,7 @@ func TestElasticsearchIndexer(t *testing.T) { return resp.StatusCode == http.StatusOK }, time.Minute, time.Second, "Expected elasticsearch to be up") - indexer := NewIndexer(url, fmt.Sprintf("test_elasticsearch_indexer_%d", time.Now().Unix())) + indexer := NewIndexer(rawURL, fmt.Sprintf("test_elasticsearch_indexer_%d", time.Now().Unix())) defer indexer.Close() tests.TestIndexer(t, indexer) diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 3e38ac49b7..a67e96c0a2 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -416,28 +416,42 @@ func searchIssueInProject(t *testing.T) { }{ { SearchOptions{ - ProjectID: optional.Some(int64(1)), + ProjectIDs: []int64{1}, }, []int64{5, 3, 2, 1}, }, - { - SearchOptions{ - ProjectColumnID: optional.Some(int64(1)), - }, - []int64{1}, - }, - { - SearchOptions{ - ProjectColumnID: optional.Some(int64(0)), // issue with in default column - }, - []int64{2}, - }, } for _, test := range tests { issueIDs, _, err := SearchIssues(t.Context(), &test.opts) require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } + + // Test filtering for issues with no project assigned using dynamic validation + t.Run("no project assigned", func(t *testing.T) { + issueIDs, total, err := SearchIssues(t.Context(), &SearchOptions{ + ProjectIDs: []int64{db.NoConditionID}, + }) + require.NoError(t, err) + assert.NotEmpty(t, issueIDs) + assert.Equal(t, total, int64(len(issueIDs))) + + // Verify each returned issue actually has no project + for _, issueID := range issueIDs { + issue, err := issues.GetIssueByID(t.Context(), issueID) + require.NoError(t, err) + err = issue.LoadProjects(t.Context()) + require.NoError(t, err) + assert.Empty(t, issue.Projects, "Issue %d should have no projects", issueID) + } + + // Count total issues with no project to verify we got them all + allIssues, err := issues.Issues(t.Context(), &issues.IssuesOptions{ + ProjectIDs: []int64{db.NoConditionID}, + }) + require.NoError(t, err) + assert.Len(t, issueIDs, len(allIssues), "Should return all issues with no project") + }) } func searchIssueWithPaginator(t *testing.T) { diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 0d4f0f727d..84979a8e64 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -30,8 +30,9 @@ type IndexerData struct { LabelIDs []int64 `json:"label_ids"` NoLabel bool `json:"no_label"` // True if LabelIDs is empty MilestoneID int64 `json:"milestone_id"` - ProjectID int64 `json:"project_id"` - ProjectColumnID int64 `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible + ProjectIDs []int64 `json:"project_ids"` + NoProject bool `json:"no_project"` // True if ProjectIDs is empty + ProjectColumnMap map[int64]int64 `json:"project_column_map,omitempty"` // Maps project ID to column ID for each project the issue is in PosterID int64 `json:"poster_id"` AssigneeID int64 `json:"assignee_id"` MentionIDs []int64 `json:"mention_ids"` @@ -94,8 +95,8 @@ type SearchOptions struct { MilestoneIDs []int64 // milestones the issues have - ProjectID optional.Option[int64] // project the issues belong to - ProjectColumnID optional.Option[int64] // project column the issues belong to + ProjectIDs []int64 // project the issues belong to. FIXME: ISSUE-MULTIPLE-PROJECTS-FILTER: no multiple project filter support yet. Search logic is wrong. + NoProjectOnly bool // if the issues have no project, if true, ProjectIDs will be ignored PosterID string // poster of the issues, "(none)" or "(any)" or a user ID AssigneeID string // assignee of the issues, "(none)" or "(any)" or a user ID diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index 7aebbbcd58..20e64a5955 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -116,6 +116,16 @@ var cases = []*testIndexerCase{ assert.Equal(t, len(data), int(result.Total)) }, }, + { + // Exercises the single-doc Index/Delete fast path in backends that have one (e.g. Elasticsearch). + Name: "single-doc index", + ExtraData: []*internal.IndexerData{ + {ID: 999, Title: "solo-issue-marker"}, + }, + SearchOptions: &internal.SearchOptions{Keyword: "solo-issue-marker"}, + ExpectedIDs: []int64{999}, + ExpectedTotal: 1, + }, { Name: "Keyword", ExtraData: []*internal.IndexerData{ @@ -301,75 +311,41 @@ var cases = []*testIndexerCase{ }, }, { - Name: "ProjectID", + Name: "ProjectIDs", SearchOptions: &internal.SearchOptions{ Paginator: &db.ListOptions{ PageSize: 5, }, - ProjectID: optional.Some(int64(1)), + ProjectIDs: []int64{1}, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 5) for _, v := range result.Hits { - assert.Equal(t, int64(1), data[v.ID].ProjectID) + assert.Contains(t, data[v.ID].ProjectIDs, int64(1)) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectID == 1 + return slices.Contains(v.ProjectIDs, int64(1)) }), result.Total) }, }, { - Name: "no ProjectID", + Name: "no ProjectIDs (empty array)", SearchOptions: &internal.SearchOptions{ Paginator: &db.ListOptions{ - PageSize: 5, + PageSize: 50, }, - ProjectID: optional.Some(int64(0)), + NoProjectOnly: true, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Len(t, result.Hits, 5) + // Verify only issues with no projects are returned for _, v := range result.Hits { - assert.Equal(t, int64(0), data[v.ID].ProjectID) + assert.Empty(t, data[v.ID].ProjectIDs, "Issue %d should have no projects", v.ID) } - assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectID == 0 - }), result.Total) - }, - }, - { - Name: "ProjectColumnID", - SearchOptions: &internal.SearchOptions{ - Paginator: &db.ListOptions{ - PageSize: 5, - }, - ProjectColumnID: optional.Some(int64(1)), - }, - Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Len(t, result.Hits, 5) - for _, v := range result.Hits { - assert.Equal(t, int64(1), data[v.ID].ProjectColumnID) - } - assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectColumnID == 1 - }), result.Total) - }, - }, - { - Name: "no ProjectColumnID", - SearchOptions: &internal.SearchOptions{ - Paginator: &db.ListOptions{ - PageSize: 5, - }, - ProjectColumnID: optional.Some(int64(0)), - }, - Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Len(t, result.Hits, 5) - for _, v := range result.Hits { - assert.Equal(t, int64(0), data[v.ID].ProjectColumnID) - } - assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.ProjectColumnID == 0 - }), result.Total) + // Verify we got ALL issues with no projects + expectedCount := countIndexerData(data, func(v *internal.IndexerData) bool { + return len(v.ProjectIDs) == 0 + }) + assert.Equal(t, expectedCount, result.Total, "Should return all %d issues with no project", expectedCount) }, }, { @@ -706,6 +682,10 @@ func generateDefaultIndexerData() []*internal.IndexerData { for i := range subscriberIDs { subscriberIDs[i] = int64(i) + 1 // SubscriberID should not be 0 } + projectIDs := make([]int64, id%5) + for i := range projectIDs { + projectIDs[i] = int64(i) + 1 // projectID should not be 0 + } data = append(data, &internal.IndexerData{ ID: id, @@ -719,8 +699,8 @@ func generateDefaultIndexerData() []*internal.IndexerData { LabelIDs: labelIDs, NoLabel: len(labelIDs) == 0, MilestoneID: issueIndex % 4, - ProjectID: issueIndex % 5, - ProjectColumnID: issueIndex % 6, + ProjectIDs: projectIDs, + NoProject: len(projectIDs) == 0, PosterID: id%10 + 1, // PosterID should not be 0 AssigneeID: issueIndex % 10, MentionIDs: mentionIDs, diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go index 5715cf4794..6ac6d239c8 100644 --- a/modules/indexer/issues/meilisearch/meilisearch.go +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -20,7 +20,7 @@ import ( ) const ( - issueIndexerLatestVersion = 4 + issueIndexerLatestVersion = 5 // TODO: make this configurable if necessary maxTotalHits = 10000 @@ -71,8 +71,8 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer { "label_ids", "no_label", "milestone_id", - "project_id", - "project_board_id", + "project_ids", + "no_project", "poster_id", "assignee_id", "mention_ids", @@ -182,11 +182,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.And(inner_meilisearch.NewFilterIn("milestone_id", options.MilestoneIDs...)) } - if options.ProjectID.Has() { - query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value())) - } - if options.ProjectColumnID.Has() { - query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value())) + if options.NoProjectOnly { + query.And(inner_meilisearch.NewFilterEq("no_project", true)) + } else if len(options.ProjectIDs) > 0 { + // FIXME: ISSUE-MULTIPLE-PROJECTS-FILTER: this logic is not right, it should use "AND" but not "OR" + query.And(inner_meilisearch.NewFilterIn("project_ids", options.ProjectIDs...)) } if options.PosterID != "" { diff --git a/modules/indexer/issues/meilisearch/meilisearch_test.go b/modules/indexer/issues/meilisearch/meilisearch_test.go index 81a27487bb..bfc953c9ea 100644 --- a/modules/indexer/issues/meilisearch/meilisearch_test.go +++ b/modules/indexer/issues/meilisearch/meilisearch_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/indexer/issues/internal" "code.gitea.io/gitea/modules/indexer/issues/internal/tests" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/test" "github.com/meilisearch/meilisearch-go" "github.com/stretchr/testify/assert" @@ -21,18 +22,8 @@ import ( func TestMeilisearchIndexer(t *testing.T) { // The meilisearch instance started by pull-db-tests.yml > test-unit > services > meilisearch - url := "http://meilisearch:7700" - key := "" // auth has been disabled in test environment - - if os.Getenv("CI") == "" { - // Make it possible to run tests against a local meilisearch instance - url = os.Getenv("TEST_MEILISEARCH_URL") - if url == "" { - t.Skip("TEST_MEILISEARCH_URL not set and not running in CI") - return - } - key = os.Getenv("TEST_MEILISEARCH_KEY") - } + url := test.ExternalServiceHTTP(t, "TEST_MEILISEARCH_URL", "http://meilisearch:7700") + key := os.Getenv("TEST_MEILISEARCH_KEY") require.Eventually(t, func() bool { resp, err := http.Get(url) diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index 7647be58e8..fafe9b8bbd 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -87,14 +87,9 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD return nil, false, err } - var projectID int64 - if issue.Project != nil { - projectID = issue.Project.ID - } - - projectColumnID, err := issue.ProjectColumnID(ctx) - if err != nil { - return nil, false, err + projectIDs := make([]int64, 0, len(issue.Projects)) + for _, project := range issue.Projects { + projectIDs = append(projectIDs, project.ID) } if err := issue.Repo.LoadOwner(ctx); err != nil { @@ -114,8 +109,8 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD LabelIDs: labels, NoLabel: len(labels) == 0, MilestoneID: issue.MilestoneID, - ProjectID: projectID, - ProjectColumnID: projectColumnID, + ProjectIDs: projectIDs, + NoProject: len(projectIDs) == 0, PosterID: issue.PosterID, AssigneeID: issue.AssigneeID, MentionIDs: mentionIDs, diff --git a/modules/json/jsongoccy.go b/modules/json/jsongoccy.go deleted file mode 100644 index 77ea047fa7..0000000000 --- a/modules/json/jsongoccy.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package json - -import ( - "bytes" - "io" - - "github.com/goccy/go-json" -) - -var _ Interface = jsonGoccy{} - -type jsonGoccy struct{} - -func (jsonGoccy) Marshal(v any) ([]byte, error) { - return json.Marshal(v) -} - -func (jsonGoccy) Unmarshal(data []byte, v any) error { - return json.Unmarshal(data, v) -} - -func (jsonGoccy) NewEncoder(writer io.Writer) Encoder { - return json.NewEncoder(writer) -} - -func (jsonGoccy) NewDecoder(reader io.Reader) Decoder { - return json.NewDecoder(reader) -} - -func (jsonGoccy) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error { - return json.Indent(dst, src, prefix, indent) -} diff --git a/modules/json/jsonlegacy.go b/modules/json/jsonlegacy.go index 156e456041..81d644d4f4 100644 --- a/modules/json/jsonlegacy.go +++ b/modules/json/jsonlegacy.go @@ -6,11 +6,12 @@ package json import ( + "encoding/json" //nolint:depguard // this package wraps it "io" ) func getDefaultJSONHandler() Interface { - return jsonGoccy{} + return jsonV1{} } func MarshalKeepOptionalEmpty(v any) ([]byte, error) { @@ -20,3 +21,5 @@ func MarshalKeepOptionalEmpty(v any) ([]byte, error) { func NewDecoderCaseInsensitive(reader io.Reader) Decoder { return DefaultJSONHandler.NewDecoder(reader) } + +type Value = json.RawMessage diff --git a/modules/json/jsonv2.go b/modules/json/jsonv2.go index 0bba2783bc..c4afc9513b 100644 --- a/modules/json/jsonv2.go +++ b/modules/json/jsonv2.go @@ -8,6 +8,7 @@ package json import ( "bytes" jsonv1 "encoding/json" //nolint:depguard // this package wraps it + "encoding/json/jsontext" //nolint:depguard // this package wraps it jsonv2 "encoding/json/v2" //nolint:depguard // this package wraps it "io" ) @@ -90,3 +91,5 @@ func (d *jsonV2Decoder) Decode(v any) error { func NewDecoderCaseInsensitive(reader io.Reader) Decoder { return &jsonV2Decoder{reader: reader, opts: jsonV2.unmarshalCaseInsensitiveOptions} } + +type Value = jsontext.Value diff --git a/modules/lfs/content_store.go b/modules/lfs/content_store.go index 0d9c0c98ac..be1e6c8e90 100644 --- a/modules/lfs/content_store.go +++ b/modules/lfs/content_store.go @@ -104,7 +104,7 @@ func (s *ContentStore) Verify(pointer Pointer) (bool, error) { } // ReadMetaObject will read a git_model.LFSMetaObject and return a reader -func ReadMetaObject(pointer Pointer) (io.ReadSeekCloser, error) { +func ReadMetaObject(pointer Pointer) (storage.Object, error) { contentStore := NewContentStore() return contentStore.Get(pointer) } diff --git a/modules/log/logger_global.go b/modules/log/logger_global.go index 2bc8c4f449..08ac5d2d5f 100644 --- a/modules/log/logger_global.go +++ b/modules/log/logger_global.go @@ -54,10 +54,6 @@ func ErrorWithSkip(skip int, format string, v ...any) { Log(skip+1, ERROR, format, v...) } -func Critical(format string, v ...any) { - Log(1, ERROR, format, v...) -} - var OsExiter = os.Exit // Fatal records fatal log and exit process @@ -75,12 +71,23 @@ func IsLoggerEnabled(name string) bool { return GetManager().GetLogger(name).IsEnabled() } -func SetConsoleLogger(loggerName, writerName string, level Level) { +func SetupStderrLogger(loggerName, writerName string, level Level) { writer := NewEventWriterConsole(writerName, WriterMode{ - Level: level, - Flags: FlagsFromBits(LstdFlags), - Colorize: CanColorStdout, - WriterOption: WriterConsoleOption{}, + Level: level, + Flags: FlagsFromBits(LstdFlags), + Colorize: CanColorStderr, + + // For most CLI commands, it's better to use Stderr as log output: + // this logger is installed early (app.Before), before subcommands like "dump" redirect logging to stderr. + // If Stdout, early log output (e.g.: warning during config loading) goes to stdout + // and corrupts any command that writes data to stdout (e.g. "gitea dump --file -"). + // + // It is inconsistent with the web server's default console logger from config + // (which will be initialized later and use Stdout by default), but there is no other way at the moment: + // many existing users depend on such behavior to collect web logs (e.g. fail2ban). + // + // Maybe need to refactor the logger system again in the future. + WriterOption: WriterConsoleOption{Stderr: true}, }) GetManager().GetLogger(loggerName).ReplaceAllWriters(writer) } diff --git a/modules/log/logger_impl.go b/modules/log/logger_impl.go index 551c1454aa..b15dc2f28d 100644 --- a/modules/log/logger_impl.go +++ b/modules/log/logger_impl.go @@ -5,6 +5,7 @@ package log import ( "context" + "net/url" "reflect" "runtime" "strings" @@ -226,6 +227,8 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any) } } else if ls := asLogStringer(v); ls != nil { msgArgs[i] = logStringFormatter{v: ls} + } else if str, ok := v.(string); ok { + msgArgs[i] = protectSensitiveInfo(str) } } @@ -235,6 +238,24 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any) l.SendLogEvent(event) } +func protectSensitiveInfo(s string) string { + u, err := url.Parse(s) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + return s + } + q := u.Query() + for _, vals := range q { + for i := range vals { + vals[i] = "_" + } + } + masked := &url.URL{Scheme: u.Scheme, Host: u.Host, Path: u.Path, RawQuery: q.Encode()} + if u.User != nil { + masked.User = url.User("_masked_") + } + return masked.String() +} + func (l *LoggerImpl) GetLevel() Level { return Level(l.level.Load()) } diff --git a/modules/log/logger_test.go b/modules/log/logger_test.go index a74139dc51..6e38610ddd 100644 --- a/modules/log/logger_test.go +++ b/modules/log/logger_test.go @@ -177,3 +177,10 @@ func TestLoggerExpressionFilter(t *testing.T) { assert.Equal(t, []string{"foo\n", "foo bar\n", "by filename\n"}, w1.FetchLogs()) } + +func TestProtectSensitiveInfo(t *testing.T) { + assert.Empty(t, protectSensitiveInfo("")) + assert.Equal(t, "mailto:user@example.com", protectSensitiveInfo("mailto:user@example.com")) + assert.Equal(t, "https://example.com", protectSensitiveInfo("https://example.com")) + assert.Equal(t, "https://_masked_@example.com/path?k=_", protectSensitiveInfo("https://u:p@example.com/path?k=v#hash")) +} diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go index 4d447e301a..4b3c96fd33 100644 --- a/modules/markup/external/external.go +++ b/modules/markup/external/external.go @@ -21,7 +21,33 @@ import ( // RegisterRenderers registers all supported third part renderers according settings func RegisterRenderers() { - markup.RegisterRenderer(&openAPIRenderer{}) + markup.RegisterRenderer(&frontendRenderer{ + name: "openapi-swagger", + patterns: []string{ + "openapi.yaml", + "openapi.yml", + "openapi.json", + "swagger.yaml", + "swagger.yml", + "swagger.json", + }, + }) + + markup.RegisterRenderer(&frontendRenderer{ + name: "viewer-3d", + patterns: []string{ + // It needs more logic to make it overall right (render a text 3D model automatically): + // we need to distinguish the ambiguous filename extensions. + // For example: "*.amf, *.obj, *.off, *.step" might be or not be a 3D model file. + // So when it is a text file, we can't assume that "we only render it by 3D plugin", + // otherwise the end users would be impossible to view its real content when the file is not a 3D model. + "*.3dm", "*.3ds", "*.3mf", "*.amf", "*.bim", "*.brep", + "*.dae", "*.fbx", "*.fcstd", "*.glb", "*.gltf", + "*.ifc", "*.igs", "*.iges", "*.stp", "*.step", + "*.stl", "*.obj", "*.off", "*.ply", "*.wrl", + }, + }) + for _, renderer := range setting.ExternalMarkupRenderers { markup.RegisterRenderer(&Renderer{renderer}) } diff --git a/modules/markup/external/frontend.go b/modules/markup/external/frontend.go new file mode 100644 index 0000000000..7327503d28 --- /dev/null +++ b/modules/markup/external/frontend.go @@ -0,0 +1,95 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package external + +import ( + "encoding/base64" + "io" + "unicode/utf8" + + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/public" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +type frontendRenderer struct { + name string + patterns []string +} + +var ( + _ markup.PostProcessRenderer = (*frontendRenderer)(nil) + _ markup.ExternalRenderer = (*frontendRenderer)(nil) +) + +func (p *frontendRenderer) Name() string { + return p.name +} + +func (p *frontendRenderer) NeedPostProcess() bool { + return false +} + +func (p *frontendRenderer) FileNamePatterns() []string { + // TODO: the file extensions are ambiguous, even if the file name matches, it doesn't mean that the file is a 3D model + // There are some approaches to make it more accurate, but they are all complicated: + // A. Make backend know everything (detect a file is a 3D model or not) + // B. Let frontend renders to try render one by one + // + // If there would be more frontend renders in the future, we need to implement the "frontend" approach: + // 1. Make backend or parent window collect the supported extensions of frontend renders (done: backend external render framework) + // 2. If the current file matches any extension, start the general iframe embedded render (done: this renderer) + // 3. The iframe window calls the frontend renders one by one (done: frontend external render) + // 4. Report the render result to parent by postMessage (TODO: when needed) + return p.patterns +} + +func (p *frontendRenderer) SanitizerRules() []setting.MarkupSanitizerRule { + return nil +} + +func (p *frontendRenderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) { + ret.SanitizerDisabled = true + ret.DisplayInIframe = true + ret.ContentSandbox = "allow-scripts allow-forms allow-modals allow-popups allow-downloads" + return ret +} + +func (p *frontendRenderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { + if ctx.RenderOptions.StandalonePageOptions == nil { + opts := p.GetExternalRendererOptions() + return markup.RenderIFrame(ctx, &opts, output) + } + + content, err := util.ReadWithLimit(input, int(setting.UI.MaxDisplayFileSize)) + if err != nil { + return err + } + + contentEncoding, contentString := "text", util.UnsafeBytesToString(content) + if !utf8.Valid(content) { + contentEncoding = "base64" + contentString = base64.StdEncoding.EncodeToString(content) + } + + _, err = htmlutil.HTMLPrintf(output, + ` + + + + + + +
+ + + +`, + p.name, ctx.RenderOptions.RelativePath, + contentEncoding, contentString, + public.AssetURI("js/external-render-frontend.js")) + return err +} diff --git a/modules/markup/external/openapi.go b/modules/markup/external/openapi.go deleted file mode 100644 index ac5eae53ff..0000000000 --- a/modules/markup/external/openapi.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2026 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package external - -import ( - "fmt" - "html" - "io" - - "code.gitea.io/gitea/modules/markup" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" -) - -type openAPIRenderer struct{} - -var ( - _ markup.PostProcessRenderer = (*openAPIRenderer)(nil) - _ markup.ExternalRenderer = (*openAPIRenderer)(nil) -) - -func (p *openAPIRenderer) Name() string { - return "openapi" -} - -func (p *openAPIRenderer) NeedPostProcess() bool { - return false -} - -func (p *openAPIRenderer) FileNamePatterns() []string { - return []string{ - "openapi.yaml", - "openapi.yml", - "openapi.json", - "swagger.yaml", - "swagger.yml", - "swagger.json", - } -} - -func (p *openAPIRenderer) SanitizerRules() []setting.MarkupSanitizerRule { - return nil -} - -func (p *openAPIRenderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) { - ret.SanitizerDisabled = true - ret.DisplayInIframe = true - ret.ContentSandbox = "" - return ret -} - -func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { - content, err := util.ReadWithLimit(input, int(setting.UI.MaxDisplayFileSize)) - if err != nil { - return err - } - // TODO: can extract this to a tmpl file later - _, err = io.WriteString(output, fmt.Sprintf( - ` - - - - - - -
- - -`, - setting.StaticURLPrefix, - setting.AssetVersion, - html.EscapeString(ctx.RenderOptions.RelativePath), - html.EscapeString(util.UnsafeBytesToString(content)), - setting.StaticURLPrefix, - setting.AssetVersion, - )) - return err -} diff --git a/modules/markup/html.go b/modules/markup/html.go index 1c2ae6918d..0fe37ae305 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -6,6 +6,7 @@ package markup import ( "bytes" "fmt" + "html/template" "io" "regexp" "slices" @@ -149,9 +150,9 @@ func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) e return postProcess(ctx, procs, input, output) } -// PostProcessCommitMessage will use the same logic as PostProcess, but will disable -// the shortLinkProcessor. -func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error) { +// PostProcessCommitMessage will use the same logic as PostProcess, but will disable the shortLinkProcessor. +// FIXME: this function and its family have a very strange design: it takes HTML as input and output, processes the "escaped" content. +func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) (template.HTML, error) { procs := []processor{ fullIssuePatternProcessor, comparePatternProcessor, @@ -165,7 +166,8 @@ func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error emojiProcessor, emojiShortCodeProcessor, } - return postProcessString(ctx, procs, content) + s, err := postProcessString(ctx, procs, string(content)) + return template.HTML(s), err } var emojiProcessors = []processor{ diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index e62747c724..4fa9466d19 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/internal/renderinternal.go b/modules/markup/internal/renderinternal.go index 9fd9a1c0e8..370e642454 100644 --- a/modules/markup/internal/renderinternal.go +++ b/modules/markup/internal/renderinternal.go @@ -77,6 +77,7 @@ func (r *RenderInternal) ProtectSafeAttrs(content template.HTML) template.HTML { } func (r *RenderInternal) FormatWithSafeAttrs(w io.Writer, fmt template.HTML, a ...any) error { - _, err := w.Write([]byte(r.ProtectSafeAttrs(htmlutil.HTMLFormat(fmt, a...)))) + htmlStr := r.ProtectSafeAttrs(htmlutil.HTMLFormat(fmt, a...)) + _, err := io.WriteString(w, string(htmlStr)) return err } diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 555a171685..4a560517f2 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -70,6 +70,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa } case *ast.CodeSpan: g.transformCodeSpan(ctx, v, reader) + case *ast.FencedCodeBlock: + g.transformFencedCodeblock(v, reader) case *ast.Blockquote: return g.transformBlockquote(v, reader) } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index cca44a8774..1cc75d763d 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -21,7 +21,6 @@ import ( chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/yuin/goldmark" highlighting "github.com/yuin/goldmark-highlighting/v2" - meta "github.com/yuin/goldmark-meta" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" @@ -75,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() @@ -166,7 +161,6 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender { ParseBlockDollar: setting.Markdown.MathCodeBlockOptions.ParseBlockDollar, ParseBlockSquareBrackets: setting.Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets, // this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping }), - meta.Meta, ), goldmark.WithParserOptions( parser.WithAttribute(), diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 261c4e780c..2f14a0fae9 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -429,9 +429,12 @@ test --- test `, - `- item1 -- item2 - + `
+
    +
  • item1
  • +
  • item2
  • +
+

test

`, }, @@ -443,8 +446,8 @@ anything --- test `, - `anything - + `
+

anything

test

`, }, @@ -471,14 +474,26 @@ foo: bar
  • task 1
+`, + }, + // we have our own frontmatter parser, don't need to use github.com/yuin/goldmark-meta + { + "InvalidFrontmatter", + `--- +foo +`, + `
+

foo

`, }, } - for _, test := range testcases { - res, err := markdown.RenderString(markup.NewTestRenderContext(), test.input) - assert.NoError(t, err, "Unexpected error in testcase: %q", test.name) - assert.Equal(t, test.expected, string(res), "Unexpected result in testcase %q", test.name) + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + res, err := markdown.RenderString(markup.NewTestRenderContext(), tt.input) + assert.NoError(t, err, "Unexpected error in testcase: %q", tt.name) + assert.Equal(t, tt.expected, string(res), "Unexpected result in testcase %q", tt.name) + }) } } @@ -568,3 +583,39 @@ func TestMarkdownLink(t *testing.T) { assert.Equal(t, `

https://example.com/__init__.py

`, string(result)) } + +func TestMarkdownUlDir(t *testing.T) { + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, false)() + result, err := markdown.RenderString(markup.NewTestRenderContext(), ` +* a + * b +`) + assert.NoError(t, err) + assert.Equal(t, `
    +
  • a +
      +
    • b
    • +
    +
  • +
+`, string(result)) +} + +func TestMarkdownFencedCodeBlock(t *testing.T) { + testRender := func(input, expected string) { + buffer, err := markdown.RenderString(markup.NewTestRenderContext(), input) + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) + } + const nl = "\n" + const prefix = `
`
+	const suffix = `
` + + testRender("```\ncode\n```", prefix+`code`+nl+``+suffix) + + const jsCommon = prefix + `code` + nl + `` + suffix + testRender("```js\ncode\n```", jsCommon) + testRender("```js:app.ts\ncode\n```", jsCommon) + testRender("```js,ignore\ncode\n```", jsCommon) + testRender("```js ignore\ncode\n```", jsCommon) +} diff --git a/modules/markup/markdown/meta.go b/modules/markup/markdown/meta.go index e76b253ecd..6ddd892110 100644 --- a/modules/markup/markdown/meta.go +++ b/modules/markup/markdown/meta.go @@ -60,8 +60,8 @@ func ExtractMetadata(contents string, out any) (string, error) { return string(body), err } -// ExtractMetadata consumes a markdown file, parses YAML frontmatter, -// and returns the frontmatter metadata separated from the markdown content +// ExtractMetadataBytes consumes a Markdown content, parses YAML frontmatter, +// and returns the frontmatter metadata separated from the Markdown content func ExtractMetadataBytes(contents []byte, out any) ([]byte, error) { var front, body []byte diff --git a/modules/markup/markdown/transform_codeblock.go b/modules/markup/markdown/transform_codeblock.go new file mode 100644 index 0000000000..de9264c4c4 --- /dev/null +++ b/modules/markup/markdown/transform_codeblock.go @@ -0,0 +1,32 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markdown + +import ( + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/text" +) + +func (g *ASTTransformer) transformFencedCodeblock(v *ast.FencedCodeBlock, reader text.Reader) { + // * Some engines support a meta syntax for appending the filename after the language, separated by a colon + // * https://www.glukhov.org/documentation-tools/markdown/markdown-codeblocks/ + // * Some engines support additional "options" after the language, separated by a space or comma: ```rust,ignore``` + // * https://docs.readme.com/rdmd/docs/code-blocks + // * https://next-book.vercel.app/reference/fencedcode + if v.Info == nil { + return + } + info := v.Info.Segment.Value(reader.Source()) + newEnd := -1 + for i, b := range info { + if b == ' ' || b == ',' || b == ':' { + newEnd = i + break + } + } + if newEnd != -1 { + start := v.Info.Segment.Start + v.Info = ast.NewTextSegment(text.NewSegment(start, start+newEnd)) + } +} diff --git a/modules/markup/markdown/transform_list.go b/modules/markup/markdown/transform_list.go index c89ad2f2cf..6cafa8ff78 100644 --- a/modules/markup/markdown/transform_list.go +++ b/modules/markup/markdown/transform_list.go @@ -81,5 +81,16 @@ func (g *ASTTransformer) transformList(_ *markup.RenderContext, v *ast.List, rc v.AppendChild(v, newChild) } } - g.applyElementDir(v) + + nestedList := false + for p := v.Parent(); p != nil; p = p.Parent() { + if _, ok := p.(*ast.List); ok { + nestedList = true + break + } + } + if !nestedList { + // "dir=auto" should be only added to top-level "ul". https://github.com/go-gitea/gitea/issues/35058 + g.applyElementDir(v) + } } diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index fd3071645a..ea331672bf 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -106,31 +106,27 @@ func (r *orgWriter) resolveLink(link string) string { // WriteRegularLink renders images, links or videos func (r *orgWriter) WriteRegularLink(l org.RegularLink) { link := r.resolveLink(l.URL) - - printHTML := func(html template.HTML, a ...any) { - _, _ = fmt.Fprint(r, htmlutil.HTMLFormat(html, a...)) - } // Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427 switch l.Kind() { case "image": if l.Description == nil { - printHTML(`%s`, link, link) + _, _ = htmlutil.HTMLPrintf(r, `%s`, link, link) } else { imageSrc := r.resolveLink(org.String(l.Description...)) - printHTML(`%s`, link, imageSrc, imageSrc) + _, _ = htmlutil.HTMLPrintf(r, `%s`, link, imageSrc, imageSrc) } case "video": if l.Description == nil { - printHTML(``, link, link) + _, _ = htmlutil.HTMLPrintf(r, ``, link, link) } else { videoSrc := r.resolveLink(org.String(l.Description...)) - printHTML(``, link, videoSrc, videoSrc) + _, _ = htmlutil.HTMLPrintf(r, ``, link, videoSrc, videoSrc) } default: var description any = link if l.Description != nil { description = template.HTML(r.WriteNodesAsString(l.Description...)) // orgmode HTMLWriter outputs HTML content } - printHTML(`%s`, link, description) + _, _ = htmlutil.HTMLPrintf(r, `%s`, link, description) } } diff --git a/modules/markup/render.go b/modules/markup/render.go index 5785dc5ad5..6e8838d49f 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -6,6 +6,7 @@ package markup import ( "bytes" "context" + "errors" "fmt" "html/template" "io" @@ -16,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/markup/internal" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" @@ -37,6 +39,15 @@ var RenderBehaviorForTesting struct { DisableAdditionalAttributes bool } +type WebThemeInterface interface { + PublicAssetURI() string +} + +type StandalonePageOptions struct { + CurrentWebTheme WebThemeInterface + RenderQueryString string +} + type RenderOptions struct { UseAbsoluteLink bool @@ -54,7 +65,7 @@ type RenderOptions struct { Metas map[string]string // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page - InStandalonePage bool + StandalonePageOptions *StandalonePageOptions // EnableHeadingIDGeneration controls whether to auto-generate IDs for HTML headings without id attribute. // This should be enabled for repository files and wiki pages, but disabled for comments to avoid duplicate IDs. @@ -126,8 +137,8 @@ func (ctx *RenderContext) WithMetas(metas map[string]string) *RenderContext { return ctx } -func (ctx *RenderContext) WithInStandalonePage(v bool) *RenderContext { - ctx.RenderOptions.InStandalonePage = v +func (ctx *RenderContext) WithStandalonePage(opts StandalonePageOptions) *RenderContext { + ctx.RenderOptions.StandalonePageOptions = &opts return ctx } @@ -196,20 +207,24 @@ func RenderString(ctx *RenderContext, content string) (string, error) { return buf.String(), nil } -func renderIFrame(ctx *RenderContext, sandbox string, output io.Writer) error { +func RenderIFrame(ctx *RenderContext, opts *ExternalRendererOptions, output io.Writer) error { + ownerName, repoName := ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"] + refSubURL := ctx.RenderOptions.Metas["RefTypeNameSubURL"] + if ownerName == "" || repoName == "" || refSubURL == "" { + setting.PanicInDevOrTesting("RenderIFrame requires user, repo and RefTypeNameSubURL metas") + return errors.New("RenderIFrame requires user, repo and RefTypeNameSubURL metas") + } src := fmt.Sprintf("%s/%s/%s/render/%s/%s", setting.AppSubURL, - url.PathEscape(ctx.RenderOptions.Metas["user"]), - url.PathEscape(ctx.RenderOptions.Metas["repo"]), - util.PathEscapeSegments(ctx.RenderOptions.Metas["RefTypeNameSubURL"]), + url.PathEscape(ownerName), + url.PathEscape(repoName), + ctx.RenderOptions.Metas["RefTypeNameSubURL"], util.PathEscapeSegments(ctx.RenderOptions.RelativePath), ) - - var sandboxAttrValue template.HTML - if sandbox != "" { - sandboxAttrValue = htmlutil.HTMLFormat(`sandbox="%s"`, sandbox) + var extraAttrs template.HTML + if opts.ContentSandbox != "" { + extraAttrs = htmlutil.HTMLFormat(` sandbox="%s"`, opts.ContentSandbox) } - iframe := htmlutil.HTMLFormat(``, src, sandboxAttrValue) - _, err := io.WriteString(output, string(iframe)) + _, err := htmlutil.HTMLPrintf(output, ``, src, extraAttrs) return err } @@ -221,7 +236,7 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) { } } -func getExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, _ bool) { +func GetExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, _ bool) { if externalRender, ok := renderer.(ExternalRenderer); ok { return externalRender.GetExternalRendererOptions(), true } @@ -230,17 +245,23 @@ func getExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { var extraHeadHTML template.HTML - if extOpts, ok := getExternalRendererOptions(renderer); ok && extOpts.DisplayInIframe { - if !ctx.RenderOptions.InStandalonePage { + if extOpts, ok := GetExternalRendererOptions(renderer); ok && extOpts.DisplayInIframe { + if ctx.RenderOptions.StandalonePageOptions == nil { // for an external "DisplayInIFrame" render, it could only output its content in a standalone page // otherwise, a `, ret) + + ret = render(ctx, ExternalRendererOptions{ContentSandbox: "allow"}) + assert.Equal(t, ``, ret) +} diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 77ba8bf4f4..447cf4807e 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -56,6 +56,11 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("src", "autoplay", "controls").OnElements("video") + // Native support of "" + // ATTENTION: it only works with "auto" theme, because "media" query doesn't work with the theme chosen by end user manually. + // For example: browser's color scheme is "dark", but end user chooses "light" theme. Maybe it needs JS to help to make it work. + policy.AllowAttrs("media", "srcset").OnElements("source") + policy.AllowAttrs("loading").OnElements("img") // Allow generally safe attributes (reference: https://github.com/jch/html-pipeline) @@ -86,6 +91,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { "dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary", "details", "caption", "figure", "figcaption", "abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr", + "picture", "source", } // FIXME: Need to handle longdesc in img but there is no easy way to do it policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...) diff --git a/modules/markup/sanitizer_default_test.go b/modules/markup/sanitizer_default_test.go index e5ba018e1b..e66f00c02f 100644 --- a/modules/markup/sanitizer_default_test.go +++ b/modules/markup/sanitizer_default_test.go @@ -58,6 +58,9 @@ func TestSanitizer(t *testing.T) { `my custom URL scheme`, `my custom URL scheme`, `my custom URL scheme`, `my custom URL scheme`, + // picture + `c`, `c`, + // Disallow dangerous url schemes `bad`, `bad`, `bad`, `bad`, diff --git a/modules/migration/file_format.go b/modules/migration/file_format.go index e8b6891ca1..fd6ac45a21 100644 --- a/modules/migration/file_format.go +++ b/modules/migration/file_format.go @@ -12,10 +12,17 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" - "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/santhosh-tekuri/jsonschema/v6" "gopkg.in/yaml.v3" ) +// schemaLoader implements jsonschema.URLLoader +type schemaLoader struct{} + +func (l *schemaLoader) Load(url string) (any, error) { + return openSchema(url) +} + // Load project data from file, with optional validation func Load(filename string, data any, validation bool) error { isJSON := strings.HasSuffix(filename, ".json") @@ -43,7 +50,7 @@ func unmarshal(bs []byte, data any, isJSON bool) error { func getSchema(filename string) (*jsonschema.Schema, error) { c := jsonschema.NewCompiler() - c.LoadURL = openSchema + c.UseLoader(&schemaLoader{}) return c.Compile(filename) } diff --git a/modules/migration/file_format_test.go b/modules/migration/file_format_test.go index da997f645b..fd7db9074b 100644 --- a/modules/migration/file_format_test.go +++ b/modules/migration/file_format_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/santhosh-tekuri/jsonschema/v6" "github.com/stretchr/testify/assert" ) diff --git a/modules/migration/options.go b/modules/migration/options.go index 163aa0cfaa..0f73c55ac4 100644 --- a/modules/migration/options.go +++ b/modules/migration/options.go @@ -40,5 +40,7 @@ type MigrateOptions struct { MirrorInterval string `json:"mirror_interval"` AWSAccessKeyID string - AWSSecretAccessKey string + AWSSecretAccessKey string `json:",omitempty"` + + AWSSecretAccessKeyEncrypted string `json:"aws_secret_access_key_encrypted,omitempty"` } diff --git a/modules/migration/repo.go b/modules/migration/repo.go index 22c2cf6fb3..8f5364fefa 100644 --- a/modules/migration/repo.go +++ b/modules/migration/repo.go @@ -11,6 +11,7 @@ type Repository struct { IsPrivate bool `yaml:"is_private"` IsMirror bool `yaml:"is_mirror"` Description string + Website string CloneURL string `yaml:"clone_url"` // SECURITY: This must be checked to ensure that is safe to be used OriginalURL string `yaml:"original_url"` DefaultBranch string diff --git a/modules/migration/schemas_bindata.go b/modules/migration/schemas_bindata.go index 695c2c1135..d7c7081596 100644 --- a/modules/migration/schemas_bindata.go +++ b/modules/migration/schemas_bindata.go @@ -8,14 +8,15 @@ package migration import ( - "io" "io/fs" "path" "sync" + "code.gitea.io/gitea/modules/assetfs" + _ "embed" - "code.gitea.io/gitea/modules/assetfs" + "github.com/santhosh-tekuri/jsonschema/v6" ) //go:embed bindata.dat @@ -25,6 +26,11 @@ var BuiltinAssets = sync.OnceValue(func() fs.FS { return assetfs.NewEmbeddedFS(bindata) }) -func openSchema(filename string) (io.ReadCloser, error) { - return BuiltinAssets().Open(path.Base(filename)) +func openSchema(filename string) (any, error) { + f, err := BuiltinAssets().Open(path.Base(filename)) + if err != nil { + return nil, err + } + defer f.Close() + return jsonschema.UnmarshalJSON(f) } diff --git a/modules/migration/schemas_dynamic.go b/modules/migration/schemas_dynamic.go index dca109d6af..46db879285 100644 --- a/modules/migration/schemas_dynamic.go +++ b/modules/migration/schemas_dynamic.go @@ -6,14 +6,15 @@ package migration import ( - "io" "net/url" "os" "path" "path/filepath" + + "github.com/santhosh-tekuri/jsonschema/v6" ) -func openSchema(s string) (io.ReadCloser, error) { +func openSchema(s string) (any, error) { u, err := url.Parse(s) if err != nil { return nil, err @@ -34,5 +35,10 @@ func openSchema(s string) (io.ReadCloser, error) { filename = filepath.Join("modules/migration/schemas", basename) } } - return os.Open(filename) + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + return jsonschema.UnmarshalJSON(f) } diff --git a/modules/nosql/manager_leveldb.go b/modules/nosql/manager_leveldb.go index 4d2c90debc..636a2a122b 100644 --- a/modules/nosql/manager_leveldb.go +++ b/modules/nosql/manager_leveldb.go @@ -59,7 +59,7 @@ func (m *Manager) GetLevelDB(connection string) (db *leveldb.DB, err error) { defer func() { recovered = recover() if recovered != nil { - log.Critical("PANIC during GetLevelDB: %v\nStacktrace: %s", recovered, log.Stack(2)) + log.Error("PANIC during GetLevelDB: %v\nStacktrace: %s", recovered, log.Stack(2)) } close(done) }() diff --git a/modules/nosql/manager_redis.go b/modules/nosql/manager_redis.go index 3c82651541..af7c611f5e 100644 --- a/modules/nosql/manager_redis.go +++ b/modules/nosql/manager_redis.go @@ -52,7 +52,7 @@ func (m *Manager) GetRedisClient(connection string) (client redis.UniversalClien defer func() { recovered = recover() if recovered != nil { - log.Critical("PANIC during GetRedisClient: %v\nStacktrace: %s", recovered, log.Stack(2)) + log.Error("PANIC during GetRedisClient: %v\nStacktrace: %s", recovered, log.Stack(2)) } close(done) }() diff --git a/modules/options/options_bindata.go b/modules/options/options_bindata.go index b2321d7eb5..f85f30065e 100644 --- a/modules/options/options_bindata.go +++ b/modules/options/options_bindata.go @@ -10,9 +10,9 @@ package options import ( "sync" - _ "embed" - "code.gitea.io/gitea/modules/assetfs" + + _ "embed" ) //go:embed bindata.dat diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 5124627395..ae15e4ec83 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -140,7 +140,7 @@ type nuspecPackage struct { func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) { archive, err := zip.NewReader(r, size) if err != nil { - return nil, err + return nil, util.NewInvalidArgumentErrorf("unable to parse package meta: %v", err) } for _, file := range archive.File { diff --git a/modules/packages/nuget/symbol_extractor.go b/modules/packages/nuget/symbol_extractor.go index 2eadee5463..5e398151e8 100644 --- a/modules/packages/nuget/symbol_extractor.go +++ b/modules/packages/nuget/symbol_extractor.go @@ -42,7 +42,7 @@ func (l PortablePdbList) Close() { func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) { archive, err := zip.NewReader(r, size) if err != nil { - return nil, err + return nil, util.NewInvalidArgumentErrorf("unable to extract portable pdb: %v", err) } var pdbs PortablePdbList diff --git a/modules/packages/rpm/metadata.go b/modules/packages/rpm/metadata.go index f4f78c2cab..d8ac7ea75f 100644 --- a/modules/packages/rpm/metadata.go +++ b/modules/packages/rpm/metadata.go @@ -46,10 +46,11 @@ type Package struct { } type VersionMetadata struct { - License string `json:"license,omitempty"` - ProjectURL string `json:"project_url,omitempty"` - Summary string `json:"summary,omitempty"` - Description string `json:"description,omitempty"` + License string `json:"license,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + Updates []*Update `json:"updates,omitempty"` } type FileMetadata struct { @@ -296,3 +297,43 @@ func getChangelogs(h *rpmutils.RpmHeader) []*Changelog { } return changelogs } + +type DateAttr struct { + Date string `xml:"date,attr" json:"date"` +} + +type Update struct { + From string `xml:"from,attr" json:"from"` + Status string `xml:"status,attr" json:"status"` + Type string `xml:"type,attr" json:"type"` + Version string `xml:"version,attr" json:"version"` + ID string `xml:"id" json:"id"` + Title string `xml:"title" json:"title"` + Severity string `xml:"severity" json:"severity"` + Description string `xml:"description" json:"description"` + Issued *DateAttr `xml:"issued" json:"issued"` + Updated *DateAttr `xml:"updated" json:"updated"` + References []*Reference `xml:"references>reference" json:"references"` + PkgList []*Collection `xml:"pkglist>collection" json:"pkg_list"` +} + +type Reference struct { + Href string `xml:"href,attr" json:"href"` + ID string `xml:"id,attr" json:"id"` + Title string `xml:"title,attr" json:"title"` + Type string `xml:"type,attr" json:"type"` +} + +type Collection struct { + Short string `xml:"short,attr" json:"short"` + Packages []*UpdatePackage `xml:"package" json:"packages"` +} + +type UpdatePackage struct { + Arch string `xml:"arch,attr" json:"arch"` + Name string `xml:"name,attr" json:"name"` + Release string `xml:"release,attr" json:"release"` + Src string `xml:"src,attr" json:"src"` + Version string `xml:"version,attr" json:"version"` + Filename string `xml:"filename" json:"filename"` +} diff --git a/modules/packages/rubygems/marshal.go b/modules/packages/rubygems/marshal.go index 1505221acc..bf7b7e2c53 100644 --- a/modules/packages/rubygems/marshal.go +++ b/modules/packages/rubygems/marshal.go @@ -91,7 +91,7 @@ func (e *MarshalEncoder) marshal(v any) error { val := reflect.ValueOf(v) typ := reflect.TypeOf(v) - if typ.Kind() == reflect.Ptr { + if typ.Kind() == reflect.Pointer { val = val.Elem() typ = typ.Elem() } diff --git a/modules/packages/swift/metadata.go b/modules/packages/swift/metadata.go index 78925c6e6d..d0137f8dfe 100644 --- a/modules/packages/swift/metadata.go +++ b/modules/packages/swift/metadata.go @@ -47,6 +47,7 @@ type Metadata struct { Keywords []string `json:"keywords,omitempty"` RepositoryURL string `json:"repository_url,omitempty"` License string `json:"license,omitempty"` + LicenseURL string `json:"license_url,omitempty"` Author Person `json:"author"` Manifests map[string]*Manifest `json:"manifests,omitempty"` } @@ -67,7 +68,8 @@ type SoftwareSourceCode struct { Keywords []string `json:"keywords,omitempty"` CodeRepository string `json:"codeRepository,omitempty"` License string `json:"license,omitempty"` - Author Person `json:"author"` + LicenseURL string `json:"licenseURL,omitempty"` + Author *Person `json:"author,omitempty"` ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"` RepositoryURLs []string `json:"repositoryURLs,omitempty"` } @@ -181,26 +183,31 @@ func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) { if err := json.NewDecoder(mr).Decode(&ssc); err != nil { return nil, err } - p.Metadata.Description = ssc.Description p.Metadata.Keywords = ssc.Keywords p.Metadata.License = ssc.License - author := Person{ - Name: ssc.Author.Name, - GivenName: ssc.Author.GivenName, - MiddleName: ssc.Author.MiddleName, - FamilyName: ssc.Author.FamilyName, + p.Metadata.LicenseURL = ssc.LicenseURL + if ssc.Author != nil { + author := Person{ + Name: ssc.Author.Name, + GivenName: ssc.Author.GivenName, + MiddleName: ssc.Author.MiddleName, + FamilyName: ssc.Author.FamilyName, + } + // If Name is not provided, generate it from individual name components + if author.Name == "" { + author.Name = author.String() + } + p.Metadata.Author = author } - // If Name is not provided, generate it from individual name components - if author.Name == "" { - author.Name = author.String() - } - p.Metadata.Author = author p.Metadata.RepositoryURL = ssc.CodeRepository if !validation.IsValidURL(p.Metadata.RepositoryURL) { p.Metadata.RepositoryURL = "" } + if !validation.IsValidURL(p.Metadata.LicenseURL) { + p.Metadata.LicenseURL = "" + } p.RepositoryURLs = ssc.RepositoryURLs } diff --git a/modules/packages/swift/metadata_test.go b/modules/packages/swift/metadata_test.go index 461773cbfc..440bcb9fac 100644 --- a/modules/packages/swift/metadata_test.go +++ b/modules/packages/swift/metadata_test.go @@ -4,11 +4,12 @@ package swift import ( - "archive/zip" "bytes" "strings" "testing" + "code.gitea.io/gitea/modules/test" + "github.com/hashicorp/go-version" "github.com/stretchr/testify/assert" ) @@ -18,36 +19,24 @@ const ( packageVersion = "1.0.1" packageDescription = "Package Description" packageRepositoryURL = "https://gitea.io/gitea/gitea" + packageLicenseURL = "https://opensource.org/license/mit" packageAuthor = "KN4CK3R" packageLicense = "MIT" ) func TestParsePackage(t *testing.T) { - createArchive := func(files map[string][]byte) *bytes.Reader { - var buf bytes.Buffer - zw := zip.NewWriter(&buf) - for filename, content := range files { - w, _ := zw.Create(filename) - w.Write(content) - } - zw.Close() - return bytes.NewReader(buf.Bytes()) - } - t.Run("MissingManifestFile", func(t *testing.T) { - data := createArchive(map[string][]byte{"dummy.txt": {}}) - - p, err := ParsePackage(data, data.Size(), nil) + data := test.WriteZipArchive(map[string]string{"dummy.txt": ""}) + p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil) assert.Nil(t, p) assert.ErrorIs(t, err, ErrMissingManifestFile) }) t.Run("ManifestFileTooLarge", func(t *testing.T) { - data := createArchive(map[string][]byte{ - "Package.swift": make([]byte, maxManifestFileSize+1), + data := test.WriteZipArchive(map[string]string{ + "Package.swift": strings.Repeat("a", maxManifestFileSize+1), }) - - p, err := ParsePackage(data, data.Size(), nil) + p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil) assert.Nil(t, p) assert.ErrorIs(t, err, ErrManifestFileTooLarge) }) @@ -56,12 +45,12 @@ func TestParsePackage(t *testing.T) { content1 := "// swift-tools-version:5.7\n//\n// Package.swift" content2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift" - data := createArchive(map[string][]byte{ - "Package.swift": []byte(content1), - "Package@swift-5.5.swift": []byte(content2), + data := test.WriteZipArchive(map[string]string{ + "Package.swift": content1, + "Package@swift-5.5.swift": content2, }) - p, err := ParsePackage(data, data.Size(), nil) + p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil) assert.NotNil(t, p) assert.NoError(t, err) @@ -77,14 +66,13 @@ func TestParsePackage(t *testing.T) { }) t.Run("WithMetadata", func(t *testing.T) { - data := createArchive(map[string][]byte{ - "Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"), + data := test.WriteZipArchive(map[string]string{ + "Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift", }) p, err := ParsePackage( - data, - data.Size(), - strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`), + bytes.NewReader(data.Bytes()), int64(data.Len()), + strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","licenseURL":"`+packageLicenseURL+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`), ) assert.NotNil(t, p) assert.NoError(t, err) @@ -97,6 +85,7 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, packageDescription, p.Metadata.Description) assert.ElementsMatch(t, []string{"swift", "package"}, p.Metadata.Keywords) assert.Equal(t, packageLicense, p.Metadata.License) + assert.Equal(t, packageLicenseURL, p.Metadata.LicenseURL) assert.Equal(t, packageAuthor, p.Metadata.Author.Name) assert.Equal(t, packageAuthor, p.Metadata.Author.GivenName) assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL) @@ -104,14 +93,13 @@ func TestParsePackage(t *testing.T) { }) t.Run("WithExplicitNameField", func(t *testing.T) { - data := createArchive(map[string][]byte{ - "Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"), + data := test.WriteZipArchive(map[string]string{ + "Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift", }) authorName := "John Doe" p, err := ParsePackage( - data, - data.Size(), + bytes.NewReader(data.Bytes()), int64(data.Len()), strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","author":{"name":"`+authorName+`","givenName":"John","familyName":"Doe"}}`), ) assert.NotNil(t, p) @@ -122,15 +110,30 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, "Doe", p.Metadata.Author.FamilyName) }) + t.Run("WithEmptyJSONMetadata", func(t *testing.T) { + data := test.WriteZipArchive(map[string]string{ + "Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift", + }) + + p, err := ParsePackage( + bytes.NewReader(data.Bytes()), int64(data.Len()), + strings.NewReader(`{}`), + ) + assert.NotNil(t, p) + assert.NoError(t, err) + assert.NotNil(t, p.Metadata) + assert.Empty(t, p.Metadata.Author.Name) + assert.Empty(t, p.RepositoryURLs) + }) + t.Run("NameFieldGeneration", func(t *testing.T) { - data := createArchive(map[string][]byte{ - "Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"), + data := test.WriteZipArchive(map[string]string{ + "Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift", }) // Test with only individual name components - Name should be auto-generated p, err := ParsePackage( - data, - data.Size(), + bytes.NewReader(data.Bytes()), int64(data.Len()), strings.NewReader(`{"author":{"givenName":"John","middleName":"Q","familyName":"Doe"}}`), ) assert.NotNil(t, p) diff --git a/modules/packages/terraform/lock.go b/modules/packages/terraform/lock.go new file mode 100644 index 0000000000..3c326c04e9 --- /dev/null +++ b/modules/packages/terraform/lock.go @@ -0,0 +1,100 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package terraform + +import ( + "context" + "errors" + "io" + "time" + + "code.gitea.io/gitea/models/db" + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +const LockFile = "terraform.lock" + +// LockInfo is the metadata for a terraform lock. +type LockInfo struct { + ID string `json:"ID"` + Operation string `json:"Operation"` + Info string `json:"Info"` + Who string `json:"Who"` + Version string `json:"Version"` + Created time.Time `json:"Created"` + Path string `json:"Path"` +} + +func (l *LockInfo) IsLocked() bool { + return l.ID != "" +} + +func ParseLockInfo(r io.Reader) (*LockInfo, error) { + var lock LockInfo + err := json.NewDecoder(r).Decode(&lock) + if err != nil { + return nil, err + } + // ID is required. Rest is less important. + if lock.ID == "" { + return nil, util.NewInvalidArgumentErrorf("terraform lock is missing an ID") + } + return &lock, nil +} + +// GetLock returns the terraform lock for the given package. +// Lock is empty if no lock exists. +func GetLock(ctx context.Context, packageID int64) (LockInfo, error) { + var lock LockInfo + locks, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypePackage, packageID, LockFile) + if err != nil { + return lock, err + } + if len(locks) == 0 || locks[0].Value == "" { + return lock, nil + } + + err = json.Unmarshal([]byte(locks[0].Value), &lock) + return lock, err +} + +// SetLock sets the terraform lock for the given package. +func SetLock(ctx context.Context, packageID int64, lock *LockInfo) error { + jsonBytes, err := json.Marshal(lock) + if err != nil { + return err + } + + return updateLock(ctx, packageID, string(jsonBytes), builder.Eq{"value": ""}) +} + +// RemoveLock removes the terraform lock for the given package. +func RemoveLock(ctx context.Context, packageID int64) error { + return updateLock(ctx, packageID, "", builder.Neq{"value": ""}) +} + +func updateLock(ctx context.Context, refID int64, value string, cond builder.Cond) error { + pp := packages_model.PackageProperty{RefType: packages_model.PropertyTypePackage, RefID: refID, Name: LockFile} + ok, err := db.GetEngine(ctx).Get(&pp) + if err != nil { + return err + } + if ok { + n, err := db.GetEngine(ctx).Where("ref_type=? AND ref_id=? AND name=?", packages_model.PropertyTypePackage, refID, LockFile).And(cond).Cols("value").Update(&packages_model.PackageProperty{Value: value}) + if err != nil { + return err + } + if n == 0 { + return errors.New("failed to update lock state") + } + + return nil + } + _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, refID, LockFile, value) + return err +} diff --git a/modules/packages/terraform/state.go b/modules/packages/terraform/state.go new file mode 100644 index 0000000000..5763128699 --- /dev/null +++ b/modules/packages/terraform/state.go @@ -0,0 +1,38 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package terraform + +import ( + "io" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" +) + +// Note: this is a subset of the Terraform state file format as the full one has two forms. +// If needed, it can be expanded in the future. + +type State struct { + Serial uint64 `json:"serial"` + Lineage string `json:"lineage"` +} + +// ParseState parses the required parts of Terraform state file +func ParseState(r io.Reader) (*State, error) { + var state State + err := json.NewDecoder(r).Decode(&state) + if err != nil { + return nil, err + } + // Serial starts at 1; 0 means it wasn't set in the state file + if state.Serial == 0 { + return nil, util.NewInvalidArgumentErrorf("state serial is missing") + } + // Lineage should always be set + if state.Lineage == "" { + return nil, util.NewInvalidArgumentErrorf("state lineage is missing") + } + + return &state, nil +} diff --git a/modules/public/manifest.go b/modules/public/manifest.go new file mode 100644 index 0000000000..f807244c89 --- /dev/null +++ b/modules/public/manifest.go @@ -0,0 +1,164 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package public + +import ( + "io" + "path" + "sync" + "sync/atomic" + "time" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +type manifestEntry struct { + File string `json:"file"` + Name string `json:"name"` + IsEntry bool `json:"isEntry"` + CSS []string `json:"css"` +} + +type manifestDataStruct struct { + paths map[string]string // unhashed path -> hashed path + names map[string]string // hashed path -> entry name + modTime int64 + checkTime time.Time +} + +var ( + manifestData atomic.Pointer[manifestDataStruct] + manifestFS = sync.OnceValue(AssetFS) +) + +const manifestPath = "assets/.vite/manifest.json" + +func parseManifest(data []byte) (map[string]string, map[string]string) { + var manifest map[string]manifestEntry + if err := json.Unmarshal(data, &manifest); err != nil { + log.Error("Failed to parse frontend manifest: %v", err) + return nil, nil + } + + paths := make(map[string]string) + names := make(map[string]string) + for _, entry := range manifest { + if !entry.IsEntry || entry.Name == "" { + continue + } + // Build unhashed key from file path: "js/index.js", "css/theme-gitea-dark.css" + dir := path.Dir(entry.File) + ext := path.Ext(entry.File) + key := dir + "/" + entry.Name + ext + paths[key] = entry.File + names[entry.File] = entry.Name + // Map associated CSS files, e.g. "css/index.css" -> "css/index.B3zrQPqD.css" + // FIXME: INCORRECT-VITE-MANIFEST-PARSER: the logic is wrong, Vite manifest doesn't work this way + // It just happens to be correct for the current modules dependencies + for _, css := range entry.CSS { + cssKey := path.Dir(css) + "/" + entry.Name + path.Ext(css) + paths[cssKey] = css + names[css] = entry.Name + } + } + return paths, names +} + +func reloadManifest(existingData *manifestDataStruct) *manifestDataStruct { + now := time.Now() + data := existingData + if data != nil && now.Sub(data.checkTime) < time.Second { + // a single request triggers multiple calls to getHashedPath + // do not check the manifest file too frequently + return data + } + + f, err := manifestFS().Open(manifestPath) + if err != nil { + log.Error("Failed to open frontend manifest: %v", err) + return data + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + log.Error("Failed to stat frontend manifest: %v", err) + return data + } + + needReload := data == nil || fi.ModTime().UnixNano() != data.modTime + if !needReload { + return data + } + manifestContent, err := io.ReadAll(f) + if err != nil { + log.Error("Failed to read frontend manifest: %v", err) + return data + } + return storeManifestFromBytes(manifestContent, fi.ModTime().UnixNano(), now) +} + +func storeManifestFromBytes(manifestContent []byte, modTime int64, checkTime time.Time) *manifestDataStruct { + paths, names := parseManifest(manifestContent) + data := &manifestDataStruct{ + paths: paths, + names: names, + modTime: modTime, + checkTime: checkTime, + } + manifestData.Store(data) + return data +} + +func getManifestData() *manifestDataStruct { + data := manifestData.Load() + + // In production the manifest is immutable (embedded in the binary). + // In dev mode, check if it changed on disk (for watch-frontend). + if data == nil || !setting.IsProd { + data = reloadManifest(data) + } + if data == nil { + data = &manifestDataStruct{} + } + return data +} + +// AssetURI returns the URI for a frontend asset. +// It may return a relative path or a full URL depending on the StaticURLPrefix setting. +// In Vite dev mode, known entry points are mapped to their source paths +// so the reverse proxy serves them from the Vite dev server. +// In production, it resolves the content-hashed path from the manifest. +func AssetURI(originPath string) string { + if IsViteDevMode() { + if src := viteDevSourceURL(originPath); src != "" { + return src + } + // it should be caused by incorrect vite config + setting.PanicInDevOrTesting("Failed to locate local path for managed asset URI: %s", originPath) + } + + // Try to resolve an unhashed asset path (origin path) to its content-hashed path from the frontend manifest. + // Example: "js/index.js" -> "js/index.C6Z2MRVQ.js" + data := getManifestData() + assetPath := data.paths[originPath] + if assetPath == "" { + // it should be caused by either: "incorrect vite config" or "user's custom theme" + assetPath = originPath + if !setting.IsProd { + log.Warn("Failed to find managed asset URI for origin path: %s", originPath) + } + } + + return setting.StaticURLPrefix + "/assets/" + assetPath +} + +// AssetNameFromHashedPath returns the asset entry name for a given hashed asset path. +// Example: returns "theme-gitea-dark" for "css/theme-gitea-dark.CyAaQnn5.css". +// Returns empty string if the path is not found in the manifest. +func AssetNameFromHashedPath(hashedPath string) string { + return getManifestData().names[hashedPath] +} diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go new file mode 100644 index 0000000000..acfeaa6dbe --- /dev/null +++ b/modules/public/manifest_test.go @@ -0,0 +1,80 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package public + +import ( + "testing" + "time" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestViteManifest(t *testing.T) { + defer test.MockVariableValue(&setting.IsProd, true)() + + const testManifest = `{ + "web_src/js/index.ts": { + "file": "js/index.C6Z2MRVQ.js", + "name": "index", + "src": "web_src/js/index.ts", + "isEntry": true, + "css": ["css/index.B3zrQPqD.css"] + }, + "web_src/css/themes/theme-gitea-dark.css": { + "file": "css/theme-gitea-dark.CyAaQnn5.css", + "name": "theme-gitea-dark", + "src": "web_src/css/themes/theme-gitea-dark.css", + "isEntry": true + }, + "web_src/js/features/eventsource.sharedworker.ts": { + "file": "js/eventsource.sharedworker.Dug1twio.js", + "name": "eventsource.sharedworker", + "src": "web_src/js/features/eventsource.sharedworker.ts", + "isEntry": true + }, + "_chunk.js": { + "file": "js/chunk.abc123.js", + "name": "chunk" + } +}` + + t.Run("EmptyManifest", func(t *testing.T) { + storeManifestFromBytes([]byte(``), 0, time.Now()) + assert.Equal(t, "/assets/js/index.js", AssetURI("js/index.js")) + assert.Equal(t, "/assets/css/theme-gitea-dark.css", AssetURI("css/theme-gitea-dark.css")) + assert.Equal(t, "", AssetNameFromHashedPath("css/no-such-file.css")) + }) + + t.Run("ParseManifest", func(t *testing.T) { + storeManifestFromBytes([]byte(testManifest), 0, time.Now()) + paths, names := manifestData.Load().paths, manifestData.Load().names + + // JS entries + assert.Equal(t, "js/index.C6Z2MRVQ.js", paths["js/index.js"]) + assert.Equal(t, "js/eventsource.sharedworker.Dug1twio.js", paths["js/eventsource.sharedworker.js"]) + + // Associated CSS from JS entries + assert.Equal(t, "css/index.B3zrQPqD.css", paths["css/index.css"]) + + // CSS-only entries + assert.Equal(t, "css/theme-gitea-dark.CyAaQnn5.css", paths["css/theme-gitea-dark.css"]) + + // Non-entry chunks should not be included + assert.Empty(t, paths["js/chunk.js"]) + + // Names: hashed path -> entry name + assert.Equal(t, "index", names["js/index.C6Z2MRVQ.js"]) + assert.Equal(t, "index", names["css/index.B3zrQPqD.css"]) + assert.Equal(t, "theme-gitea-dark", names["css/theme-gitea-dark.CyAaQnn5.css"]) + assert.Equal(t, "eventsource.sharedworker", names["js/eventsource.sharedworker.Dug1twio.js"]) + + // Test Asset related functions + assert.Equal(t, "/assets/js/index.C6Z2MRVQ.js", AssetURI("js/index.js")) + assert.Equal(t, "/assets/css/theme-gitea-dark.CyAaQnn5.css", AssetURI("css/theme-gitea-dark.css")) + assert.Equal(t, "theme-gitea-dark", AssetNameFromHashedPath("css/theme-gitea-dark.CyAaQnn5.css")) + }) +} diff --git a/modules/public/mime_types.go b/modules/public/mime_types.go index fef85d77cb..fa4691c6a9 100644 --- a/modules/public/mime_types.go +++ b/modules/public/mime_types.go @@ -4,31 +4,36 @@ package public import ( + "mime" "strings" + "sync" ) -// wellKnownMimeTypesLower comes from Golang's builtin mime package: `builtinTypesLower`, see the comment of DetectWellKnownMimeType -var wellKnownMimeTypesLower = map[string]string{ - ".avif": "image/avif", - ".css": "text/css; charset=utf-8", - ".gif": "image/gif", - ".htm": "text/html; charset=utf-8", - ".html": "text/html; charset=utf-8", - ".jpeg": "image/jpeg", - ".jpg": "image/jpeg", - ".js": "text/javascript; charset=utf-8", - ".json": "application/json", - ".mjs": "text/javascript; charset=utf-8", - ".pdf": "application/pdf", - ".png": "image/png", - ".svg": "image/svg+xml", - ".wasm": "application/wasm", - ".webp": "image/webp", - ".xml": "text/xml; charset=utf-8", +// wellKnownMimeTypesLower comes from Golang's builtin mime package: `builtinTypesLower`, +// see the comment of DetectWellKnownMimeType +var wellKnownMimeTypesLower = sync.OnceValue(func() map[string]string { + return map[string]string{ + ".avif": "image/avif", + ".css": "text/css; charset=utf-8", + ".gif": "image/gif", + ".htm": "text/html; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json", + ".mjs": "text/javascript; charset=utf-8", + ".pdf": "application/pdf", + ".png": "image/png", + ".svg": "image/svg+xml", + ".wasm": "application/wasm", + ".webp": "image/webp", + ".xml": "text/xml; charset=utf-8", - // well, there are some types missing from the builtin list - ".txt": "text/plain; charset=utf-8", -} + // well, there are some types missing from the builtin list + ".txt": "text/plain; charset=utf-8", + } +}) // DetectWellKnownMimeType will return the mime-type for a well-known file ext name // The purpose of this function is to bypass the unstable behavior of Golang's mime.TypeByExtension @@ -38,5 +43,8 @@ var wellKnownMimeTypesLower = map[string]string{ // DetectWellKnownMimeType makes the Content-Type for well-known files stable. func DetectWellKnownMimeType(ext string) string { ext = strings.ToLower(ext) - return wellKnownMimeTypesLower[ext] + if s, ok := wellKnownMimeTypesLower()[ext]; ok { + return s + } + return mime.TypeByExtension(ext) } diff --git a/modules/public/public.go b/modules/public/public.go index 004aad5f3b..bb4721a48d 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -18,6 +18,8 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + + "github.com/go-chi/cors" ) func CustomAssets() *assetfs.Layer { @@ -28,6 +30,15 @@ func AssetFS() *assetfs.LayeredFS { return assetfs.Layered(CustomAssets(), BuiltinAssets()) } +func AssetsCors() func(next http.Handler) http.Handler { + // static assets need to be served for external renders (sandboxed) + return cors.Handler(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"HEAD", "GET"}, + MaxAge: 3600 * 24, + }) +} + // FileHandlerFunc implements the static handler for serving files in "public" assets func FileHandlerFunc() http.HandlerFunc { assetFS := AssetFS() diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go new file mode 100644 index 0000000000..615090b05d --- /dev/null +++ b/modules/public/vitedev.go @@ -0,0 +1,205 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package public + +import ( + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "strings" + "sync/atomic" + "time" + + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web/routing" +) + +const viteDevPortFile = "public/assets/.vite/dev-port" + +var viteDevProxy atomic.Pointer[httputil.ReverseProxy] + +func getViteDevServerBaseURL() string { + portFile := filepath.Join(setting.StaticRootPath, viteDevPortFile) + portContent, _ := os.ReadFile(portFile) + port := strings.TrimSpace(string(portContent)) + if port == "" { + return "" + } + return "http://localhost:" + port +} + +func getViteDevProxy() *httputil.ReverseProxy { + if proxy := viteDevProxy.Load(); proxy != nil { + return proxy + } + + viteDevServerBaseURL := getViteDevServerBaseURL() + if viteDevServerBaseURL == "" { + return nil + } + + target, err := url.Parse(viteDevServerBaseURL) + if err != nil { + log.Error("Failed to parse Vite dev server base URL %s, err: %v", viteDevServerBaseURL, err) + return nil + } + + // there is a strange error log (from Golang's HTTP package) + // 2026/03/28 19:50:13 modules/log/misc.go:72:(*loggerToWriter).Write() [I] Unsolicited response received on idle HTTP channel starting with "HTTP/1.1 400 Bad Request\r\n\r\n"; err= + // maybe it is caused by that the Vite dev server doesn't support keep-alive connections? or different keep-alive timeouts? + transport := &http.Transport{ + IdleConnTimeout: 5 * time.Second, + ResponseHeaderTimeout: 5 * time.Second, + } + log.Info("Proxying Vite dev server requests to %s", target) + proxy := &httputil.ReverseProxy{ + Transport: transport, + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(target) + r.Out.Host = target.Host + }, + ModifyResponse: func(resp *http.Response) error { + // add a header to indicate the Vite dev server port, + // make developers know that this request is proxied to Vite dev server and which port it is + resp.Header.Add("X-Gitea-Vite-Dev-Server", viteDevServerBaseURL) + return nil + }, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + if r.Context().Err() != nil { + return // request cancelled (e.g. client disconnected), silently ignore + } + log.Error("Error proxying to Vite dev server: %v", err) + http.Error(w, "Error proxying to Vite dev server: "+err.Error(), http.StatusBadGateway) + }, + } + viteDevProxy.Store(proxy) + return proxy +} + +// ViteDevMiddleware proxies matching requests to the Vite dev server. +// It is registered as middleware in non-production mode and lazily discovers +// 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) + return + } + proxy := getViteDevProxy() + if proxy == nil { + next.ServeHTTP(resp, req) + return + } + markLongPolling(proxy).ServeHTTP(resp, req) + }) +} + +var viteDevModeCheck atomic.Pointer[struct { + isDev bool + time time.Time +}] + +// IsViteDevMode returns true if the Vite dev server port file exists and the server is alive +func IsViteDevMode() bool { + if setting.IsProd { + return false + } + + now := time.Now() + lastCheck := viteDevModeCheck.Load() + if lastCheck != nil && time.Now().Sub(lastCheck.time) < time.Second { + return lastCheck.isDev + } + + viteDevServerBaseURL := getViteDevServerBaseURL() + if viteDevServerBaseURL == "" { + return false + } + + req := httplib.NewRequest(viteDevServerBaseURL+"/web_src/js/__vite_dev_server_check", "GET") + resp, _ := req.Response() + if resp != nil { + _ = resp.Body.Close() + } + isDev := resp != nil && resp.StatusCode == http.StatusOK + viteDevModeCheck.Store(&struct { + isDev bool + time time.Time + }{ + isDev: isDev, + time: now, + }) + return isDev +} + +func detectWebSrcPath(webSrcPath string) string { + localPath := util.FilePathJoinAbs(setting.StaticRootPath, "web_src", webSrcPath) + if _, err := os.Stat(localPath); err == nil { + return setting.AppSubURL + "/web_src/" + webSrcPath + } + return "" +} + +func viteDevSourceURL(name string) string { + if strings.HasPrefix(name, "css/theme-") { + // Only redirect built-in themes to Vite source; custom themes are served from custom/public/assets/css/ + themeFilePath := "css/themes/" + strings.TrimPrefix(name, "css/") + if srcPath := detectWebSrcPath(themeFilePath); srcPath != "" { + return srcPath + } + } + // try to map ".js" files to ".ts" files + pathPrefix, ok := strings.CutSuffix(name, ".js") + if ok { + if srcPath := detectWebSrcPath(pathPrefix + ".ts"); srcPath != "" { + return srcPath + } + } + // for all others that the names match + return detectWebSrcPath(name) +} + +// isViteDevRequest returns true if the request should be proxied to the Vite dev server. +// Ref: Vite source packages/vite/src/node/constants.ts and packages/vite/src/shared/constants.ts +func isViteDevRequest(req *http.Request) bool { + if req.Header.Get("Upgrade") == "websocket" { + wsProtocol := req.Header.Get("Sec-WebSocket-Protocol") + return wsProtocol == "vite-hmr" || wsProtocol == "vite-ping" + } + path := req.URL.Path + + // vite internal requests + if strings.HasPrefix(path, "/@vite/") /* HMR client */ || + strings.HasPrefix(path, "/@fs/") /* out-of-root file access, see vite.config.ts: fs.allow */ || + strings.HasPrefix(path, "/@id/") /* virtual modules */ { + return true + } + + // local source requests (VITE-DEV-SERVER-SECURITY: don't serve sensitive files outside the allowed paths) + if strings.HasPrefix(path, "/node_modules/") || + strings.HasPrefix(path, "/public/assets/") || + strings.HasPrefix(path, "/web_src/") { + return true + } + + // Vite uses a path relative to project root and adds "?import" to non-JS/CSS asset imports: + // - {WebSite}/public/assets/... (e.g. SVG icons from "{RepoRoot}/public/assets/img/svg/") + // - {WebSite}/assets/.json: exception for frontend-imported repo-root assets: + // - KEEP IN MIND: all static frontend assets are served from "{AssetFS}/assets" to "{WebSite}/assets" by Gitea Web Server + // - "{AssetFS}" is a layered filesystem from "{RepoRoot}/public" or embedded assets, and user's custom files in "{CustomPath}/public" + // - "{RepoRoot}/assets/*.json" just happens to live under the dir name "assets"; it is not related to frontend assets + // - BAD DESIGN: indeed it is a "conflicted and polluted name" sample + switch path { + case "/assets/emoji.json", "/assets/codemirror-languages.json": + return true + } + return false +} diff --git a/modules/queue/base_redis_test.go b/modules/queue/base_redis_test.go index 6478988d7f..f4a6af12d1 100644 --- a/modules/queue/base_redis_test.go +++ b/modules/queue/base_redis_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/nosql" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -57,10 +58,10 @@ func TestBaseRedis(t *testing.T) { }() if !waitRedisReady("redis://127.0.0.1:6379/0", 0) { redisServer = redisServerCmd(t) - if redisServer == nil && os.Getenv("CI") == "" { - t.Skip("redis-server not found") - return + if redisServer == nil && test.AllowSkipExternalService() { + t.Skip("redis server command not found, skipped") } + require.NotNil(t, redisServer) assert.NoError(t, redisServer.Start()) require.True(t, waitRedisReady("redis://127.0.0.1:6379/0", 5*time.Second), "start redis-server") } diff --git a/modules/queue/manager_test.go b/modules/queue/manager_test.go index fda498cc84..f9f9b7310b 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/repository/branch.go b/modules/repository/branch.go index 0a8f7cc464..8dda3f82d7 100644 --- a/modules/repository/branch.go +++ b/modules/repository/branch.go @@ -97,7 +97,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, RepoID: repo.ID, Name: branch, CommitID: commit.ID.String(), - CommitMessage: commit.Summary(), + CommitMessage: commit.MessageTitle(), PusherID: doerID, CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), }) @@ -112,7 +112,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, RepoID: repo.ID, Name: branch, CommitID: commit.ID.String(), - CommitMessage: commit.Summary(), + CommitMessage: commit.MessageTitle(), PusherID: doerID, CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), }) diff --git a/modules/repository/commits.go b/modules/repository/commits.go index a3e253e998..32550a9f03 100644 --- a/modules/repository/commits.go +++ b/modules/repository/commits.go @@ -152,7 +152,7 @@ func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string { func CommitToPushCommit(commit *git.Commit) *PushCommit { return &PushCommit{ Sha1: commit.ID.String(), - Message: commit.Message(), + Message: commit.MessageUTF8(), AuthorEmail: commit.Author.Email, AuthorName: commit.Author.Name, CommitterEmail: commit.Committer.Email, diff --git a/modules/repository/commits_test.go b/modules/repository/commits_test.go index 04c0711828..46db7b028b 100644 --- a/modules/repository/commits_test.go +++ b/modules/repository/commits_test.go @@ -145,7 +145,7 @@ func TestCommitToPushCommit(t *testing.T) { ID: sha1, Author: sig, Committer: sig, - CommitMessage: "Commit Message", + CommitMessage: git.CommitMessage{MessageRaw: "Commit Message"}, }) assert.Equal(t, hexString, pushCommit.Sha1) assert.Equal(t, "Commit Message", pushCommit.Message) @@ -176,13 +176,13 @@ func TestListToPushCommits(t *testing.T) { ID: hash1, Author: sig, Committer: sig, - CommitMessage: "Message1", + CommitMessage: git.CommitMessage{MessageRaw: "Message1"}, }, { ID: hash2, Author: sig, Committer: sig, - CommitMessage: "Message2", + CommitMessage: git.CommitMessage{MessageRaw: "Message2"}, }, } diff --git a/modules/session/redis.go b/modules/session/redis.go index 083869f4e1..f5cac8e636 100644 --- a/modules/session/redis.go +++ b/modules/session/redis.go @@ -1,18 +1,6 @@ // Copyright 2013 Beego Authors // Copyright 2014 The Macaron Authors // Copyright 2020 The Gitea Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. // SPDX-License-Identifier: Apache-2.0 package session diff --git a/modules/setting/actions.go b/modules/setting/actions.go index 7a91ecb593..0d1bdadc8e 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/database.go b/modules/setting/database.go index 1a4bf64805..edaaf32c14 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -4,40 +4,34 @@ package setting import ( - "errors" - "fmt" - "net" - "net/url" - "os" "path/filepath" - "strings" "time" ) +const defaultSQLiteBusyTimeout = 20 * 1000 + var ( - // SupportedDatabaseTypes includes all XORM supported databases type, sqlite3 maybe added by `database_sqlite3.go` + // SupportedDatabaseTypes includes all XORM supported databases type, sqlite3 maybe added by the tag-controlled drivers SupportedDatabaseTypes = []string{"mysql", "postgres", "mssql"} // DatabaseTypeNames contains the friendly names for all database types - DatabaseTypeNames = map[string]string{"mysql": "MySQL", "postgres": "PostgreSQL", "mssql": "MSSQL", "sqlite3": "SQLite3"} - - // EnableSQLite3 use SQLite3, set by build flag - EnableSQLite3 bool + DatabaseTypeNames = map[string]string{"mysql": "MySQL", "postgres": "PostgreSQL", "mssql": "MSSQL", DatabaseTypeSQLite3: "SQLite3"} // Database holds the database settings Database = struct { - Type DatabaseType - Host string - Name string - User string - Passwd string - Schema string - SSLMode string - Path string + Type DatabaseType + Host string + Name string + User string + Passwd string + Schema string + SSLMode string + Path string + + SQLiteBusyTimeout int + SQLiteJournalMode string + LogSQL bool - MysqlCharset string CharsetCollation string - Timeout int // seconds - SQLiteJournalMode string DBConnectRetries int DBConnectBackoff time.Duration MaxIdleConns int @@ -47,8 +41,8 @@ var ( AutoMigration bool SlowQueryThreshold time.Duration }{ - Timeout: 500, - IterateBufferSize: 50, + IterateBufferSize: 50, + SlowQueryThreshold: 5 * time.Second, } ) @@ -64,15 +58,20 @@ func loadDBSetting(rootCfg ConfigProvider) { Database.Host = sec.Key("HOST").String() Database.Name = sec.Key("NAME").String() Database.User = sec.Key("USER").String() - if len(Database.Passwd) == 0 { - Database.Passwd = sec.Key("PASSWD").String() - } + Database.Passwd = sec.Key("PASSWD").String() + Database.Schema = sec.Key("SCHEMA").String() Database.SSLMode = sec.Key("SSL_MODE").MustString("disable") Database.CharsetCollation = sec.Key("CHARSET_COLLATION").String() Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "gitea.db")) - Database.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500) + + Database.SQLiteBusyTimeout = sec.Key("SQLITE_TIMEOUT").MustInt(defaultSQLiteBusyTimeout) + // mattn driver isn't really affected by this timeout, but other drivers are affected + // the default value was 500 (0.5s), to avoid breaking existing users, make sure the timeout is long enough (at least, 5 seconds) + if Database.SQLiteBusyTimeout < 5000 { + Database.SQLiteBusyTimeout = defaultSQLiteBusyTimeout + } Database.SQLiteJournalMode = sec.Key("SQLITE_JOURNAL_MODE").MustString("") Database.MaxIdleConns = sec.Key("MAX_IDLE_CONNS").MustInt(2) @@ -88,128 +87,16 @@ func loadDBSetting(rootCfg ConfigProvider) { Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10) Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second) Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true) - Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(5 * time.Second) -} - -// DBConnStr returns database connection string -func DBConnStr() (string, error) { - var connStr string - paramSep := "?" - if strings.Contains(Database.Name, paramSep) { - paramSep = "&" - } - switch Database.Type { - case "mysql": - connType := "tcp" - if len(Database.Host) > 0 && Database.Host[0] == '/' { // looks like a unix socket - connType = "unix" - } - tls := Database.SSLMode - if tls == "disable" { // allow (Postgres-inspired) default value to work in MySQL - tls = "false" - } - connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%sparseTime=true&tls=%s", - Database.User, Database.Passwd, connType, Database.Host, Database.Name, paramSep, tls) - case "postgres": - connStr = getPostgreSQLConnectionString(Database.Host, Database.User, Database.Passwd, Database.Name, Database.SSLMode) - case "mssql": - host, port := ParseMSSQLHostPort(Database.Host) - connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, Database.Name, Database.User, Database.Passwd) - case "sqlite3": - if !EnableSQLite3 { - return "", errors.New("this Gitea binary was not built with SQLite3 support") - } - if err := os.MkdirAll(filepath.Dir(Database.Path), os.ModePerm); err != nil { - return "", fmt.Errorf("Failed to create directories: %w", err) - } - journalMode := "" - if Database.SQLiteJournalMode != "" { - journalMode = "&_journal_mode=" + Database.SQLiteJournalMode - } - connStr = fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate%s", - Database.Path, Database.Timeout, journalMode) - default: - return "", fmt.Errorf("unknown database type: %s", Database.Type) - } - - return connStr, nil -} - -// parsePostgreSQLHostPort parses given input in various forms defined in -// https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING -// and returns proper host and port number. -func parsePostgreSQLHostPort(info string) (host, port string) { - if h, p, err := net.SplitHostPort(info); err == nil { - host, port = h, p - } else { - // treat the "info" as "host", if it's an IPv6 address, remove the wrapper - host = info - if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { - host = host[1 : len(host)-1] - } - } - - // set fallback values - if host == "" { - host = "127.0.0.1" - } - if port == "" { - port = "5432" - } - return host, port -} - -func getPostgreSQLConnectionString(dbHost, dbUser, dbPasswd, dbName, dbsslMode string) (connStr string) { - dbName, dbParam, _ := strings.Cut(dbName, "?") - host, port := parsePostgreSQLHostPort(dbHost) - connURL := url.URL{ - Scheme: "postgres", - User: url.UserPassword(dbUser, dbPasswd), - Host: net.JoinHostPort(host, port), - Path: dbName, - OmitHost: false, - RawQuery: dbParam, - } - query := connURL.Query() - if strings.HasPrefix(host, "/") { // looks like a unix socket - query.Add("host", host) - connURL.Host = ":" + port - } - query.Set("sslmode", dbsslMode) - connURL.RawQuery = query.Encode() - return connURL.String() -} - -// ParseMSSQLHostPort splits the host into host and port -func ParseMSSQLHostPort(info string) (string, string) { - // the default port "0" might be related to MSSQL's dynamic port, maybe it should be double-confirmed in the future - host, port := "127.0.0.1", "0" - if strings.Contains(info, ":") { - host = strings.Split(info, ":")[0] - port = strings.Split(info, ":")[1] - } else if strings.Contains(info, ",") { - host = strings.Split(info, ",")[0] - port = strings.TrimSpace(strings.Split(info, ",")[1]) - } else if len(info) > 0 { - host = info - } - if host == "" { - host = "127.0.0.1" - } - if port == "" { - port = "0" - } - return host, port + Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(Database.SlowQueryThreshold) } +// DatabaseType FIXME: it is also used directly with "schemas.DBType", so the names must be consistent type DatabaseType string -func (t DatabaseType) String() string { - return string(t) -} +const DatabaseTypeSQLite3 = "sqlite3" func (t DatabaseType) IsSQLite3() bool { - return t == "sqlite3" + return t == DatabaseTypeSQLite3 } func (t DatabaseType) IsMySQL() bool { diff --git a/modules/setting/database_sqlite.go b/modules/setting/database_sqlite.go deleted file mode 100644 index c1037cfb27..0000000000 --- a/modules/setting/database_sqlite.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build sqlite - -// Copyright 2014 The Gogs Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package setting - -import ( - _ "github.com/mattn/go-sqlite3" -) - -func init() { - EnableSQLite3 = true - SupportedDatabaseTypes = append(SupportedDatabaseTypes, "sqlite3") -} diff --git a/modules/setting/incoming_email.go b/modules/setting/incoming_email.go index 4e433dde60..bf9f7f9776 100644 --- a/modules/setting/incoming_email.go +++ b/modules/setting/incoming_email.go @@ -12,10 +12,11 @@ import ( "code.gitea.io/gitea/modules/log" ) +const IncomingEmailTokenPlaceholder = "%{token}" + var IncomingEmail = struct { Enabled bool ReplyToAddress string - TokenPlaceholder string `ini:"-"` Host string Port int UseTLS bool `ini:"USE_TLS"` @@ -28,7 +29,6 @@ var IncomingEmail = struct { }{ Mailbox: "INBOX", DeleteHandledMessage: true, - TokenPlaceholder: "%{token}", MaximumMessageSize: 10485760, } @@ -54,19 +54,10 @@ func checkReplyToAddress() error { return errors.New("name must not be set") } - c := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmail.TokenPlaceholder) - switch c { - case 0: - return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder) - case 1: - default: - return fmt.Errorf("%s must appear only once", IncomingEmail.TokenPlaceholder) + placeholderCount := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmailTokenPlaceholder) + userPart, _, _ := strings.Cut(IncomingEmail.ReplyToAddress, "@") + if placeholderCount != 1 || !strings.Contains(userPart, IncomingEmailTokenPlaceholder) { + return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmailTokenPlaceholder) } - - parts := strings.Split(IncomingEmail.ReplyToAddress, "@") - if !strings.Contains(parts[0], IncomingEmail.TokenPlaceholder) { - return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder) - } - return nil } diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index 7f2d0ae159..3ec21860ae 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -81,10 +81,7 @@ func loadLFSFrom(rootCfg ConfigProvider) error { jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET") LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(jwtSecretBase64) if err != nil { - LFS.JWTSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64() - if err != nil { - return fmt.Errorf("error generating JWT Secret for custom config: %v", err) - } + LFS.JWTSecretBytes, jwtSecretBase64 = generate.NewJwtSecretWithBase64() // Save secret saveCfg, err := rootCfg.PrepareSaving() diff --git a/modules/setting/log.go b/modules/setting/log.go index 59866c7605..764c2a82f4 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -256,7 +256,7 @@ func initLoggerByName(manager *log.LoggerManager, rootCfg ConfigProvider, logger } func InitSQLLoggersForCli(level log.Level) { - log.SetConsoleLogger("xorm", "console", level) + log.SetupStderrLogger("xorm", "console-stderr", level) } func IsAccessLogEnabled() bool { diff --git a/modules/setting/mailer_test.go b/modules/setting/mailer_test.go index f281715973..7e1c988568 100644 --- a/modules/setting/mailer_test.go +++ b/modules/setting/mailer_test.go @@ -6,6 +6,8 @@ package setting import ( "testing" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) @@ -39,3 +41,30 @@ func Test_loadMailerFrom(t *testing.T) { }) } } + +func TestLoadSettingsForInstallMailServiceFlags(t *testing.T) { + defer test.MockVariableValue(&Service)() + defer test.MockVariableValue(&MailService)() + + cfg, err := NewConfigProviderFromData(` +[database] +DB_TYPE = postgres + +[mailer] +ENABLED = true +SMTP_ADDR = 127.0.0.1 +SMTP_PORT = 465 +FROM = noreply@example.com + +[service] +REGISTER_EMAIL_CONFIRM = true +ENABLE_NOTIFY_MAIL = true +`) + assert.NoError(t, err) + loadDBSetting(cfg) + loadServiceFrom(cfg) + loadMailsFrom(cfg) + + assert.True(t, Service.RegisterEmailConfirm) + assert.True(t, Service.EnableNotifyMail) +} diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 2dfe77dda9..83891387a7 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, @@ -139,10 +140,7 @@ func loadOAuth2From(rootCfg ConfigProvider) { if InstallLock { jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64) if err != nil { - jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64() - if err != nil { - log.Fatal("error generating JWT secret: %v", err) - } + jwtSecretBytes, jwtSecretBase64 = generate.NewJwtSecretWithBase64() saveCfg, err := rootCfg.PrepareSaving() if err != nil { log.Fatal("save oauth2.JWT_SECRET failed: %v", err) @@ -162,10 +160,7 @@ var generalSigningSecret atomic.Pointer[[]byte] func GetGeneralTokenSigningSecret() []byte { old := generalSigningSecret.Load() if old == nil || len(*old) == 0 { - jwtSecret, _, err := generate.NewJwtSecretWithBase64() - if err != nil { - log.Fatal("Unable to generate general JWT secret: %v", err) - } + jwtSecret, _ := generate.NewJwtSecretWithBase64() if generalSigningSecret.CompareAndSwap(old, &jwtSecret) { return jwtSecret } diff --git a/modules/setting/packages.go b/modules/setting/packages.go index b598424064..38ee2ad55e 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -16,30 +16,31 @@ var ( Storage *Storage Enabled bool - LimitTotalOwnerCount int64 - LimitTotalOwnerSize int64 - LimitSizeAlpine int64 - LimitSizeArch int64 - LimitSizeCargo int64 - LimitSizeChef int64 - LimitSizeComposer int64 - LimitSizeConan int64 - LimitSizeConda int64 - LimitSizeContainer int64 - LimitSizeCran int64 - LimitSizeDebian int64 - LimitSizeGeneric int64 - LimitSizeGo int64 - LimitSizeHelm int64 - LimitSizeMaven int64 - LimitSizeNpm int64 - LimitSizeNuGet int64 - LimitSizePub int64 - LimitSizePyPI int64 - LimitSizeRpm int64 - LimitSizeRubyGems int64 - LimitSizeSwift int64 - LimitSizeVagrant int64 + LimitTotalOwnerCount int64 + LimitTotalOwnerSize int64 + LimitSizeAlpine int64 + LimitSizeArch int64 + LimitSizeCargo int64 + LimitSizeChef int64 + LimitSizeComposer int64 + LimitSizeConan int64 + LimitSizeConda int64 + LimitSizeContainer int64 + LimitSizeCran int64 + LimitSizeDebian int64 + LimitSizeGeneric int64 + LimitSizeGo int64 + LimitSizeHelm int64 + LimitSizeMaven int64 + LimitSizeNpm int64 + LimitSizeNuGet int64 + LimitSizePub int64 + LimitSizePyPI int64 + LimitSizeRpm int64 + LimitSizeRubyGems int64 + LimitSizeSwift int64 + LimitSizeTerraformState int64 + LimitSizeVagrant int64 DefaultRPMSignEnabled bool }{ @@ -86,6 +87,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) { Packages.LimitSizeRpm = mustBytes(sec, "LIMIT_SIZE_RPM") Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS") Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT") + Packages.LimitSizeTerraformState = mustBytes(sec, "LIMIT_SIZE_TERRAFORM_STATE") Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT") Packages.DefaultRPMSignEnabled = sec.Key("DEFAULT_RPM_SIGN_ENABLED").MustBool(false) return nil diff --git a/modules/setting/path.go b/modules/setting/path.go index f51457a620..45c6759a73 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/repository.go b/modules/setting/repository.go index 9195b7ee50..a8bc91c089 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -18,6 +18,12 @@ const ( RepoCreatingPublic = "public" ) +// enumerates the values for [repository.pull-request] DEFAULT_TITLE_SOURCE +const ( + RepoPRTitleSourceFirstCommit = "first-commit" + RepoPRTitleSourceAuto = "auto" +) + // ItemsPerPage maximum items per page in forks, watchers and stars of a repo const ItemsPerPage = 40 @@ -89,6 +95,7 @@ var ( RetargetChildrenOnMerge bool DelayCheckForInactiveDays int DefaultDeleteBranchAfterMerge bool + DefaultTitleSource string } `ini:"repository.pull-request"` // Issue Setting @@ -213,6 +220,7 @@ var ( RetargetChildrenOnMerge bool DelayCheckForInactiveDays int DefaultDeleteBranchAfterMerge bool + DefaultTitleSource string }{ WorkInProgressPrefixes: []string{"WIP:", "[WIP]"}, // Same as GitHub. See @@ -229,6 +237,7 @@ var ( AddCoCommitterTrailers: true, RetargetChildrenOnMerge: true, DelayCheckForInactiveDays: 7, + DefaultTitleSource: RepoPRTitleSourceFirstCommit, }, // Issue settings diff --git a/modules/setting/security.go b/modules/setting/security.go index a1fd0bce2e..8b7664baba 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 ( @@ -31,6 +33,7 @@ var ( ReverseProxyAuthEmail string ReverseProxyAuthFullName string ReverseProxyLimit int + ReverseProxyLogoutRedirect string ReverseProxyTrustedProxies []string MinPasswordLength int ImportLocalPaths bool @@ -124,6 +127,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) { ReverseProxyAuthFullName = sec.Key("REVERSE_PROXY_AUTHENTICATION_FULL_NAME").MustString("X-WEBAUTH-FULLNAME") ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1) + ReverseProxyLogoutRedirect = sec.Key("REVERSE_PROXY_LOGOUT_REDIRECT").String() ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",") if len(ReverseProxyTrustedProxies) == 0 { ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"} @@ -152,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 f0fbbce970..dc58e43c43 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" ) @@ -72,9 +70,6 @@ var ( // It maps to ini:"LOCAL_ROOT_URL" in [server] LocalURL string - // AssetVersion holds an opaque value that is used for cache-busting assets - AssetVersion string - // appTempPathInternal is the temporary path for the app, it is only an internal variable // DO NOT use it directly, always use AppDataTempDir appTempPathInternal string @@ -115,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") @@ -316,12 +248,6 @@ func loadServerFrom(rootCfg ConfigProvider) { Domain = urlHostname } - AbsoluteAssetURL = MakeAbsoluteAssetURL(appURL, StaticURLPrefix) - AssetVersion = strings.ReplaceAll(AppVer, "+", "~") // make sure the version string is clear (no real escaping is needed) - - 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.go b/modules/setting/setting.go index f2b6274edc..1dcea92602 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/user" + "code.gitea.io/gitea/modules/util" ) // settings @@ -28,8 +29,10 @@ var ( CfgProvider ConfigProvider IsWindows bool - // IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing - // TODO: this is only a temporary solution, we should make the test code more reliable + // IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for: + // * Skip nonsense error logs during testing caused by unreliable code (TODO: this is only a temporary solution, we should make the test code more reliable) + // * Panic in dev or testing mode to make the problem more obvious and easier to debug + // * Mock some functions or options to make testing easier (eg: session store, time, URL detection, etc.) IsInTesting = false ) @@ -39,9 +42,9 @@ func init() { AppVer = "dev" } - // We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically + // FIXME: the logger shouldn't be initialized here, the app entry should initialize the logger // By default set this logger at Info - we'll change it later, but we need to start with something. - log.SetConsoleLogger(log.DEFAULT, "console", log.INFO) + log.SetupStderrLogger(log.DEFAULT, "console-stderr", log.INFO) } // IsRunUserMatchCurrentUser returns false if configured run user does not match @@ -57,6 +60,10 @@ func IsRunUserMatchCurrentUser(runUser string) (string, bool) { return currentUser, runUser == currentUser } +func IsInE2eTesting() bool { + return os.Getenv("GITEA_TEST_E2E") == "true" +} + // PrepareAppDataPath creates app data directory if necessary func PrepareAppDataPath() error { // FIXME: There are too many calls to MkdirAll in old code. It is incorrect. @@ -157,32 +164,38 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error { func loadRunModeFrom(rootCfg ConfigProvider) { rootSec := rootCfg.Section("") + mustNotRunAsRoot(rootSec) + + runModeValue := os.Getenv("GITEA_RUN_MODE") + runModeValue = util.IfZero(runModeValue, rootSec.Key("RUN_MODE").String()) + // non-dev mode is treated as prod mode, to protect users from accidentally running in dev mode if there is a typo in this value. + IsProd = !strings.EqualFold(runModeValue, "dev") // TODO: can use case-sensitive comparing in the future + RunMode = util.Iif(IsProd, "prod", "dev") + + // there is a separate check: mustCurrentRunUserMatch (IsRunUserMatchCurrentUser) RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername()) +} + +func mustNotRunAsRoot(rootSec ConfigSection) { + if os.Getuid() != 0 { + return + } + + mustRunAsRoot := os.Getenv("SNAP") != "" && os.Getenv("SNAP_NAME") != "" // snap container runs the app as uid=0 + if mustRunAsRoot { + return + } // The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches. // Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly. - unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT") - unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value() - RunMode = os.Getenv("GITEA_RUN_MODE") - if RunMode == "" { - RunMode = rootSec.Key("RUN_MODE").MustString("prod") - } + allowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT") || // check gitea config + optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value() // check gitea env var - // non-dev mode is treated as prod mode, to protect users from accidentally running in dev mode if there is a typo in this value. - RunMode = strings.ToLower(RunMode) - if RunMode != "dev" { - RunMode = "prod" - } - IsProd = RunMode != "dev" - - // check if we run as root - if os.Getuid() == 0 { - if !unsafeAllowRunAsRoot { - // Special thanks to VLC which inspired the wording of this messaging. - log.Fatal("Gitea is not supposed to be run as root. Sorry. If you need to use privileged TCP ports please instead use setcap and the `cap_net_bind_service` permission") - } - log.Critical("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.") + if !allowRunAsRoot { + // Special thanks to VLC which inspired the wording of this messaging. + log.Fatal("Gitea is not supposed to be run as root. If you need to use privileged TCP ports please instead use `setcap` and the `cap_net_bind_service` permission.") } + log.Warn("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.") } // HasInstallLock checks the install-lock in ConfigProvider directly, because sometimes the config file is not loaded into setting variables yet. @@ -195,7 +208,7 @@ func mustCurrentRunUserMatch(rootCfg ConfigProvider) { if HasInstallLock(rootCfg) { currentUser, match := IsRunUserMatchCurrentUser(RunUser) if !match { - log.Fatal("Expect user '%s' but current user is: %s", RunUser, currentUser) + log.Fatal("Expect user '%s' (RUN_USER in app.ini) but current user is: %s", RunUser, currentUser) } } } @@ -226,7 +239,7 @@ func LoadSettings() { func LoadSettingsForInstall() { loadDBSetting(CfgProvider) loadServiceFrom(CfgProvider) - loadMailerFrom(CfgProvider) + loadMailsFrom(CfgProvider) } var configuredPaths = make(map[string]string) diff --git a/modules/setting/setting_test.go b/modules/setting/setting_test.go deleted file mode 100644 index 13575f52a6..0000000000 --- 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 853521c328..24d489d77c 100644 --- a/modules/setting/testenv.go +++ b/modules/setting/testenv.go @@ -4,69 +4,172 @@ package setting import ( + "errors" "fmt" "os" "path/filepath" "runtime" "strings" + "code.gitea.io/gitea/modules/auth/password/hash" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" + + "github.com/kballard/go-shellquote" ) -var giteaTestSourceRoot *string +var giteaTestSourceRoot *string // intentionally use a pointer to make sure the uninitialized access panics func GetGiteaTestSourceRoot() string { return *giteaTestSourceRoot } +func detectGiteaTestRoot() string { + _, 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) + } + return giteaRoot +} + func SetupGiteaTestEnv() { if giteaTestSourceRoot != nil { return // already initialized } 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 == "" { + giteaRoot = detectGiteaTestRoot() + } + 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 +} + +func PrepareIntegrationTestConfig() error { + giteaTestRoot := detectGiteaTestRoot() + isInCI := os.Getenv("CI") != "" + testDatabase := os.Getenv("GITEA_TEST_DATABASE") + if testDatabase == "" { + if isInCI { + return errors.New("GITEA_TEST_DATABASE environment variable not set") + } + // for local development, default to sqlite. CI needs to explicitly set a database to avoid unexpected results + testDatabase = "sqlite" + _, _ = fmt.Fprintf(os.Stderr, "Environment variable GITEA_TEST_DATABASE not set - defaulting to %s\n", testDatabase) + } + + _ = os.Setenv("GITEA_TEST_ROOT", giteaTestRoot) + _ = os.Setenv("GITEA_TEST_CONF", filepath.Join("tests", testDatabase+".ini")) + + workPath := filepath.Join(giteaTestRoot, "tests/integration/gitea-integration-"+testDatabase) + if err := os.MkdirAll(workPath, 0o755); err != nil { + return err + } + + confFile := filepath.Join(giteaTestRoot, "tests", testDatabase+".ini") + tmplBuf, err := os.ReadFile(confFile + ".tmpl") + if err != nil { + return err + } + tmpl := string(tmplBuf) + envVars, err := shellquote.Split(os.Getenv("MAKEFILE_VARS")) + if err != nil { + return err + } + envVarMap := map[string]string{ + "TEST_WORK_PATH": workPath, + "TEST_LOGGER": "test,file", + } + for _, env := range append(os.Environ(), envVars...) { + k, v, _ := strings.Cut(env, "=") + k = strings.TrimSpace(k) + v = strings.TrimSpace(v) + envVarMap[k] = v + } + for k, v := range envVarMap { + tmpl = strings.ReplaceAll(tmpl, fmt.Sprintf("{{%s}}", k), v) + } + err = os.WriteFile(confFile, []byte(tmpl), 0o644) + return err } diff --git a/modules/ssh/ssh_graceful.go b/modules/ssh/ssh_graceful.go index cad2c685bd..c714f79ffc 100644 --- a/modules/ssh/ssh_graceful.go +++ b/modules/ssh/ssh_graceful.go @@ -20,7 +20,7 @@ func listen(server *ssh.Server) { if err != nil { select { case <-graceful.GetManager().IsShutdown(): - log.Critical("Failed to start SSH server: %v", err) + log.Error("Failed to start SSH server: %v", err) default: log.Fatal("Failed to start SSH server: %v", err) } diff --git a/modules/storage/azureblob_test.go b/modules/storage/azureblob_test.go index b5d5d0fecc..b3544dd7bb 100644 --- a/modules/storage/azureblob_test.go +++ b/modules/storage/azureblob_test.go @@ -5,25 +5,22 @@ package storage import ( "io" - "os" "strings" "testing" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" ) func TestAzureBlobStorage(t *testing.T) { - if os.Getenv("CI") == "" { - t.Skip("azureBlobStorage not present outside of CI") - return - } + endpoint := test.ExternalServiceHTTP(t, "TEST_AZURESTORAGE_ENDPOINT", "http://devstoreaccount1.azurite.local:10000") storageType := setting.AzureBlobStorageType config := &setting.Storage{ AzureBlobConfig: setting.AzureBlobStorageConfig{ // https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#ip-style-url - Endpoint: "http://devstoreaccount1.azurite.local:10000", + Endpoint: endpoint, // https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#well-known-storage-account-and-key AccountName: "devstoreaccount1", AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==", @@ -77,15 +74,11 @@ func TestAzureBlobStoragePath(t *testing.T) { } func Test_azureBlobObject(t *testing.T) { - if os.Getenv("CI") == "" { - t.Skip("azureBlobStorage not present outside of CI") - return - } - + endpoint := test.ExternalServiceHTTP(t, "TEST_AZURESTORAGE_ENDPOINT", "http://devstoreaccount1.azurite.local:10000") s, err := NewStorage(setting.AzureBlobStorageType, &setting.Storage{ AzureBlobConfig: setting.AzureBlobStorageConfig{ // https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#ip-style-url - Endpoint: "http://devstoreaccount1.azurite.local:10000", + Endpoint: endpoint, // https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#well-known-storage-account-and-key AccountName: "devstoreaccount1", AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==", diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 1355280f36..ace78bb610 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -23,11 +23,7 @@ import ( "github.com/minio/minio-go/v7/pkg/credentials" ) -var ( - _ ObjectStorage = &MinioStorage{} - - quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") -) +var _ ObjectStorage = &MinioStorage{} type minioObject struct { *minio.Object diff --git a/modules/storage/minio_test.go b/modules/storage/minio_test.go index 5c15ee1ed6..c7825258a7 100644 --- a/modules/storage/minio_test.go +++ b/modules/storage/minio_test.go @@ -11,20 +11,18 @@ import ( "testing" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/minio/minio-go/v7" "github.com/stretchr/testify/assert" ) func TestMinioStorage(t *testing.T) { - if os.Getenv("CI") == "" { - t.Skip("minioStorage not present outside of CI") - return - } + endpoint := test.ExternalServiceHTTP(t, "TEST_MINIO_ENDPOINT", "minio:9000") storageType := setting.MinioStorageType config := &setting.Storage{ MinioConfig: setting.MinioStorageConfig{ - Endpoint: "minio:9000", + Endpoint: endpoint, AccessKeyID: "123456", SecretAccessKey: "12345678", Bucket: "gitea", diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 2491c77a3e..1271440e5a 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -12,6 +12,7 @@ import ( "os" "path" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" @@ -20,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 @@ -62,31 +44,30 @@ type Object interface { type ServeDirectOptions struct { // Overrides the automatically detected MIME type. ContentType string - // Overrides the default Content-Disposition header, which is `inline; filename="name"`. - ContentDisposition string } // Safe defaults are applied only when not explicitly overridden by the caller. -func prepareServeDirectOptions(optsOptional *ServeDirectOptions, name string) (ret ServeDirectOptions) { +func prepareServeDirectOptions(optsOptional *ServeDirectOptions, name string) (ret struct { + ContentType string + ContentDisposition string +}, +) { // Here we might not know the real filename, and it's quite inefficient to detect the MIME type by pre-fetching the object head. // So we just do a quick detection by extension name, at least it works for the "View Raw File" for an LFS file on the Web UI. // TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future if optsOptional != nil { - ret = *optsOptional + ret.ContentType = optsOptional.ContentType } - - // TODO: UNIFY-CONTENT-DISPOSITION-FROM-STORAGE + name = path.Base(name) if ret.ContentType == "" { ext := path.Ext(name) ret.ContentType = public.DetectWellKnownMimeType(ext) } - if ret.ContentDisposition == "" { - // When using ServeDirect, the URL is from the object storage's web server, - // it is not the same origin as Gitea server, so it should be safe enough to use "inline" to render the content directly. - // If a browser doesn't support the content type to be displayed inline, browser will download with the filename. - ret.ContentDisposition = fmt.Sprintf(`inline; filename="%s"`, quoteEscaper.Replace(name)) - } + // When using ServeDirect, the URL is from the object storage's web server, + // it is not the same origin as Gitea server, so it should be safe enough to use "inline" to render the content directly. + // If a browser doesn't support the content type to be displayed inline, browser will download with the filename. + ret.ContentDisposition = httplib.EncodeContentDispositionInline(name) return ret } diff --git a/modules/storage/storage_test.go b/modules/storage/storage_test.go index 4156723c36..83ee2ef793 100644 --- a/modules/storage/storage_test.go +++ b/modules/storage/storage_test.go @@ -53,7 +53,12 @@ func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) { } } -func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path, name string, expected ServeDirectOptions, reqParams *ServeDirectOptions) { +type expectedServeDirectHeaders struct { + ContentType string + ContentDisposition string +} + +func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path, name string, expected expectedServeDirectHeaders, reqParams *ServeDirectOptions) { u, err := s.ServeDirectURL(path, name, http.MethodGet, reqParams) require.NoError(t, err) resp, err := http.Get(u.String()) @@ -71,36 +76,29 @@ func testBlobStorageURLContentTypeAndDisposition(t *testing.T, typStr Type, cfg s, err := NewStorage(typStr, cfg) assert.NoError(t, err) - data := "Q2xTckt6Y1hDOWh0" // arbitrary test content; specific value is irrelevant to this test - testfilename := "test.txt" // arbitrary file name; specific value is irrelevant to this test - _, err = s.Save(testfilename, strings.NewReader(data), int64(len(data))) + testFilename := "test.txt" + _, err = s.Save(testFilename, strings.NewReader("dummy-content"), -1) assert.NoError(t, err) - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.txt", ServeDirectOptions{ + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.txt", expectedServeDirectHeaders{ ContentType: "text/plain; charset=utf-8", - ContentDisposition: `inline; filename="test.txt"`, + ContentDisposition: `inline; filename=test.txt`, }, nil) - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.pdf", ServeDirectOptions{ + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.pdf", expectedServeDirectHeaders{ ContentType: "application/pdf", - ContentDisposition: `inline; filename="test.pdf"`, + ContentDisposition: `inline; filename=test.pdf`, }, nil) - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.wasm", ServeDirectOptions{ - ContentDisposition: `inline; filename="test.wasm"`, + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{ + ContentDisposition: `inline; filename=test.wasm`, }, nil) - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.wasm", ServeDirectOptions{ - ContentDisposition: `inline; filename="test.wasm"`, - }, &ServeDirectOptions{}) - - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.txt", ServeDirectOptions{ - ContentType: "application/octet-stream", - ContentDisposition: `inline; filename="test.xml"`, + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{ + ContentType: "application/wasm", + ContentDisposition: `inline; filename=test.wasm`, }, &ServeDirectOptions{ - ContentType: "application/octet-stream", - ContentDisposition: `inline; filename="test.xml"`, + ContentType: "application/wasm", }) - - assert.NoError(t, s.Delete(testfilename)) + assert.NoError(t, s.Delete(testFilename)) } diff --git a/modules/structs/activity.go b/modules/structs/activity.go index 9085495593..b896adfed5 100644 --- a/modules/structs/activity.go +++ b/modules/structs/activity.go @@ -12,7 +12,7 @@ type Activity struct { UserID int64 `json:"user_id"` // Receiver user // the type of action // - // enum: create_repo,rename_repo,star_repo,watch_repo,commit_repo,create_issue,create_pull_request,transfer_repo,push_tag,comment_issue,merge_pull_request,close_issue,reopen_issue,close_pull_request,reopen_pull_request,delete_tag,delete_branch,mirror_sync_push,mirror_sync_create,mirror_sync_delete,approve_pull_request,reject_pull_request,comment_pull,publish_release,pull_review_dismissed,pull_request_ready_for_review,auto_merge_pull_request + // enum: ["create_repo","rename_repo","star_repo","watch_repo","commit_repo","create_issue","create_pull_request","transfer_repo","push_tag","comment_issue","merge_pull_request","close_issue","reopen_issue","close_pull_request","reopen_pull_request","delete_tag","delete_branch","mirror_sync_push","mirror_sync_create","mirror_sync_delete","approve_pull_request","reject_pull_request","comment_pull","publish_release","pull_review_dismissed","pull_request_ready_for_review","auto_merge_pull_request"] OpType string `json:"op_type"` // The ID of the user who performed the action ActUserID int64 `json:"act_user_id"` diff --git a/modules/structs/admin_user.go b/modules/structs/admin_user.go index d158a5fd31..ee65e016bc 100644 --- a/modules/structs/admin_user.go +++ b/modules/structs/admin_user.go @@ -30,7 +30,7 @@ type CreateUserOption struct { // Whether the user has restricted access privileges Restricted *bool `json:"restricted"` // User visibility level: public, limited, or private - Visibility string `json:"visibility" binding:"In(,public,limited,private)"` + Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"` // For explicitly setting the user creation timestamp. Useful when users are // migrated from other systems. When omitted, the user's creation timestamp @@ -79,5 +79,5 @@ type EditUserOption struct { // Whether the user has restricted access privileges Restricted *bool `json:"restricted"` // User visibility level: public, limited, or private - Visibility string `json:"visibility" binding:"In(,public,limited,private)"` + Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"` } diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 57af38464a..99c1535155 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -19,6 +19,8 @@ var ErrInvalidReceiveHook = errors.New("Invalid JSON payload received over webho type Hook struct { // The unique identifier of the webhook ID int64 `json:"id"` + // Optional human-readable name for the webhook + Name string `json:"name"` // The type of the webhook (e.g., gitea, slack, discord) Type string `json:"type"` // Branch filter pattern to determine which branches trigger the webhook @@ -51,7 +53,7 @@ type CreateHookOptionConfig map[string]string // CreateHookOption options when create a hook type CreateHookOption struct { // required: true - // enum: dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist + // enum: ["dingtalk","discord","gitea","gogs","msteams","slack","telegram","feishu","wechatwork","packagist"] // The type of the webhook to create Type string `json:"type" binding:"Required"` // required: true @@ -66,6 +68,8 @@ type CreateHookOption struct { // default: false // Whether the webhook should be active upon creation Active bool `json:"active"` + // Optional human-readable name for the webhook + Name string `json:"name" binding:"MaxSize(255)"` } // EditHookOption options when modify one hook @@ -80,6 +84,8 @@ type EditHookOption struct { AuthorizationHeader string `json:"authorization_header"` // Whether the webhook is active and will be triggered Active *bool `json:"active"` + // Optional human-readable name + Name *string `json:"name,omitzero" binding:"MaxSize(255)"` } // Payloader payload is some part of one hook diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 2540481d0f..f108cf3d0a 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -14,6 +14,8 @@ import ( ) // StateType issue state type +// +// swagger:enum StateType type StateType string const ( @@ -21,10 +23,11 @@ const ( StateOpen StateType = "open" // StateClosed pr is closed StateClosed StateType = "closed" - // StateAll is all - StateAll StateType = "all" ) +// StateAll is a query parameter filter value, not a valid object state. +const StateAll = "all" + // PullRequestMeta PR info if an issue is a PR type PullRequestMeta struct { HasMerged bool `json:"merged"` @@ -57,16 +60,13 @@ type Issue struct { Attachments []*Attachment `json:"assets"` Labels []*Label `json:"labels"` Milestone *Milestone `json:"milestone"` + Projects []*Project `json:"projects"` // deprecated - Assignee *User `json:"assignee"` - Assignees []*User `json:"assignees"` - // Whether the issue is open or closed - // - // type: string - // enum: open,closed - State StateType `json:"state"` - IsLocked bool `json:"is_locked"` - Comments int `json:"comments"` + Assignee *User `json:"assignee"` + Assignees []*User `json:"assignees"` + State StateType `json:"state"` + IsLocked bool `json:"is_locked"` + Comments int `json:"comments"` // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time @@ -82,6 +82,8 @@ type Issue struct { Repo *RepositoryMeta `json:"repository"` PinOrder int `json:"pin_order"` + // The version of the issue content for optimistic locking + ContentVersion int `json:"content_version"` } // CreateIssueOption options to create one issue @@ -99,7 +101,9 @@ type CreateIssueOption struct { Milestone int64 `json:"milestone"` // list of label ids Labels []int64 `json:"labels"` - Closed bool `json:"closed"` + // list of project ids + Projects []int64 `json:"projects"` + Closed bool `json:"closed"` } // EditIssueOption options for editing an issue @@ -111,10 +115,14 @@ type EditIssueOption struct { Assignee *string `json:"assignee"` Assignees []string `json:"assignees"` Milestone *int64 `json:"milestone"` - State *string `json:"state"` + // list of project ids to set (replaces existing projects) + Projects *[]int64 `json:"projects"` + State *string `json:"state"` // swagger:strfmt date-time Deadline *time.Time `json:"due_date"` RemoveDeadline *bool `json:"unset_due_date"` + // The current version of the issue content to detect conflicts during editing + ContentVersion *int `json:"content_version"` } // EditDeadlineOption options for creating a deadline @@ -132,6 +140,8 @@ type IssueDeadline struct { } // IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes" +// +// swagger:enum IssueFormFieldType type IssueFormFieldType string const ( @@ -168,7 +178,8 @@ func (iff IssueFormField) VisibleInContent() bool { } // IssueFormFieldVisible defines issue form field visible -// swagger:model +// +// swagger:enum IssueFormFieldVisible type IssueFormFieldVisible string const ( diff --git a/modules/structs/issue_milestone.go b/modules/structs/issue_milestone.go index 226c613d47..dd8bdc6cda 100644 --- a/modules/structs/issue_milestone.go +++ b/modules/structs/issue_milestone.go @@ -40,7 +40,7 @@ type CreateMilestoneOption struct { // swagger:strfmt date-time // Deadline is the due date for the milestone Deadline *time.Time `json:"due_on"` - // enum: open,closed + // enum: ["open","closed"] // State indicates the initial state of the milestone State string `json:"state"` } @@ -52,6 +52,7 @@ type EditMilestoneOption struct { // Description provides updated details about the milestone Description *string `json:"description"` // State indicates the updated state of the milestone + // enum: ["open","closed"] State *string `json:"state"` // Deadline is the updated due date for the milestone Deadline *time.Time `json:"due_on"` diff --git a/modules/structs/notifications.go b/modules/structs/notifications.go index cee5da6624..b94e02aee3 100644 --- a/modules/structs/notifications.go +++ b/modules/structs/notifications.go @@ -40,7 +40,7 @@ type NotificationSubject struct { // Type indicates the type of the notification subject Type NotifySubjectType `json:"type" binding:"In(Issue,Pull,Commit,Repository)"` // State indicates the current state of the notification subject - State StateType `json:"state"` + State NotifySubjectStateType `json:"state"` } // NotificationCount number of unread notifications @@ -49,16 +49,31 @@ type NotificationCount struct { New int64 `json:"new"` } +// NotifySubjectStateType represents the state of a notification subject +// swagger:enum NotifySubjectStateType +type NotifySubjectStateType string + +const ( + // NotifySubjectStateOpen is an open subject + NotifySubjectStateOpen NotifySubjectStateType = "open" + // NotifySubjectStateClosed is a closed subject + NotifySubjectStateClosed NotifySubjectStateType = "closed" + // NotifySubjectStateMerged is a merged pull request + NotifySubjectStateMerged NotifySubjectStateType = "merged" +) + // NotifySubjectType represent type of notification subject +// +// swagger:enum NotifySubjectType 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/org.go b/modules/structs/org.go index d79b1d1d1c..b93f68b600 100644 --- a/modules/structs/org.go +++ b/modules/structs/org.go @@ -22,7 +22,7 @@ type Organization struct { // The location of the organization Location string `json:"location"` // The visibility level of the organization (public, limited, private) - Visibility string `json:"visibility"` + Visibility UserVisibility `json:"visibility"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` // username of the organization @@ -60,8 +60,7 @@ type CreateOrgOption struct { // The location of the organization Location string `json:"location" binding:"MaxSize(50)"` // possible values are `public` (default), `limited` or `private` - // enum: public,limited,private - Visibility string `json:"visibility" binding:"In(,public,limited,private)"` + Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` } @@ -79,8 +78,7 @@ type EditOrgOption struct { // The location of the organization Location *string `json:"location" binding:"MaxSize(50)"` // possible values are `public`, `limited` or `private` - // enum: public,limited,private - Visibility *string `json:"visibility" binding:"In(,public,limited,private)"` + Visibility *UserVisibility `json:"visibility" binding:"In(,public,limited,private)"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"` } diff --git a/modules/structs/org_team.go b/modules/structs/org_team.go index d34de5b6d2..20959931d3 100644 --- a/modules/structs/org_team.go +++ b/modules/structs/org_team.go @@ -15,9 +15,8 @@ type Team struct { // The organization that the team belongs to Organization *Organization `json:"organization"` // Whether the team has access to all repositories in the organization - IncludesAllRepositories bool `json:"includes_all_repositories"` - // enum: none,read,write,admin,owner - Permission string `json:"permission"` + IncludesAllRepositories bool `json:"includes_all_repositories"` + Permission AccessLevelName `json:"permission"` // example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"] // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. Units []string `json:"units"` @@ -34,9 +33,8 @@ type CreateTeamOption struct { // The description of the team Description string `json:"description" binding:"MaxSize(255)"` // Whether the team has access to all repositories in the organization - IncludesAllRepositories bool `json:"includes_all_repositories"` - // enum: read,write,admin - Permission string `json:"permission"` + IncludesAllRepositories bool `json:"includes_all_repositories"` + Permission RepoWritePermission `json:"permission"` // example: ["repo.actions","repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.ext_wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"] // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. Units []string `json:"units"` @@ -53,9 +51,8 @@ type EditTeamOption struct { // The description of the team Description *string `json:"description" binding:"MaxSize(255)"` // Whether the team has access to all repositories in the organization - IncludesAllRepositories *bool `json:"includes_all_repositories"` - // enum: read,write,admin - Permission string `json:"permission"` + IncludesAllRepositories *bool `json:"includes_all_repositories"` + Permission RepoWritePermission `json:"permission"` // example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"] // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. Units []string `json:"units"` diff --git a/modules/structs/project.go b/modules/structs/project.go new file mode 100644 index 0000000000..5feb122767 --- /dev/null +++ b/modules/structs/project.go @@ -0,0 +1,33 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +import ( + "time" +) + +// Project represents a project +// swagger:model +type Project struct { + // ID is the unique identifier for the project + ID int64 `json:"id"` + // Title is the title of the project + Title string `json:"title"` + // Description provides details about the project + Description string `json:"description"` + // OwnerID is the owner of the project (for org-level projects) + OwnerID int64 `json:"owner_id,omitempty"` + // RepoID is the repository this project belongs to (for repo-level projects) + RepoID int64 `json:"repo_id,omitempty"` + // CreatorID is the user who created the project + CreatorID int64 `json:"creator_id"` + // IsClosed indicates if the project is closed + IsClosed bool `json:"is_closed"` + // swagger:strfmt date-time + Created time.Time `json:"created_at"` + // swagger:strfmt date-time + Updated time.Time `json:"updated_at"` + // swagger:strfmt date-time + Closed *time.Time `json:"closed_at,omitempty"` +} diff --git a/modules/structs/pull.go b/modules/structs/pull.go index 3ad2f78bd3..cd2ffbe719 100644 --- a/modules/structs/pull.go +++ b/modules/structs/pull.go @@ -91,6 +91,8 @@ type PullRequest struct { // The pin order for the pull request PinOrder int `json:"pin_order"` + // The version of the pull request content for optimistic locking + ContentVersion int `json:"content_version"` } // PRBranchInfo information about a branch @@ -168,6 +170,8 @@ type EditPullRequestOption struct { RemoveDeadline *bool `json:"unset_due_date"` // Whether to allow maintainer edits AllowMaintainerEdit *bool `json:"allow_maintainer_edit"` + // The current version of the pull request content to detect conflicts during editing + ContentVersion *int `json:"content_version"` } // ChangedFile store information about files affected by the pull request diff --git a/modules/structs/pull_review.go b/modules/structs/pull_review.go index f44d2f84f5..82c86b5b4a 100644 --- a/modules/structs/pull_review.go +++ b/modules/structs/pull_review.go @@ -8,6 +8,8 @@ import ( ) // ReviewStateType review state type +// +// swagger:enum ReviewStateType type ReviewStateType string const ( @@ -21,10 +23,11 @@ const ( ReviewStateRequestChanges ReviewStateType = "REQUEST_CHANGES" // ReviewStateRequestReview review is requested from user ReviewStateRequestReview ReviewStateType = "REQUEST_REVIEW" - // ReviewStateUnknown state of pr is unknown - ReviewStateUnknown ReviewStateType = "" ) +// ReviewStateUnknown is an internal sentinel for unknown review state, not a valid API value. +const ReviewStateUnknown = "" + // PullReview represents a pull request review type PullReview struct { ID int64 `json:"id"` @@ -91,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.go b/modules/structs/repo.go index 3507cc410a..eca1b15b02 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -8,6 +8,15 @@ import ( "time" ) +// ObjectFormatName is the git hash algorithm used by a repository. +// swagger:enum ObjectFormatName +type ObjectFormatName string + +const ( + ObjectFormatSHA1 ObjectFormatName = "sha1" + ObjectFormatSHA256 ObjectFormatName = "sha256" +) + // Permission represents a set of permissions type Permission struct { Admin bool `json:"admin"` // Admin indicates if the user is an administrator of the repository. @@ -104,23 +113,26 @@ type Repository struct { AllowRebaseMerge bool `json:"allow_rebase_explicit"` AllowSquash bool `json:"allow_squash_merge"` AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"` + AllowMergeUpdate bool `json:"allow_merge_update"` AllowRebaseUpdate bool `json:"allow_rebase_update"` AllowManualMerge bool `json:"allow_manual_merge"` AutodetectManualMerge bool `json:"autodetect_manual_merge"` DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"` DefaultMergeStyle string `json:"default_merge_style"` + DefaultUpdateStyle string `json:"default_update_style"` DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"` AvatarURL string `json:"avatar_url"` Internal bool `json:"internal"` MirrorInterval string `json:"mirror_interval"` // ObjectFormatName of the underlying git repository - // enum: sha1,sha256 - ObjectFormatName string `json:"object_format_name"` + ObjectFormatName ObjectFormatName `json:"object_format_name"` // swagger:strfmt date-time - MirrorUpdated time.Time `json:"mirror_updated"` - RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"` - Topics []string `json:"topics"` - Licenses []string `json:"licenses"` + MirrorUpdated time.Time `json:"mirror_updated"` + // swagger:strfmt date-time + MirrorLastSyncAt time.Time `json:"mirror_last_sync_at"` + RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"` + Topics []string `json:"topics"` + Licenses []string `json:"licenses"` } // CreateRepoOption options when creating repository @@ -150,11 +162,10 @@ type CreateRepoOption struct { // DefaultBranch of the repository (used when initializes and in template) DefaultBranch string `json:"default_branch" binding:"GitRefName;MaxSize(100)"` // TrustModel of the repository - // enum: default,collaborator,committer,collaboratorcommitter + // enum: ["default","collaborator","committer","collaboratorcommitter"] TrustModel string `json:"trust_model"` // ObjectFormatName of the underlying git repository, empty string for default (sha1) - // enum: sha1,sha256 - ObjectFormatName string `json:"object_format_name" binding:"MaxSize(6)"` + ObjectFormatName ObjectFormatName `json:"object_format_name" binding:"MaxSize(6)"` } // EditRepoOption options when editing a repository's properties @@ -215,12 +226,16 @@ type EditRepoOption struct { AllowManualMerge *bool `json:"allow_manual_merge,omitempty"` // either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur. AutodetectManualMerge *bool `json:"autodetect_manual_merge,omitempty"` + // either `true` to allow updating pull request branch by merge, or `false` to prevent it. + AllowMergeUpdate *bool `json:"allow_merge_update,omitempty"` // either `true` to allow updating pull request branch by rebase, or `false` to prevent it. AllowRebaseUpdate *bool `json:"allow_rebase_update,omitempty"` // set to `true` to delete pr branch after merge by default DefaultDeleteBranchAfterMerge *bool `json:"default_delete_branch_after_merge,omitempty"` // set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", "squash", or "fast-forward-only". DefaultMergeStyle *string `json:"default_merge_style,omitempty"` + // set to an update style to be used by this repository: "merge" or "rebase". + DefaultUpdateStyle *string `json:"default_update_style,omitempty"` // set to `true` to allow edits from maintainers by default DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"` // set to `true` to archive this repository. @@ -229,6 +244,12 @@ type EditRepoOption struct { MirrorInterval *string `json:"mirror_interval,omitempty"` // enable prune - remove obsolete remote-tracking references when mirroring EnablePrune *bool `json:"enable_prune,omitempty"` + // authentication username for the remote repository (mirrors) + MirrorUsername *string `json:"mirror_username,omitempty"` + // authentication password for the remote repository (mirrors) + MirrorPassword *string `json:"mirror_password,omitempty"` + // authentication token for the remote repository (mirrors) + MirrorToken *string `json:"mirror_token,omitempty"` } // GenerateRepoOption options when creating a repository using a template @@ -378,7 +399,7 @@ type MigrateRepoOptions struct { // required: true RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` - // enum: git,github,gitea,gitlab,gogs,onedev,gitbucket,codebase,codecommit + // enum: ["git","github","gitea","gitlab","gogs","onedev","gitbucket","codebase","codecommit"] Service string `json:"service"` AuthUsername string `json:"auth_username"` AuthPassword string `json:"auth_password"` diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 92ca9bccce..4592c18ed6 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/structs/repo_branch.go b/modules/structs/repo_branch.go index 75f7878aa6..fc84ab364e 100644 --- a/modules/structs/repo_branch.go +++ b/modules/structs/repo_branch.go @@ -50,6 +50,9 @@ type BranchProtection struct { EnableMergeWhitelist bool `json:"enable_merge_whitelist"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + EnableBypassAllowlist bool `json:"enable_bypass_allowlist"` + BypassAllowlistUsernames []string `json:"bypass_allowlist_usernames"` + BypassAllowlistTeams []string `json:"bypass_allowlist_teams"` EnableStatusCheck bool `json:"enable_status_check"` StatusCheckContexts []string `json:"status_check_contexts"` RequiredApprovals int64 `json:"required_approvals"` @@ -90,6 +93,9 @@ type CreateBranchProtectionOption struct { EnableMergeWhitelist bool `json:"enable_merge_whitelist"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + EnableBypassAllowlist bool `json:"enable_bypass_allowlist"` + BypassAllowlistUsernames []string `json:"bypass_allowlist_usernames"` + BypassAllowlistTeams []string `json:"bypass_allowlist_teams"` EnableStatusCheck bool `json:"enable_status_check"` StatusCheckContexts []string `json:"status_check_contexts"` RequiredApprovals int64 `json:"required_approvals"` @@ -123,6 +129,9 @@ type EditBranchProtectionOption struct { EnableMergeWhitelist *bool `json:"enable_merge_whitelist"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + EnableBypassAllowlist *bool `json:"enable_bypass_allowlist"` + BypassAllowlistUsernames []string `json:"bypass_allowlist_usernames"` + BypassAllowlistTeams []string `json:"bypass_allowlist_teams"` EnableStatusCheck *bool `json:"enable_status_check"` StatusCheckContexts []string `json:"status_check_contexts"` RequiredApprovals *int64 `json:"required_approvals"` diff --git a/modules/structs/repo_collaborator.go b/modules/structs/repo_collaborator.go index 9ede7f075a..fb36ecf485 100644 --- a/modules/structs/repo_collaborator.go +++ b/modules/structs/repo_collaborator.go @@ -3,17 +3,41 @@ package structs +// RepoWritePermission is a permission level callers may grant to a team or +// collaborator on input. Output fields use AccessLevelName instead. +// swagger:enum RepoWritePermission +type RepoWritePermission string + +const ( + RepoWritePermissionRead RepoWritePermission = "read" + RepoWritePermissionWrite RepoWritePermission = "write" + RepoWritePermissionAdmin RepoWritePermission = "admin" +) + +// AccessLevelName is the string rendering of a perm.AccessMode produced on +// API responses. Callers must not send these values; use RepoWritePermission +// on input. +// swagger:enum AccessLevelName +type AccessLevelName string + +const ( + AccessLevelNameNone AccessLevelName = "none" + AccessLevelNameRead AccessLevelName = "read" + AccessLevelNameWrite AccessLevelName = "write" + AccessLevelNameAdmin AccessLevelName = "admin" + AccessLevelNameOwner AccessLevelName = "owner" +) + // AddCollaboratorOption options when adding a user as a collaborator of a repository type AddCollaboratorOption struct { - // enum: read,write,admin // Permission level to grant the collaborator - Permission *string `json:"permission"` + Permission *RepoWritePermission `json:"permission"` } // RepoCollaboratorPermission to get repository permission for a collaborator type RepoCollaboratorPermission struct { // Permission level of the collaborator - Permission string `json:"permission"` + Permission AccessLevelName `json:"permission"` // RoleName is the name of the permission role RoleName string `json:"role_name"` // User information of the collaborator diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go index 59665062b7..53ce5aeae2 100644 --- a/modules/structs/repo_file.go +++ b/modules/structs/repo_file.go @@ -72,7 +72,7 @@ type ChangeFileOperation struct { // indicates what to do with the file: "create" for creating a new file, "update" for updating an existing file, // "upload" for creating or updating a file, "rename" for renaming a file, and "delete" for deleting an existing file. // required: true - // enum: create,update,upload,rename,delete + // enum: ["create","update","upload","rename","delete"] Operation string `json:"operation" binding:"Required"` // path to the existing or new file // required: true diff --git a/modules/structs/user.go b/modules/structs/user.go index 90dbcff25c..a25c0c5a1e 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -51,7 +51,7 @@ type User struct { // the user's description Description string `json:"description"` // User visibility level option: public, limited, private - Visibility string `json:"visibility"` + Visibility UserVisibility `json:"visibility"` // user counts Followers int `json:"followers_count"` diff --git a/modules/structs/visible_type.go b/modules/structs/visible_type.go index b5ff353b87..f537884963 100644 --- a/modules/structs/visible_type.go +++ b/modules/structs/visible_type.go @@ -56,3 +56,14 @@ func ExtractKeysFromMapString(in map[string]VisibleType) (keys []string) { } return keys } + +// UserVisibility defines the visibility level of a user or organization as +// rendered in API payloads. The DB representation is VisibleType (int). +// swagger:enum UserVisibility +type UserVisibility string + +const ( + UserVisibilityPublic UserVisibility = "public" + UserVisibilityLimited UserVisibility = "limited" + UserVisibilityPrivate UserVisibility = "private" +) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index d2d4d364df..aebbfc7407 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/templates/eval" @@ -22,22 +23,19 @@ import ( "code.gitea.io/gitea/services/gitdiff" ) -// NewFuncMap returns functions for injecting to templates -func NewFuncMap() template.FuncMap { +func newFuncMapWebPage() template.FuncMap { return map[string]any{ "DumpVar": dumpVar, "NIL": func() any { return nil }, // ----------------------------------------------------------------- // html/template related functions - "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. - "Iif": iif, - "Eval": evalTokens, - "HTMLFormat": htmlFormat, - "QueryEscape": queryEscape, - "QueryBuild": QueryBuild, - "SanitizeHTML": SanitizeHTML, - "DotEscape": dotEscape, + "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. + "Iif": iif, + "Eval": evalTokens, + "HTMLFormat": htmlFormat, + "QueryEscape": queryEscape, + "QueryBuild": QueryBuild, "PathEscape": url.PathEscape, "PathEscapeSegments": util.PathEscapeSegments, @@ -58,6 +56,7 @@ func NewFuncMap() template.FuncMap { // ----------------------------------------------------------------- // time / number / format + "ShortSha": base.ShortSha, "FileSize": base.FileSize, "CountFmt": countFmt, "Sec2Hour": util.SecToHours, @@ -68,6 +67,8 @@ func NewFuncMap() template.FuncMap { return strconv.FormatInt(time.Since(startTime).Nanoseconds()/1e6, 10) + "ms" }, + "AssetURI": public.AssetURI, + // ----------------------------------------------------------------- // setting "AppName": func() string { @@ -79,22 +80,12 @@ func NewFuncMap() template.FuncMap { "AssetUrlPrefix": func() string { return setting.StaticURLPrefix + "/assets" }, - "AppUrl": func() string { - // The usage of AppUrl should be avoided as much as possible, - // because the AppURL(ROOT_URL) may not match user's visiting site and the ROOT_URL in app.ini may be incorrect. - // And it's difficult for Gitea to guess absolute URL correctly with zero configuration, - // because Gitea doesn't know whether the scheme is HTTP or HTTPS unless the reverse proxy could tell Gitea. - return setting.AppURL - }, "AppVer": func() string { return setting.AppVer }, - "AppDomain": func() string { // documented in mail-templates.md + "AppDomain": func() string { // TODO: helm registry still uses it, need to use current request host in the future return setting.Domain }, - "AssetVersion": func() string { - return setting.AssetVersion - }, "ShowFooterTemplateLoadTime": func() bool { return setting.Other.ShowFooterTemplateLoadTime }, @@ -141,20 +132,17 @@ func NewFuncMap() template.FuncMap { // ----------------------------------------------------------------- // misc (TODO: move them to MiscUtils to avoid bloating the main func map) - "ShortSha": base.ShortSha, - "ActionContent2Commits": ActionContent2Commits, - "IsMultilineCommitMessage": isMultilineCommitMessage, - "CommentMustAsDiff": gitdiff.CommentMustAsDiff, - "MirrorRemoteAddress": mirrorRemoteAddress, + "ActionContent2Commits": ActionContent2Commits, + "CommentMustAsDiff": gitdiff.CommentMustAsDiff, + "MirrorRemoteAddress": mirrorRemoteAddress, "FilenameIsImage": filenameIsImage, "TabSizeClass": tabSizeClass, } } -// SanitizeHTML sanitizes the input by default sanitization rules. -func SanitizeHTML(s string) template.HTML { - return markup.Sanitize(s) +func sanitizeHTML(msg string) template.HTML { + return markup.Sanitize(msg) } func htmlFormat(s any, args ...any) template.HTML { @@ -175,11 +163,6 @@ func queryEscape(s string) template.URL { return template.URL(url.QueryEscape(s)) } -// dotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent auto-linkers from detecting these as urls -func dotEscape(raw string) string { - return strings.ReplaceAll(raw, ".", "\u200d.\u200d") -} - // iif is an "inline-if", similar util.Iif[T] but templates need the non-generic version, // and it could be simply used as "{{iif expr trueVal}}" (omit the falseVal). func iif(condition any, vals ...any) any { diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index f90818c0ad..c21c20efff 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" @@ -58,7 +59,7 @@ func TestSubjectBodySeparator(t *testing.T) { } func TestSanitizeHTML(t *testing.T) { - assert.Equal(t, template.HTML(`link xss
inline
`), SanitizeHTML(`link xss
inline
`)) + assert.Equal(t, template.HTML(`link xss
inline
`), sanitizeHTML(`link xss
inline
`)) } func TestTemplateIif(t *testing.T) { @@ -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/templates/mail.go b/modules/templates/mail.go index ca13626468..f81073902f 100644 --- a/modules/templates/mail.go +++ b/modules/templates/mail.go @@ -6,12 +6,14 @@ package templates import ( "html/template" "io" + "net/url" "regexp" "slices" "strings" "sync" texttmpl "text/template" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -34,6 +36,11 @@ type MailRender struct { mockedBodyTemplates map[string]*template.Template } +// dotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent auto-linkers from detecting these as urls +func dotEscape(raw string) string { + return strings.ReplaceAll(raw, ".", "\u200d.\u200d") +} + // mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject func mailSubjectTextFuncMap() texttmpl.FuncMap { return texttmpl.FuncMap{ @@ -41,6 +48,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap { "Eval": evalTokens, "EllipsisString": util.EllipsisDisplayString, + "AppName": func() string { return setting.AppName }, @@ -50,6 +58,51 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap { } } +func mailBodyFuncMap() template.FuncMap { + // Some of them are documented in mail-templates.md + return template.FuncMap{ + "DumpVar": dumpVar, + "NIL": func() any { return nil }, + + // html/template related functions + "dict": dict, + "Iif": iif, + "Eval": evalTokens, + "HTMLFormat": htmlFormat, + "QueryEscape": queryEscape, + "QueryBuild": QueryBuild, + + // deprecated, use "HTMLFormat" instead, but some user custom mail templates still use it + // see: https://github.com/go-gitea/gitea/issues/36049 + "SanitizeHTML": sanitizeHTML, + + "PathEscape": url.PathEscape, + "PathEscapeSegments": util.PathEscapeSegments, + + "DotEscape": dotEscape, + + // utils + "StringUtils": NewStringUtils, + "SliceUtils": NewSliceUtils, + "JsonUtils": NewJsonUtils, + + // time / number / format + "ShortSha": base.ShortSha, + "FileSize": base.FileSize, + + // setting + "AppName": func() string { + return setting.AppName + }, + "AppUrl": func() string { + return setting.AppURL + }, + "AppDomain": func() string { + return setting.Domain + }, + } +} + var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`) func newMailRenderer() (*MailRender, error) { @@ -103,7 +156,7 @@ func newMailRenderer() (*MailRender, error) { return renderer.tmplRenderer.Templates().HasTemplate(name) } - staticFuncMap := NewFuncMap() + staticFuncMap := mailBodyFuncMap() renderer.BodyTemplates.ExecuteTemplate = func(w io.Writer, name string, data any) error { if t, ok := renderer.mockedBodyTemplates[name]; ok { return t.Execute(w, data) @@ -131,7 +184,7 @@ func (r *MailRender) MockTemplate(name, subject, body string) func() { texttmpl.Must(r.SubjectTemplates.New(name).Parse(subject)) oldBody, hasOldBody := r.mockedBodyTemplates[name] - mockFuncMap := NewFuncMap() + mockFuncMap := mailBodyFuncMap() r.mockedBodyTemplates[name] = template.Must(template.New(name).Funcs(mockFuncMap).Parse(body)) return func() { r.SubjectTemplates = oldSubject diff --git a/modules/templates/page.go b/modules/templates/page.go index 8f6c82fc4b..475cb0d678 100644 --- a/modules/templates/page.go +++ b/modules/templates/page.go @@ -5,6 +5,7 @@ package templates import ( "context" + "fmt" "html/template" "io" "net/http" @@ -24,19 +25,23 @@ type pageRenderer struct { } func (r *pageRenderer) funcMap(ctx context.Context) template.FuncMap { - pageFuncMap := NewFuncMap() + pageFuncMap := newFuncMapWebPage() pageFuncMap["ctx"] = func() any { return ctx } return pageFuncMap } func (r *pageRenderer) funcMapDummy() template.FuncMap { - dummyFuncMap := NewFuncMap() + dummyFuncMap := newFuncMapWebPage() dummyFuncMap["ctx"] = func() any { return nil } // for template compilation only, no context available return dummyFuncMap } func (r *pageRenderer) TemplateLookup(tmpl string, templateCtx context.Context) (TemplateExecutor, error) { //nolint:revive // we don't use ctx, only pass it to the template executor - return r.tmplRenderer.Templates().Executor(tmpl, r.funcMap(templateCtx)) + tmpls := r.tmplRenderer.Templates() + if tmpls == nil { + return nil, fmt.Errorf("no templates defined for %s", tmpl) + } + return tmpls.Executor(tmpl, r.funcMap(templateCtx)) } func (r *pageRenderer) HTML(w io.Writer, status int, tplName TplName, data any, templateCtx context.Context) error { //nolint:revive // we don't use ctx, only pass it to the template executor diff --git a/modules/templates/templates_bindata.go b/modules/templates/templates_bindata.go index a919591ecf..0399b2c49a 100644 --- a/modules/templates/templates_bindata.go +++ b/modules/templates/templates_bindata.go @@ -10,9 +10,9 @@ package templates import ( "sync" - _ "embed" - "code.gitea.io/gitea/modules/assetfs" + + _ "embed" ) //go:embed bindata.dat diff --git a/modules/templates/util_actions.go b/modules/templates/util_actions.go new file mode 100644 index 0000000000..ad7c695185 --- /dev/null +++ b/modules/templates/util_actions.go @@ -0,0 +1,23 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package templates + +import ( + "context" + + git_model "code.gitea.io/gitea/models/git" + actions_module "code.gitea.io/gitea/modules/actions" +) + +type ActionsUtils struct { + ctx context.Context +} + +func NewActionsUtils(ctx context.Context) *ActionsUtils { + return &ActionsUtils{ctx: ctx} +} + +func (a *ActionsUtils) CommitStatusesToActionsStatuses(statuses []*git_model.CommitStatus) actions_module.CommitActionsStatusMap { + return actions_module.GetCommitActionsStatusMap(a.ctx, statuses) +} diff --git a/modules/templates/util_misc.go b/modules/templates/util_misc.go index fb523fd53a..ac179e7178 100644 --- a/modules/templates/util_misc.go +++ b/modules/templates/util_misc.go @@ -52,11 +52,6 @@ func sortArrow(normSort, revSort, urlSort string, isDefault bool) template.HTML return "" } -// isMultilineCommitMessage checks to see if a commit message contains multiple lines. -func isMultilineCommitMessage(msg string) bool { - return strings.Count(strings.TrimSpace(msg), "\n") >= 1 -} - // Actioner describes an action type Actioner interface { GetOpType() activities_model.ActionType diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 7ff0b204f0..da186f87f2 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -16,6 +16,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/renderhelper" "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" @@ -39,7 +40,7 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { // RenderCommitMessage renders commit message with XSS-safe and special links. func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML { - cleanMsg := template.HTMLEscapeString(msg) + cleanMsg := template.HTML(template.HTMLEscapeString(msg)) // we can safely assume that it will not return any error, since there shouldn't be any special HTML. // "repo" can be nil when rendering commit messages for deleted repositories in a user's dashboard feed. fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg) @@ -47,7 +48,7 @@ func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) te log.Error("PostProcessCommitMessage: %v", err) return "" } - msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") + msgLines := strings.Split(strings.TrimSpace(string(fullMessage)), "\n") if len(msgLines) == 0 { return "" } @@ -78,24 +79,20 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, re // RenderCommitBody extracts the body of a commit message without its title. func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) template.HTML { - msgLine := strings.TrimSpace(msg) - lineEnd := strings.IndexByte(msgLine, '\n') - if lineEnd > 0 { - msgLine = msgLine[lineEnd+1:] - } else { - return "" - } - msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace) - if len(msgLine) == 0 { + _, body, _ := strings.Cut(strings.TrimSpace(msg), "\n") + body = strings.TrimFunc(body, unicode.IsSpace) + if body == "" { return "" } - renderedMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(msgLine)) + rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo) + htmlContent := template.HTML(template.HTMLEscapeString(body)) + renderedMessage, err := markup.PostProcessCommitMessage(rctx, htmlContent) if err != nil { log.Error("PostProcessCommitMessage: %v", err) return "" } - return template.HTML(renderedMessage) + return renderedMessage } // Match text that is between back ticks. @@ -277,3 +274,53 @@ func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize in extraIcon := svg.RenderHTML(info.GetExtraIconName(), iconSize) return htmlutil.HTMLFormat(`
%s %s %s
`, info.GetDescription(), icon, info.DisplayName, extraIcon) } + +func (ut *RenderUtils) RenderFlashMessage(typ, msg string) template.HTML { + msg = strings.TrimSpace(msg) + if msg == "" { + return "" + } + + cls := typ + // legacy logic: "negative" for error, "positive" for success + switch cls { + case "error": + cls = "negative" + case "success": + cls = "positive" + } + + var msgContent template.HTML + if strings.Contains(msg, "") || strings.Contains(msg, "") || strings.Contains(msg, "") || strings.Contains(msg, "") { + // If the message contains some known "block" elements, no need to do more alignment or line-break processing, just sanitize it directly. + msgContent = sanitizeHTML(msg) + } else if !strings.Contains(msg, "\n") { + // If the message is a single line, center-align it by wrapping it + msgContent = htmlutil.HTMLFormat(`
%s
`, sanitizeHTML(msg)) + } else { + // For a multi-line message, preserve line breaks, and left-align it. + msgContent = htmlutil.HTMLFormat(`%s`, sanitizeHTML(strings.ReplaceAll(msg, "\n", "
"))) + } + return htmlutil.HTMLFormat(`
%s
`, cls, typ, msgContent) +} + +func (ut *RenderUtils) RenderUnicodeEscapeToggleButton(escapeStatus *charset.EscapeStatus) template.HTML { + if escapeStatus == nil || !escapeStatus.Escaped { + return "" + } + locale := ut.ctx.Value(translation.ContextKey).(translation.Locale) + var title template.HTML + if escapeStatus.HasAmbiguous { + title += locale.Tr("repo.ambiguous_runes_line") + } else if escapeStatus.HasInvisible { + title += locale.Tr("repo.invisible_runes_line") + } + return htmlutil.HTMLFormat(``, title) +} + +func (ut *RenderUtils) RenderUnicodeEscapeToggleTd(combined, escapeStatus *charset.EscapeStatus) template.HTML { + if combined == nil || !combined.Escaped { + return "" + } + return `` + ut.RenderUnicodeEscapeToggleButton(escapeStatus) + `` +} diff --git a/modules/templates/util_slice.go b/modules/templates/util_slice.go index a3318cc11a..e74b308471 100644 --- a/modules/templates/util_slice.go +++ b/modules/templates/util_slice.go @@ -6,6 +6,11 @@ package templates import ( "fmt" "reflect" + "slices" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/util" ) type SliceUtils struct{} @@ -33,3 +38,29 @@ func (su *SliceUtils) Contains(s, v any) bool { } return false } + +// JoinInt64 joins a slice of int64 values into a comma-separated string. +func (su *SliceUtils) JoinInt64(values []int64) string { + if len(values) == 0 { + return "" + } + strs := make([]string, len(values)) + for i, v := range values { + strs[i] = strconv.FormatInt(v, 10) + } + return strings.Join(strs, ",") +} + +func (su *SliceUtils) JoinToggleIDs(values []int64, target int64) (ret struct { + IsIncluded bool + ToggledIDs string +}, +) { + ret.IsIncluded = slices.Contains(values, target) + if ret.IsIncluded { + ret.ToggledIDs = su.JoinInt64(util.SliceRemoveAll(slices.Clone(values), target)) + } else { + ret.ToggledIDs = su.JoinInt64(append(values, target)) + } + return ret +} diff --git a/modules/templates/util_test.go b/modules/templates/util_test.go index a6448a6ff2..88b05485af 100644 --- a/modules/templates/util_test.go +++ b/modules/templates/util_test.go @@ -70,6 +70,16 @@ func TestUtils(t *testing.T) { actual = execTmpl("{{StringUtils.Contains .String .Value}}", map[string]any{"String": "abc", "Value": "x"}) assert.Equal(t, "false", actual) + // Test JoinInt64 + actual = execTmpl("{{SliceUtils.JoinInt64 .Values}}", map[string]any{"Values": []int64{1, 2, 3}}) + assert.Equal(t, "1,2,3", actual) + + actual = execTmpl("{{SliceUtils.JoinInt64 .Values}}", map[string]any{"Values": []int64{}}) + assert.Empty(t, actual) + + actual = execTmpl("{{SliceUtils.JoinInt64 .Values}}", map[string]any{"Values": []int64{42}}) + assert.Equal(t, "42", actual) + tmpl := template.New("test") tmpl.Funcs(template.FuncMap{"SliceUtils": NewSliceUtils, "StringUtils": NewStringUtils}) template.Must(tmpl.Parse("{{SliceUtils.Contains .Slice .Value}}")) diff --git a/modules/test/logchecker.go b/modules/test/logchecker.go index 829f735c7c..387b4f3fc5 100644 --- a/modules/test/logchecker.go +++ b/modules/test/logchecker.go @@ -53,11 +53,11 @@ func (lc *LogChecker) checkLogEvent(event *log.EventFormatted) { } } -var checkerIndex int64 +var checkerIndex atomic.Int64 func NewLogChecker(namePrefix string) (logChecker *LogChecker, cancel func()) { logger := log.GetManager().GetLogger(namePrefix) - newCheckerIndex := atomic.AddInt64(&checkerIndex, 1) + newCheckerIndex := checkerIndex.Add(1) writerName := namePrefix + "-" + strconv.FormatInt(newCheckerIndex, 10) lc := &LogChecker{} diff --git a/modules/test/utils.go b/modules/test/utils.go index 458cf547d5..d632fb4e13 100644 --- a/modules/test/utils.go +++ b/modules/test/utils.go @@ -5,12 +5,16 @@ package test import ( "archive/tar" + "archive/zip" "bytes" "compress/gzip" "io" "net/http" "net/http/httptest" + "os" + "strconv" "strings" + "sync" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/util" @@ -46,7 +50,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) @@ -97,6 +101,17 @@ func WriteTarArchive(files map[string]string) *bytes.Buffer { return WriteTarCompression(func(w io.Writer) io.WriteCloser { return util.NopCloser{Writer: w} }, files) } +func WriteZipArchive(files map[string]string) *bytes.Buffer { + buf := &bytes.Buffer{} + zw := zip.NewWriter(buf) + for name, content := range files { + w, _ := zw.Create(name) + _, _ = w.Write([]byte(content)) + } + _ = zw.Close() + return buf +} + func WriteTarCompression[F func(io.Writer) io.WriteCloser | func(io.Writer) (io.WriteCloser, error)](compression F, files map[string]string) *bytes.Buffer { buf := &bytes.Buffer{} var cw io.WriteCloser @@ -129,3 +144,41 @@ func CompressGzip(content string) *bytes.Buffer { _ = cw.Close() return buf } + +var AllowSkipExternalService = sync.OnceValue(func() bool { + isLocalTesting := os.Getenv("CI") == "" + ciSkipExternal, _ := strconv.ParseBool(os.Getenv("GITEA_TEST_CI_SKIP_EXTERNAL")) + return isLocalTesting || ciSkipExternal +}) + +type TestingT interface { + Helper() + Skipf(format string, args ...any) + Errorf(format string, args ...any) + Fatalf(format string, args ...any) +} + +func ExternalServiceHTTP(t TestingT, envVarName, def string) string { + t.Helper() + val := util.IfZero(os.Getenv(envVarName), def) + if val == "" { + if AllowSkipExternalService() { + t.Skipf("skipping test because %s is not set", envVarName) + } else { + t.Fatalf("%s is not set, but skipping is not allowed in CI", envVarName) + } + } + // minio's endpoint is "host:port" pattern + testURL := util.Iif(strings.Contains(val, "://"), val, "http://"+val) + resp, err := http.Get(testURL) + if err != nil { + if AllowSkipExternalService() { + t.Skipf("skipping test because %s is not ready", val) + } else { + t.Fatalf("%s is not ready, but skipping is not allowed in CI", val) + } + } else { + _ = resp.Body.Close() + } + return val +} diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index 39232a3eed..151ca39703 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -90,8 +90,8 @@ func (w *testLoggerWriterCloser) Reset() { w.Unlock() } -// Printf takes a format and args and prints the string to os.Stdout -func Printf(format string, args ...any) { +// stdoutPrintf takes a format and args and prints the string to os.Stdout +func stdoutPrintf(format string, args ...any) { if !log.CanColorStdout { for i := range args { if c, ok := args[i].(*log.ColoredValue); ok { @@ -118,20 +118,20 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() { deferHasRun := false t.Cleanup(func() { if !deferHasRun { - Printf("!!! %s defer function hasn't been run but Cleanup is called, usually caused by panic", t.Name()) + stdoutPrintf("!!! %s: defer function hasn't been run but Cleanup is called, usually caused by panic\n", t.Name()) } }) - Printf("=== %s (%s:%d)\n", log.NewColoredValue(t.Name()), strings.TrimPrefix(filename, prefix), line) + stdoutPrintf("=== %s (%s:%d)\n", log.NewColoredValue(t.Name()), strings.TrimPrefix(filename, prefix), line) WriterCloser.pushT(t) timeoutChecker := time.AfterFunc(TestTimeout, func() { - Printf("!!! %s ... timeout: %v ... stacktrace:\n%s\n\n", log.NewColoredValue(t.Name(), log.Bold, log.FgRed), TestTimeout, getRuntimeStackAll()) + stdoutPrintf("!!! %s ... timeout: %v ... stacktrace:\n%s\n\n", log.NewColoredValue(t.Name(), log.Bold, log.FgRed), TestTimeout, getRuntimeStackAll()) }) return func() { deferHasRun = true flushStart := time.Now() slowFlushChecker := time.AfterFunc(TestSlowFlush, func() { - Printf("+++ %s ... still flushing after %v ...\n", log.NewColoredValue(t.Name(), log.Bold, log.FgRed), TestSlowFlush) + stdoutPrintf("+++ %s ... still flushing after %v ...\n", log.NewColoredValue(t.Name(), log.Bold, log.FgRed), TestSlowFlush) }) if err := queue.GetManager().FlushAll(t.Context(), -1); err != nil { // if panic occurs, then the t.Context() is also cancelled ahead, so here it shows "context canceled" error. @@ -143,7 +143,7 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() { runDuration := time.Since(runStart) flushDuration := time.Since(flushStart) if runDuration > TestSlowRun { - Printf("+++ %s is a slow test (run: %v, flush: %v)\n", log.NewColoredValue(t.Name(), log.Bold, log.FgYellow), runDuration, flushDuration) + stdoutPrintf("+++ %s is a slow test (run: %v, flush: %v)\n", log.NewColoredValue(t.Name(), log.Bold, log.FgYellow), runDuration, flushDuration) } WriterCloser.popT() } @@ -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/typesniffer/typesniffer.go b/modules/typesniffer/typesniffer.go index 0c4867d8f0..90423d48ce 100644 --- a/modules/typesniffer/typesniffer.go +++ b/modules/typesniffer/typesniffer.go @@ -183,3 +183,7 @@ func DetectContentType(data []byte) SniffedType { } return SniffedType{ct} } + +func FromContentType(contentType string) SniffedType { + return SniffedType{contentType} +} diff --git a/modules/util/diff_slice_test.go b/modules/util/diff_slice_test.go new file mode 100644 index 0000000000..cd61fb571b --- /dev/null +++ b/modules/util/diff_slice_test.go @@ -0,0 +1,74 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDiffSliceBasic(t *testing.T) { + // Typical integer cases + t.Run("additions", func(t *testing.T) { + added, removed := DiffSlice([]int{1, 2}, []int{1, 2, 3}) + assert.Equal(t, []int{3}, added) + assert.Empty(t, removed) + }) + + t.Run("removals", func(t *testing.T) { + added, removed := DiffSlice([]int{1, 2, 3}, []int{1, 2}) + assert.Empty(t, added) + assert.Equal(t, []int{3}, removed) + }) + + t.Run("no changes", func(t *testing.T) { + added, removed := DiffSlice([]int{1, 2}, []int{1, 2}) + assert.Empty(t, added) + assert.Empty(t, removed) + }) + + t.Run("empty slices", func(t *testing.T) { + added, removed := DiffSlice([]int{}, []int{}) + assert.Empty(t, added) + assert.Empty(t, removed) + }) + + t.Run("overlapping elements", func(t *testing.T) { + added, removed := DiffSlice([]int{1, 2, 4}, []int{2, 3, 4}) + assert.Equal(t, []int{3}, added) + assert.Equal(t, []int{1}, removed) + }) +} + +func TestDiffSliceOrderAndDuplicates(t *testing.T) { + oldSlice := []int{1, 2, 2, 3} + newSlice := []int{2, 4, 2, 5} + + added, removed := DiffSlice(oldSlice, newSlice) + assert.Equal(t, []int{4, 5}, added) + assert.Equal(t, []int{1, 3}, removed) +} + +func TestDiffSliceDeduplicatesOutput(t *testing.T) { + // Test case from issue: newSlice contains [4, 4, 5] and oldSlice is [1] + // added should return [4, 5], not [4, 4, 5] + t.Run("deduplicates added", func(t *testing.T) { + added, removed := DiffSlice([]int{1}, []int{4, 4, 5}) + assert.Equal(t, []int{4, 5}, added) + assert.Equal(t, []int{1}, removed) + }) + + t.Run("deduplicates removed", func(t *testing.T) { + added, removed := DiffSlice([]int{1, 1, 2}, []int{3}) + assert.Equal(t, []int{3}, added) + assert.Equal(t, []int{1, 2}, removed) + }) + + t.Run("deduplicates both", func(t *testing.T) { + added, removed := DiffSlice([]int{1, 1, 2, 2}, []int{3, 3, 4, 4}) + assert.Equal(t, []int{3, 4}, added) + assert.Equal(t, []int{1, 2}, removed) + }) +} diff --git a/modules/util/sanitize.go b/modules/util/sanitize.go index 0dd8b342a2..88ca34788f 100644 --- a/modules/util/sanitize.go +++ b/modules/util/sanitize.go @@ -5,7 +5,8 @@ package util import ( "bytes" - "unicode" + "net" + "strings" ) type sanitizedError struct { @@ -25,48 +26,103 @@ func SanitizeErrorCredentialURLs(err error) error { return sanitizedError{err: err} } -const userPlaceholder = "sanitized-credential" - var schemeSep = []byte("://") -// SanitizeCredentialURLs remove all credentials in URLs (starting with "scheme://") for the input string: "https://user:pass@domain.com" => "https://sanitized-credential@domain.com" +const userInfoPlaceholder = "(masked)" + +// SanitizeCredentialURLs remove all credentials in URLs for the input string: +// * "https://userinfo@domain.com" => "https://***@domain.com" +// * "user:pass@domain.com" => "***@domain.com" +// "***" is a magic string internally used, doesn't guarantee to be anything. func SanitizeCredentialURLs(s string) string { + sepColPos := strings.Index(s, ":") + if sepColPos == -1 { + return s // fast path: no colon, unlikely contain any URL credential + } + sepAtPos := strings.Index(s[sepColPos+1:], "@") + for sepAtPos == -1 { + return s // fast path: no "@" after colon, unlikely contain any URL credential + } + sepAtPos += sepColPos + 1 + + res := make([]byte, 0, len(s)+len(userInfoPlaceholder)) // a best guess to avoid too many re-allocations bs := UnsafeStringToBytes(s) - schemeSepPos := bytes.Index(bs, schemeSep) - if schemeSepPos == -1 || bytes.IndexByte(bs[schemeSepPos:], '@') == -1 { - return s // fast return if there is no URL scheme or no userinfo - } - out := make([]byte, 0, len(bs)+len(userPlaceholder)) - for schemeSepPos != -1 { - schemeSepPos += 3 // skip the "://" - sepAtPos := -1 // the possible '@' position: "https://foo@[^here]host" - sepEndPos := schemeSepPos // the possible end position: "The https://host[^here] in log for test" - sepLoop: - for ; sepEndPos < len(bs); sepEndPos++ { - c := bs[sepEndPos] - if ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9') { - continue - } + for { + // left part (before "@") is likely to be the "userinfo" (single username, or "username:password") + leftPos := sepAtPos - 1 + leftLoop: + for leftPos >= 0 { + c := bs[leftPos] switch c { - case '@': - sepAtPos = sepEndPos case '-', '.', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '%': - continue // due to RFC 3986, userinfo can contain - . _ ~ ! $ & ' ( ) * + , ; = : and any percent-encoded chars + // RFC 3986, userinfo can contain - . _ ~ ! $ & ' ( ) * + , ; = : and any percent-encoded chars default: - break sepLoop // if it is an invalid char for URL (eg: space, '/', and others), stop the loop + valid := 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' + if !valid { + break leftLoop + } } + leftPos-- } - // if there is '@', and the string is like "s://u@h", then hide the "u" part - if sepAtPos != -1 && (schemeSepPos >= 4 && unicode.IsLetter(rune(bs[schemeSepPos-4]))) && sepAtPos-schemeSepPos > 0 && sepEndPos-sepAtPos > 0 { - out = append(out, bs[:schemeSepPos]...) - out = append(out, userPlaceholder...) - out = append(out, bs[sepAtPos:sepEndPos]...) + // left pos should point to the beginning of the left part, this pos is always valid in the buffer + leftPos++ + + // right part is likely to be the host (domain name, ip address) + rightPos := sepAtPos + 1 + rightLoop: + for rightPos < len(bs) { + c := bs[rightPos] + switch c { + case '.', '-': + // valid host char + case '[': + // ipv6 begin + if rightPos != sepAtPos+1 { + break rightLoop + } + case ']': + // ipv6 end + rightPos++ + break rightLoop + default: + valid := 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' + if bs[sepAtPos+1] == '[' { + // ipv6 host + valid = 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' || '0' <= c && c <= '9' || c == ':' + } + if !valid { + break rightLoop + } + } + rightPos++ + } + + leading, leftPart, rightPart := bs[:leftPos], bs[leftPos:sepAtPos], bs[sepAtPos+1:rightPos] + + // Either: + // * git log message: "user:pass@host" (it contains a colon in userinfo), ignore "git@host" pattern + // * http like URL: "https://userinfo@host.com" (it has "://" before the userinfo) + needSanitize := bytes.IndexByte(leftPart, ':') >= 0 || bytes.HasSuffix(leading, schemeSep) + needSanitize = needSanitize && len(leftPart) > 0 && len(rightPart) > 0 + // TODO: can also do more checks for right part + // for example: ipv6 quick check + if needSanitize && rightPart[0] == '[' { + needSanitize = rightPart[len(rightPart)-1] == ']' && net.ParseIP(UnsafeBytesToString(rightPart[1:len(rightPart)-1])) != nil + } + if needSanitize { + res = append(res, leading...) + res = append(res, userInfoPlaceholder...) + res = append(res, '@') + res = append(res, rightPart...) } else { - out = append(out, bs[:sepEndPos]...) + res = append(res, bs[:rightPos]...) + } + bs = bs[rightPos:] + sepAtPos = bytes.IndexByte(bs, '@') + if sepAtPos == -1 { + break } - bs = bs[sepEndPos:] - schemeSepPos = bytes.Index(bs, schemeSep) } - out = append(out, bs...) - return UnsafeBytesToString(out) + res = append(res, bs...) + return UnsafeBytesToString(res) } diff --git a/modules/util/sanitize_test.go b/modules/util/sanitize_test.go index 0bcfd45ca4..c1a80016f8 100644 --- a/modules/util/sanitize_test.go +++ b/modules/util/sanitize_test.go @@ -13,7 +13,7 @@ import ( func TestSanitizeErrorCredentialURLs(t *testing.T) { err := errors.New("error with https://a@b.com") se := SanitizeErrorCredentialURLs(err) - assert.Equal(t, "error with https://"+userPlaceholder+"@b.com", se.Error()) + assert.Equal(t, "error with https://"+userInfoPlaceholder+"@b.com", se.Error()) } func TestSanitizeCredentialURLs(t *testing.T) { @@ -27,15 +27,35 @@ func TestSanitizeCredentialURLs(t *testing.T) { }, { "https://mytoken@github.com/go-gitea/test_repo.git", - "https://" + userPlaceholder + "@github.com/go-gitea/test_repo.git", + "https://" + userInfoPlaceholder + "@github.com/go-gitea/test_repo.git", }, { "https://user:password@github.com/go-gitea/test_repo.git", - "https://" + userPlaceholder + "@github.com/go-gitea/test_repo.git", + "https://" + userInfoPlaceholder + "@github.com/go-gitea/test_repo.git", + }, + { + "https://user:password@[::]/go-gitea/test_repo.git", + "https://" + userInfoPlaceholder + "@[::]/go-gitea/test_repo.git", + }, + { + "https://user:password@[2001:db8::1]:8080/go-gitea/test_repo.git", + "https://" + userInfoPlaceholder + "@[2001:db8::1]:8080/go-gitea/test_repo.git", + }, + { + "see https://u:p@[::1]/x and https://u2:p2@h2", + "see https://" + userInfoPlaceholder + "@[::1]/x and https://" + userInfoPlaceholder + "@h2", + }, + { + "https://user:secret@[unclosed-ipv6", + "https://user:secret@[unclosed-ipv6", + }, + { + "https://user:secret@[invalid-ipv6]", + "https://user:secret@[invalid-ipv6]", }, { "ftp://x@", - "ftp://" + userPlaceholder + "@", + "ftp://x@", }, { "ftp://x/@", @@ -43,27 +63,40 @@ func TestSanitizeCredentialURLs(t *testing.T) { }, { "ftp://u@x/@", // test multiple @ chars - "ftp://" + userPlaceholder + "@x/@", + "ftp://" + userInfoPlaceholder + "@x/@", }, { "😊ftp://u@x😊", // test unicode - "😊ftp://" + userPlaceholder + "@x😊", + "😊ftp://" + userInfoPlaceholder + "@x😊", }, { "://@", "://@", }, { - "//u:p@h", // do not process URLs without explicit scheme, they are not treated as "valid" URLs because there is no scheme context in string "//u:p@h", + "//" + userInfoPlaceholder + "@h", }, { - "s://u@h", // the minimal pattern to be sanitized - "s://" + userPlaceholder + "@h", + "s://u@h", + "s://" + userInfoPlaceholder + "@h", }, { "URLs in log https://u:b@h and https://u:b@h:80/, with https://h.com and u@h.com", - "URLs in log https://" + userPlaceholder + "@h and https://" + userPlaceholder + "@h:80/, with https://h.com and u@h.com", + "URLs in log https://" + userInfoPlaceholder + "@h and https://" + userInfoPlaceholder + "@h:80/, with https://h.com and u@h.com", + }, + { + "fatal: unable to look up username:token@github.com (port 9418)", + "fatal: unable to look up " + userInfoPlaceholder + "@github.com (port 9418)", + }, + { + "git failed for user:token@github.com/go-gitea/test_repo.git", + "git failed for " + userInfoPlaceholder + "@github.com/go-gitea/test_repo.git", + }, + { + // SSH-form git URL ("git@host:path") must not let a later credential URL through + "failed remote git@github.com:foo, retried via https://user:tok@github.com/foo", + "failed remote git@github.com:foo, retried via https://" + userInfoPlaceholder + "@github.com/foo", }, } diff --git a/modules/util/timer_test.go b/modules/util/timer_test.go index 602800c248..1f9a4ac586 100644 --- a/modules/util/timer_test.go +++ b/modules/util/timer_test.go @@ -12,19 +12,19 @@ import ( ) func TestDebounce(t *testing.T) { - var c int64 + var c atomic.Int64 d := Debounce(50 * time.Millisecond) - d(func() { atomic.AddInt64(&c, 1) }) - assert.EqualValues(t, 0, atomic.LoadInt64(&c)) - d(func() { atomic.AddInt64(&c, 1) }) - d(func() { atomic.AddInt64(&c, 1) }) + d(func() { c.Add(1) }) + assert.EqualValues(t, 0, c.Load()) + d(func() { c.Add(1) }) + d(func() { c.Add(1) }) time.Sleep(100 * time.Millisecond) - assert.EqualValues(t, 1, atomic.LoadInt64(&c)) - d(func() { atomic.AddInt64(&c, 1) }) - assert.EqualValues(t, 1, atomic.LoadInt64(&c)) - d(func() { atomic.AddInt64(&c, 1) }) - d(func() { atomic.AddInt64(&c, 1) }) - d(func() { atomic.AddInt64(&c, 1) }) + assert.EqualValues(t, 1, c.Load()) + d(func() { c.Add(1) }) + assert.EqualValues(t, 1, c.Load()) + d(func() { c.Add(1) }) + d(func() { c.Add(1) }) + d(func() { c.Add(1) }) time.Sleep(100 * time.Millisecond) - assert.EqualValues(t, 2, atomic.LoadInt64(&c)) + assert.EqualValues(t, 2, c.Load()) } diff --git a/modules/util/util.go b/modules/util/util.go index 7d1343f20d..17733736be 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -6,11 +6,16 @@ package util import ( "bytes" "crypto/rand" + "encoding/hex" "fmt" "math/big" + rand2 "math/rand/v2" "slices" "strconv" "strings" + "sync" + + "code.gitea.io/gitea/modules/container" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -58,37 +63,60 @@ func NormalizeEOL(input []byte) []byte { } // CryptoRandomInt returns a crypto random integer between 0 and limit, inclusive -func CryptoRandomInt(limit int64) (int64, error) { +func CryptoRandomInt(limit int64) int64 { rInt, err := rand.Int(rand.Reader, big.NewInt(limit)) if err != nil { - return 0, err + panic(err) // this should never happen } - return rInt.Int64(), nil + return rInt.Int64() } -const alphanumericalChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - // CryptoRandomString generates a crypto random alphanumerical string, each byte is generated by [0,61] range -func CryptoRandomString(length int64) (string, error) { +func CryptoRandomString(length int64) string { + const alphanumericalChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" buf := make([]byte, length) limit := int64(len(alphanumericalChars)) for i := range buf { - num, err := CryptoRandomInt(limit) - if err != nil { - return "", err - } + num := CryptoRandomInt(limit) buf[i] = alphanumericalChars[num] } - return string(buf), nil + return string(buf) } // CryptoRandomBytes generates `length` crypto bytes // This differs from CryptoRandomString, as each byte in CryptoRandomString is generated by [0,61] range // This function generates totally random bytes, each byte is generated by [0,255] range -func CryptoRandomBytes(length int64) ([]byte, error) { +func CryptoRandomBytes(length int64) []byte { buf := make([]byte, length) - _, err := rand.Read(buf) - return buf, err + if _, err := rand.Read(buf); err != nil { + panic(err) // this should never happen, "rand.Read" never fails + } + return buf +} + +var chaCha8RandPool = sync.OnceValue(func() *sync.Pool { + return &sync.Pool{ + New: func() any { + seed := CryptoRandomBytes(32) + return rand2.NewChaCha8([32]byte(seed)) + }, + } +}) + +func FastCryptoRandomBytes(length int) []byte { + // ChaCha8 is about 20x times faster than system's crypto/rand. + // It is suitable for UUIDs, session IDs, etc + pool := chaCha8RandPool() + chaCha8Rand := pool.Get().(*rand2.ChaCha8) + defer pool.Put(chaCha8Rand) + buf := make([]byte, length) + _, _ = chaCha8Rand.Read(buf) + return buf +} + +func FastCryptoRandomHex(length int) string { + buf := FastCryptoRandomBytes(length / 2) + return hex.EncodeToString(buf) } // ToLowerASCII returns s with all ASCII letters mapped to their lower case. @@ -255,11 +283,31 @@ 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))) +} + +func DiffSlice[T comparable](oldSlice, newSlice []T) (added, removed []T) { + oldSet := container.SetOf(oldSlice...) + newSet := container.SetOf(newSlice...) + + addedSet, removedSet := container.Set[T]{}, container.Set[T]{} + for _, v := range newSlice { + if !oldSet.Contains(v) && addedSet.Add(v) { + added = append(added, v) + } + } + for _, v := range oldSlice { + if !newSet.Contains(v) && removedSet.Add(v) { + removed = append(removed, v) + } + } + return added, removed } diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 4ec738e89e..7dbb14e374 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -86,35 +86,31 @@ func Test_NormalizeEOL(t *testing.T) { } func Test_RandomInt(t *testing.T) { - randInt, err := CryptoRandomInt(255) + randInt := CryptoRandomInt(255) assert.GreaterOrEqual(t, randInt, int64(0)) assert.LessOrEqual(t, randInt, int64(255)) - assert.NoError(t, err) } func Test_RandomString(t *testing.T) { - str1, err := CryptoRandomString(32) - assert.NoError(t, err) + str1 := CryptoRandomString(32) + var err error matches, err := regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1) assert.NoError(t, err) assert.True(t, matches) - str2, err := CryptoRandomString(32) - assert.NoError(t, err) + str2 := CryptoRandomString(32) matches, err = regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1) assert.NoError(t, err) assert.True(t, matches) assert.NotEqual(t, str1, str2) - str3, err := CryptoRandomString(256) - assert.NoError(t, err) + str3 := CryptoRandomString(256) matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str3) assert.NoError(t, err) assert.True(t, matches) - str4, err := CryptoRandomString(256) - assert.NoError(t, err) + str4 := CryptoRandomString(256) matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str4) assert.NoError(t, err) assert.True(t, matches) @@ -123,19 +119,15 @@ func Test_RandomString(t *testing.T) { } func Test_RandomBytes(t *testing.T) { - bytes1, err := CryptoRandomBytes(32) - assert.NoError(t, err) + bytes1 := CryptoRandomBytes(32) - bytes2, err := CryptoRandomBytes(32) - assert.NoError(t, err) + bytes2 := CryptoRandomBytes(32) assert.NotEqual(t, bytes1, bytes2) - bytes3, err := CryptoRandomBytes(256) - assert.NoError(t, err) + bytes3 := CryptoRandomBytes(256) - bytes4, err := CryptoRandomBytes(256) - assert.NoError(t, err) + bytes4 := CryptoRandomBytes(256) assert.NotEqual(t, bytes3, bytes4) } @@ -175,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) { @@ -192,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 86364e1173..c9de1be96c 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -5,13 +5,14 @@ package validation import ( "fmt" + "io" "regexp" "strings" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/glob" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/json" "gitea.com/go-chi/binding" ) @@ -31,10 +32,24 @@ const ( ErrInvalidBadgeSlug = "InvalidBadgeSlug" ) +type jsonProvider struct{} + +func (j jsonProvider) Marshal(v any) ([]byte, error) { return json.Marshal(v) } + +func (j jsonProvider) Unmarshal(data []byte, v any) error { return json.Unmarshal(data, v) } + +func (j jsonProvider) NewDecoder(reader io.Reader) binding.JSONDecoder { + return json.NewDecoder(reader) +} + +func (j jsonProvider) NewEncoder(writer io.Writer) binding.JSONEncoder { + return json.NewEncoder(writer) +} + // AddBindingRules adds additional binding rules func AddBindingRules() { + binding.JSONProvider = jsonProvider{} addGitRefNameBindingRule() - addValidURLListBindingRule() addValidURLBindingRule() addValidSiteURLBindingRule() addGlobPatternRule() @@ -63,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 0cd328f312..d30eb1bbb1 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 ee05de74bd..7695529beb 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 75f73b97fc..a3c3acb16c 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 cccc570a1a..0000000000 --- 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/handler.go b/modules/web/handler.go index d113bbba45..ce3b49eaef 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -4,9 +4,12 @@ package web import ( + "bufio" "fmt" + "net" "net/http" "reflect" + "slices" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/web/routing" @@ -47,6 +50,13 @@ func (r *responseWriter) WriteHeader(statusCode int) { r.respWriter.WriteHeader(statusCode) } +func (r *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if hj, ok := r.respWriter.(http.Hijacker); ok { + return hj.Hijack() + } + return nil, nil, http.ErrNotSupported +} + var ( httpReqType = reflect.TypeFor[*http.Request]() respWriterType = reflect.TypeFor[http.ResponseWriter]() @@ -122,8 +132,8 @@ type middlewareProvider = func(next http.Handler) http.Handler func executeMiddlewaresHandler(w http.ResponseWriter, r *http.Request, middlewares []middlewareProvider, endpoint http.HandlerFunc) { handler := endpoint - for i := len(middlewares) - 1; i >= 0; i-- { - handler = middlewares[i](handler).ServeHTTP + for _, middleware := range slices.Backward(middlewares) { + handler = middleware(handler).ServeHTTP } handler(w, r) } diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 05047ad3bd..85d8541081 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -29,7 +29,7 @@ func AssignForm(form any, data map[string]any) { typ := reflect.TypeOf(form) val := reflect.ValueOf(form) - for typ.Kind() == reflect.Ptr { + for typ.Kind() == reflect.Pointer { typ = typ.Elem() val = val.Elem() } @@ -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 { + if typ.Kind() == reflect.Pointer { 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/cookie.go b/modules/web/middleware/cookie.go index 336c276fe8..5456e3c6b7 100644 --- a/modules/web/middleware/cookie.go +++ b/modules/web/middleware/cookie.go @@ -35,6 +35,11 @@ func DeleteRedirectToCookie(resp http.ResponseWriter) { } func RedirectLinkUserLogin(req *http.Request) string { + if req.Header.Get("X-Gitea-Fetch-Action") != "" { + // when building the redirect link for a fetch request, the current link might be a partial page, + // so we only redirect to the login page without redirect_to parameter + return setting.AppSubURL + "/user/login" + } return setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(setting.AppSubURL+req.URL.RequestURI()) } diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index 41fb1e7e6f..7d9e816042 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -7,6 +7,7 @@ import ( "context" "time" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" ) @@ -36,5 +37,6 @@ func CommonTemplateContextData() reqctx.ContextData { "PageStartTime": time.Now(), "RunModeIsProd": setting.IsProd, + "ViteModeIsDev": public.IsViteDevMode(), } } diff --git a/modules/web/middleware/locale.go b/modules/web/middleware/locale.go index 34a16f04e7..fc396f0808 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 5ef18e9679..30f25deb4e 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)) } } @@ -266,7 +260,7 @@ func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Reques // do not respond to other requests, to simulate a real sub-path environment resp.Header().Add("Content-Type", "text/html; charset=utf-8") resp.WriteHeader(http.StatusNotFound) - _, _ = resp.Write([]byte(htmlutil.HTMLFormat(`404 page not found, sub-path is: %s`, setting.AppSubURL, setting.AppSubURL))) + _, _ = htmlutil.HTMLPrintf(resp, `404 page not found, sub-path is: %s`, setting.AppSubURL, setting.AppSubURL) return } normalized = true diff --git a/modules/web/router_path.go b/modules/web/router_path.go index 9e531346d1..7278241b6d 100644 --- a/modules/web/router_path.go +++ b/modules/web/router_path.go @@ -55,6 +55,7 @@ func (g *RouterPathGroup) MatchPattern(methods string, pattern *RouterPathGroupP type routerPathParam struct { name string + pathSepEnd bool captureGroup int } @@ -93,7 +94,15 @@ func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool { } for i, pm := range paramMatches { groupIdx := p.params[i].captureGroup * 2 - chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]]) + if pm[groupIdx] == -1 || pm[groupIdx+1] == -1 { + chiCtx.URLParams.Add(p.params[i].name, "") + continue + } + val := path[pm[groupIdx]:pm[groupIdx+1]] + if p.params[i].pathSepEnd { + val = strings.TrimSuffix(val, "/") + } + chiCtx.URLParams.Add(p.params[i].name, val) } return true } @@ -145,11 +154,19 @@ func patternRegexp(pattern string, h ...any) *RouterPathGroupPattern { // it is not used so no need to implement it now param := routerPathParam{} if partExp == "*" { - re = append(re, "(.*?)/?"...) + // "" is a shorthand for optionally matching any string (but not greedy) + partExp = ".*?" if lastEnd < len(pattern) && pattern[lastEnd] == '/' { - lastEnd++ // the "*" pattern is able to handle the last slash, so skip it + // if this param part ends with path separator "/", then consider it together: "(.*?/)" + partExp += "/" + param.pathSepEnd = true + lastEnd++ } + re = append(re, '(') + re = append(re, partExp...) + re = append(re, ')', '?') // the wildcard matching is optional } else { + // the pattern is user-provided regexp, defaults to a path part (separated by "/") partExp = util.IfZero(partExp, "[^/]+") re = append(re, '(') re = append(re, partExp...) diff --git a/modules/web/router_test.go b/modules/web/router_test.go index 645e70b869..c1c314f85e 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" @@ -100,7 +101,8 @@ func TestPathProcessor(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.RouteMethod = "GET" p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound) - assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) + shouldProcess := expectedPathParams != nil + assert.Equal(t, shouldProcess, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri) } @@ -113,6 +115,10 @@ func TestPathProcessor(t *testing.T) { testProcess("//", "/a", map[string]string{"p1": "", "p2": "a"}) testProcess("//", "/a/b", map[string]string{"p1": "a", "p2": "b"}) testProcess("//", "/a/b/c", map[string]string{"p1": "a/b", "p2": "c"}) + testProcess("//part/", "/a/part/c", map[string]string{"p1": "a", "p2": "c"}) + testProcess("//part/", "/part/c", map[string]string{"p1": "", "p2": "c"}) + testProcess("//part/", "/a/other-part/c", nil) + testProcess("/-part/", "/a-other-part/c", map[string]string{"p1": "a-other", "p2": "c"}) } func TestRouter(t *testing.T) { @@ -307,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 838abea158..7799a24e94 100644 --- a/modules/web/routing/context.go +++ b/modules/web/routing/context.go @@ -8,13 +8,20 @@ import ( "net/http" "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() {} @@ -23,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() @@ -31,22 +38,51 @@ func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) { return end } +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 +} + // 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 { +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 := getRequestRecord(req.Context()) + if record == nil { return } record.lock.Lock() - record.isLongPolling = true + record.logLevel = log.TRACE record.lock.Unlock() } // 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/routing/logger.go b/modules/web/routing/logger.go index a6a0e0d517..818766fbd2 100644 --- a/modules/web/routing/logger.go +++ b/modules/web/routing/logger.go @@ -5,26 +5,12 @@ package routing import ( "net/http" - "strings" "time" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/web/types" ) -// NewLoggerHandler is a handler that will log routing to the router log taking account of -// routing information -func NewLoggerHandler() func(next http.Handler) http.Handler { - manager := requestRecordsManager{ - requestRecords: map[uint64]*requestRecord{}, - } - manager.startSlowQueryDetector(3 * time.Second) - - logger := log.GetLogger("router") - manager.print = logPrinter(logger) - return manager.handler -} - var ( startMessage = log.NewColoredValue("started ", log.DEBUG.ColorAttributes()...) slowMessage = log.NewColoredValue("slow ", log.WARN.ColorAttributes()...) @@ -36,17 +22,8 @@ var ( func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { const callerName = "HTTPRequest" - logTrace := func(fmt string, args ...any) { - logger.Log(2, &log.Event{Level: log.TRACE, Caller: callerName}, fmt, args...) - } - logInfo := func(fmt string, args ...any) { - logger.Log(2, &log.Event{Level: log.INFO, Caller: callerName}, fmt, args...) - } - logWarn := func(fmt string, args ...any) { - logger.Log(2, &log.Event{Level: log.WARN, Caller: callerName}, fmt, args...) - } - logError := func(fmt string, args ...any) { - logger.Log(2, &log.Event{Level: log.ERROR, Caller: callerName}, fmt, args...) + logRequest := func(level log.Level, fmt string, args ...any) { + logger.Log(2, &log.Event{Level: level, Caller: callerName}, fmt, args...) } return func(trigger Event, record *requestRecord) { if trigger == StartEvent { @@ -57,7 +34,7 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { } // when a request starts, we have no information about the handler function information, we only have the request path req := record.request - logTrace("router: %s %v %s for %s", startMessage, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr) + logRequest(log.TRACE, "router: %s %v %s for %s", startMessage, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr) return } @@ -73,12 +50,12 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { if trigger == StillExecutingEvent { message := slowMessage - logf := logWarn + logLevel := log.WARN if isLongPolling { - logf = logInfo + logLevel = log.INFO message = pollingMessage } - logf("router: %s %v %s for %s, elapsed %v @ %s", + logRequest(logLevel, "router: %s %v %s for %s, elapsed %v @ %s", message, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr, log.ColoredTime(time.Since(record.startTime)), @@ -88,7 +65,7 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { } if panicErr != nil { - logWarn("router: %s %v %s for %s, panic in %v @ %s, err=%v", + logRequest(log.WARN, "router: %s %v %s for %s, panic in %v @ %s, err=%v", failedMessage, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr, log.ColoredTime(time.Since(record.startTime)), @@ -99,24 +76,25 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { } var status int - if v, ok := record.responseWriter.(types.ResponseStatusProvider); ok { + if v, ok := record.respWriter.(types.ResponseStatusProvider); ok { status = v.WrittenStatus() } - logf := logInfo + logLevel := record.logLevel + if logLevel == log.UNDEFINED { + logLevel = log.INFO + } // lower the log level for some specific requests, in most cases these logs are not useful if status > 0 && status < 400 && - strings.HasPrefix(req.RequestURI, "/assets/") /* static assets */ || - req.RequestURI == "/user/events" /* Server-Sent Events (SSE) handler */ || req.RequestURI == "/api/actions/runner.v1.RunnerService/FetchTask" /* Actions Runner polling */ { - logf = logTrace + logLevel = log.TRACE } message := completedMessage if isUnknownHandler { - logf = logError + logLevel = log.ERROR message = unknownHandlerMessage } - logf("router: %s %v %s for %s, %v %v in %v @ %s", + logRequest(logLevel, "router: %s %v %s for %s, %v %v in %v @ %s", message, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr, log.ColoredStatus(status), log.ColoredStatus(status, http.StatusText(status)), log.ColoredTime(time.Since(record.startTime)), diff --git a/modules/web/routing/logger_manager.go b/modules/web/routing/logger_manager.go index 2f767c3d66..e63c73994f 100644 --- a/modules/web/routing/logger_manager.go +++ b/modules/web/routing/logger_manager.go @@ -6,7 +6,6 @@ package routing import ( "context" "fmt" - "net/http" "sync" "time" @@ -29,26 +28,21 @@ const ( EndEvent ) -// Printer is used to output the log for a request -type Printer func(trigger Event, record *requestRecord) +// logPrinterFunc is used to output the log for a request +type logPrinterFunc func(trigger Event, record *requestRecord) -type requestRecordsManager struct { - print Printer - - lock sync.Mutex - - requestRecords map[uint64]*requestRecord - count uint64 +type loggerRequestManager struct { + logPrint logPrinterFunc + reqRecords sync.Map // it only contains the active requests which haven't been detected as "slow" } -func (manager *requestRecordsManager) startSlowQueryDetector(threshold time.Duration) { +func (manager *loggerRequestManager) startSlowQueryDetector(threshold time.Duration) { go graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Service: SlowQueryDetector", process.SystemProcessType, true) defer finished() // This go-routine checks all active requests every second. // If a request has been running for a long time (eg: /user/events), we also print a log with "still-executing" message // After the "still-executing" log is printed, the record will be removed from the map to prevent from duplicated logs in future - // We do not care about accurate duration here. It just does the check periodically, 0.5s or 1.5s are all OK. t := time.NewTicker(time.Second) for { @@ -58,69 +52,39 @@ func (manager *requestRecordsManager) startSlowQueryDetector(threshold time.Dura case <-t.C: now := time.Now() - var slowRequests []*requestRecord - - // find all slow requests with lock - manager.lock.Lock() - for index, record := range manager.requestRecords { - if now.Sub(record.startTime) < threshold { - continue - } - - slowRequests = append(slowRequests, record) - delete(manager.requestRecords, index) - } - manager.lock.Unlock() - // print logs for slow requests - for _, record := range slowRequests { - manager.print(StillExecutingEvent, record) - } + manager.reqRecords.Range(func(key, value any) bool { + index, record := key.(uint64), value.(*requestRecord) + if now.Sub(record.startTime) >= threshold { + manager.logPrint(StillExecutingEvent, record) + manager.reqRecords.Delete(index) + } + return true + }) } } }) } -func (manager *requestRecordsManager) handler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - record := &requestRecord{ - startTime: time.Now(), - request: req, - responseWriter: w, +func (manager *loggerRequestManager) handleRequestRecord(record *requestRecord) func() { + manager.reqRecords.Store(record.index, record) + manager.logPrint(StartEvent, record) + + return func() { + // just in case there is a panic. now the panics are all recovered in middleware.go + localPanicErr := recover() + if localPanicErr != nil { + record.lock.Lock() + record.panicError = fmt.Errorf("%v\n%s", localPanicErr, log.Stack(2)) + record.lock.Unlock() } - // generate a record index an insert into the map - manager.lock.Lock() - record.index = manager.count - manager.count++ - manager.requestRecords[record.index] = record - manager.lock.Unlock() + manager.reqRecords.Delete(record.index) + manager.logPrint(EndEvent, record) - defer func() { - // just in case there is a panic. now the panics are all recovered in middleware.go - localPanicErr := recover() - if localPanicErr != nil { - record.lock.Lock() - record.panicError = fmt.Errorf("%v\n%s", localPanicErr, log.Stack(2)) - record.lock.Unlock() - } - - // remove from the record map - manager.lock.Lock() - delete(manager.requestRecords, record.index) - manager.lock.Unlock() - - // log the end of the request - manager.print(EndEvent, record) - - if localPanicErr != nil { - // the panic wasn't recovered before us, so we should pass it up, and let the framework handle the panic error - panic(localPanicErr) - } - }() - - req = req.WithContext(context.WithValue(req.Context(), contextKey, record)) - manager.print(StartEvent, record) - next.ServeHTTP(w, req) - }) + if localPanicErr != nil { + // the panic wasn't recovered before us, so we should pass it up, and let the framework handle the panic error + panic(localPanicErr) + } + } } diff --git a/modules/web/routing/requestinfo.go b/modules/web/routing/requestinfo.go new file mode 100644 index 0000000000..a88b541008 --- /dev/null +++ b/modules/web/routing/requestinfo.go @@ -0,0 +1,43 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package routing + +import ( + "context" + "net/http" + "sync/atomic" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +// NewRequestInfoHandler is a handler that saves request info into request context. +// If router logger is enabled, it will also print request logs and detect slow requests. +func NewRequestInfoHandler() func(next http.Handler) http.Handler { + var reqLogger *loggerRequestManager + if setting.IsRouteLogEnabled() { + reqLogger = &loggerRequestManager{ + logPrint: logPrinter(log.GetLogger("router")), + } + reqLogger.startSlowQueryDetector(3 * time.Second) + } + var requestCounter atomic.Uint64 + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + record := &requestRecord{ + index: requestCounter.Add(1), + startTime: time.Now(), + respWriter: w, + } + req = req.WithContext(context.WithValue(req.Context(), contextKey, record)) + record.request = req + if reqLogger != nil { + end := reqLogger.handleRequestRecord(record) + defer end() + } + next.ServeHTTP(w, req) + }) + } +} diff --git a/modules/web/routing/requestrecord.go b/modules/web/routing/requestrecord.go index 888c3e5c2f..5ef6feac7e 100644 --- a/modules/web/routing/requestrecord.go +++ b/modules/web/routing/requestrecord.go @@ -7,22 +7,25 @@ import ( "net/http" "sync" "time" + + "code.gitea.io/gitea/modules/log" ) type requestRecord struct { - // index of the record in the records map - index uint64 - // immutable fields - startTime time.Time - request *http.Request - responseWriter http.ResponseWriter + index uint64 // unique number (per process) for the request + startTime time.Time + request *http.Request + respWriter http.ResponseWriter // mutex lock sync.RWMutex - // mutable fields + // below are mutable fields + funcInfo *FuncInfo + // * for "mark as long polling" isLongPolling bool - funcInfo *FuncInfo - panicError error + // * for router logger + logLevel log.Level + panicError error } diff --git a/modules/web/types/premiddleware.go b/modules/web/types/premiddleware.go new file mode 100644 index 0000000000..275b55b8c6 --- /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/fileicon/material-icon-rules.json b/options/fileicon/material-icon-rules.json index 9163732315..aa967b7100 100644 --- a/options/fileicon/material-icon-rules.json +++ b/options/fileicon/material-icon-rules.json @@ -615,6 +615,11 @@ "_github/workflows": "folder-gh-workflows", "-github/workflows": "folder-gh-workflows", "__github/workflows__": "folder-gh-workflows", + "gitea/workflows": "folder-gitea-workflows", + ".gitea/workflows": "folder-gitea-workflows", + "_gitea/workflows": "folder-gitea-workflows", + "-gitea/workflows": "folder-gitea-workflows", + "__gitea/workflows__": "folder-gitea-workflows", "git": "folder-git", ".git": "folder-git", "_git": "folder-git", @@ -990,6 +995,11 @@ "_vendors": "folder-lib", "-vendors": "folder-lib", "__vendors__": "folder-lib", + "thirdparty": "folder-lib", + ".thirdparty": "folder-lib", + "_thirdparty": "folder-lib", + "-thirdparty": "folder-lib", + "__thirdparty__": "folder-lib", "third-party": "folder-lib", ".third-party": "folder-lib", "_third-party": "folder-lib", @@ -1000,6 +1010,16 @@ "_lib64": "folder-lib", "-lib64": "folder-lib", "__lib64__": "folder-lib", + "external": "folder-lib", + ".external": "folder-lib", + "_external": "folder-lib", + "-external": "folder-lib", + "__external__": "folder-lib", + "externals": "folder-lib", + ".externals": "folder-lib", + "_externals": "folder-lib", + "-externals": "folder-lib", + "__externals__": "folder-lib", "themes": "folder-theme", ".themes": "folder-theme", "_themes": "folder-theme", @@ -1155,6 +1175,11 @@ "_dockerhub": "folder-docker", "-dockerhub": "folder-docker", "__dockerhub__": "folder-docker", + "nginx": "folder-nginx", + ".nginx": "folder-nginx", + "_nginx": "folder-nginx", + "-nginx": "folder-nginx", + "__nginx__": "folder-nginx", "astro": "folder-astro", ".astro": "folder-astro", "_astro": "folder-astro", @@ -1785,6 +1810,11 @@ "_pytest_cache": "folder-python", "-pytest_cache": "folder-python", "__pytest_cache__": "folder-python", + "r": "folder-r", + ".r": "folder-r", + "_r": "folder-r", + "-r": "folder-r", + "__r__": "folder-r", "sandbox": "folder-sandbox", ".sandbox": "folder-sandbox", "_sandbox": "folder-sandbox", @@ -4517,7 +4547,77 @@ ".forms": "folder-form", "_forms": "folder-form", "-forms": "folder-form", - "__forms__": "folder-form" + "__forms__": "folder-form", + "deprecated": "folder-deprecated", + ".deprecated": "folder-deprecated", + "_deprecated": "folder-deprecated", + "-deprecated": "folder-deprecated", + "__deprecated__": "folder-deprecated", + "scrap": "folder-scrap", + ".scrap": "folder-scrap", + "_scrap": "folder-scrap", + "-scrap": "folder-scrap", + "__scrap__": "folder-scrap", + "postman": "folder-postman", + ".postman": "folder-postman", + "_postman": "folder-postman", + "-postman": "folder-postman", + "__postman__": "folder-postman", + "skill": "folder-skills", + ".skill": "folder-skills", + "_skill": "folder-skills", + "-skill": "folder-skills", + "__skill__": "folder-skills", + "skills": "folder-skills", + ".skills": "folder-skills", + "_skills": "folder-skills", + "-skills": "folder-skills", + "__skills__": "folder-skills", + "instruction": "folder-instructions", + ".instruction": "folder-instructions", + "_instruction": "folder-instructions", + "-instruction": "folder-instructions", + "__instruction__": "folder-instructions", + "instructions": "folder-instructions", + ".instructions": "folder-instructions", + "_instructions": "folder-instructions", + "-instructions": "folder-instructions", + "__instructions__": "folder-instructions", + "appwrite": "folder-appwrite", + ".appwrite": "folder-appwrite", + "_appwrite": "folder-appwrite", + "-appwrite": "folder-appwrite", + "__appwrite__": "folder-appwrite", + "assembly": "folder-assembly", + ".assembly": "folder-assembly", + "_assembly": "folder-assembly", + "-assembly": "folder-assembly", + "__assembly__": "folder-assembly", + "asm": "folder-assembly", + ".asm": "folder-assembly", + "_asm": "folder-assembly", + "-asm": "folder-assembly", + "__asm__": "folder-assembly", + "go": "folder-go", + ".go": "folder-go", + "_go": "folder-go", + "-go": "folder-go", + "__go__": "folder-go", + "golang": "folder-go", + ".golang": "folder-go", + "_golang": "folder-go", + "-golang": "folder-go", + "__golang__": "folder-go", + "eas": "folder-eas", + ".eas": "folder-eas", + "_eas": "folder-eas", + "-eas": "folder-eas", + "__eas__": "folder-eas", + "kotlin": "folder-kotlin", + ".kotlin": "folder-kotlin", + "_kotlin": "folder-kotlin", + "-kotlin": "folder-kotlin", + "__kotlin__": "folder-kotlin" }, "folderNamesExpanded": { "rust": "folder-rust-open", @@ -5135,6 +5235,11 @@ "_github/workflows": "folder-gh-workflows-open", "-github/workflows": "folder-gh-workflows-open", "__github/workflows__": "folder-gh-workflows-open", + "gitea/workflows": "folder-gitea-workflows-open", + ".gitea/workflows": "folder-gitea-workflows-open", + "_gitea/workflows": "folder-gitea-workflows-open", + "-gitea/workflows": "folder-gitea-workflows-open", + "__gitea/workflows__": "folder-gitea-workflows-open", "git": "folder-git-open", ".git": "folder-git-open", "_git": "folder-git-open", @@ -5510,6 +5615,11 @@ "_vendors": "folder-lib-open", "-vendors": "folder-lib-open", "__vendors__": "folder-lib-open", + "thirdparty": "folder-lib-open", + ".thirdparty": "folder-lib-open", + "_thirdparty": "folder-lib-open", + "-thirdparty": "folder-lib-open", + "__thirdparty__": "folder-lib-open", "third-party": "folder-lib-open", ".third-party": "folder-lib-open", "_third-party": "folder-lib-open", @@ -5520,6 +5630,16 @@ "_lib64": "folder-lib-open", "-lib64": "folder-lib-open", "__lib64__": "folder-lib-open", + "external": "folder-lib-open", + ".external": "folder-lib-open", + "_external": "folder-lib-open", + "-external": "folder-lib-open", + "__external__": "folder-lib-open", + "externals": "folder-lib-open", + ".externals": "folder-lib-open", + "_externals": "folder-lib-open", + "-externals": "folder-lib-open", + "__externals__": "folder-lib-open", "themes": "folder-theme-open", ".themes": "folder-theme-open", "_themes": "folder-theme-open", @@ -5675,6 +5795,11 @@ "_dockerhub": "folder-docker-open", "-dockerhub": "folder-docker-open", "__dockerhub__": "folder-docker-open", + "nginx": "folder-nginx-open", + ".nginx": "folder-nginx-open", + "_nginx": "folder-nginx-open", + "-nginx": "folder-nginx-open", + "__nginx__": "folder-nginx-open", "astro": "folder-astro-open", ".astro": "folder-astro-open", "_astro": "folder-astro-open", @@ -6305,6 +6430,11 @@ "_pytest_cache": "folder-python-open", "-pytest_cache": "folder-python-open", "__pytest_cache__": "folder-python-open", + "r": "folder-r-open", + ".r": "folder-r-open", + "_r": "folder-r-open", + "-r": "folder-r-open", + "__r__": "folder-r-open", "sandbox": "folder-sandbox-open", ".sandbox": "folder-sandbox-open", "_sandbox": "folder-sandbox-open", @@ -9037,7 +9167,77 @@ ".forms": "folder-form-open", "_forms": "folder-form-open", "-forms": "folder-form-open", - "__forms__": "folder-form-open" + "__forms__": "folder-form-open", + "deprecated": "folder-deprecated-open", + ".deprecated": "folder-deprecated-open", + "_deprecated": "folder-deprecated-open", + "-deprecated": "folder-deprecated-open", + "__deprecated__": "folder-deprecated-open", + "scrap": "folder-scrap-open", + ".scrap": "folder-scrap-open", + "_scrap": "folder-scrap-open", + "-scrap": "folder-scrap-open", + "__scrap__": "folder-scrap-open", + "postman": "folder-postman-open", + ".postman": "folder-postman-open", + "_postman": "folder-postman-open", + "-postman": "folder-postman-open", + "__postman__": "folder-postman-open", + "skill": "folder-skills-open", + ".skill": "folder-skills-open", + "_skill": "folder-skills-open", + "-skill": "folder-skills-open", + "__skill__": "folder-skills-open", + "skills": "folder-skills-open", + ".skills": "folder-skills-open", + "_skills": "folder-skills-open", + "-skills": "folder-skills-open", + "__skills__": "folder-skills-open", + "instruction": "folder-instructions-open", + ".instruction": "folder-instructions-open", + "_instruction": "folder-instructions-open", + "-instruction": "folder-instructions-open", + "__instruction__": "folder-instructions-open", + "instructions": "folder-instructions-open", + ".instructions": "folder-instructions-open", + "_instructions": "folder-instructions-open", + "-instructions": "folder-instructions-open", + "__instructions__": "folder-instructions-open", + "appwrite": "folder-appwrite-open", + ".appwrite": "folder-appwrite-open", + "_appwrite": "folder-appwrite-open", + "-appwrite": "folder-appwrite-open", + "__appwrite__": "folder-appwrite-open", + "assembly": "folder-assembly-open", + ".assembly": "folder-assembly-open", + "_assembly": "folder-assembly-open", + "-assembly": "folder-assembly-open", + "__assembly__": "folder-assembly-open", + "asm": "folder-assembly-open", + ".asm": "folder-assembly-open", + "_asm": "folder-assembly-open", + "-asm": "folder-assembly-open", + "__asm__": "folder-assembly-open", + "go": "folder-go-open", + ".go": "folder-go-open", + "_go": "folder-go-open", + "-go": "folder-go-open", + "__go__": "folder-go-open", + "golang": "folder-go-open", + ".golang": "folder-go-open", + "_golang": "folder-go-open", + "-golang": "folder-go-open", + "__golang__": "folder-go-open", + "eas": "folder-eas-open", + ".eas": "folder-eas-open", + "_eas": "folder-eas-open", + "-eas": "folder-eas-open", + "__eas__": "folder-eas-open", + "kotlin": "folder-kotlin-open", + ".kotlin": "folder-kotlin-open", + "_kotlin": "folder-kotlin-open", + "-kotlin": "folder-kotlin-open", + "__kotlin__": "folder-kotlin-open" }, "rootFolderNames": {}, "rootFolderNamesExpanded": {}, @@ -9063,6 +9263,7 @@ "json5": "json", "jsonl": "json", "ndjson": "json", + "schema.json": "json_schema", "hjson": "hjson", "jinja": "jinja", "jinja2": "jinja", @@ -9261,6 +9462,7 @@ "vcxitems.filters": "visualstudio", "vcxproj": "visualstudio", "vcxproj.filters": "visualstudio", + "wixproj": "visualstudio", "vcl": "varnish", "pdb": "database", "sql": "database", @@ -9984,6 +10186,7 @@ "ast": "sas", "sast": "sas", "nupkg": "nuget", + "nuspec": "nuget", "command": "command", "dsc": "denizenscript", "code-search": "search", @@ -10206,6 +10409,10 @@ "lean": "lean", "sls": "salt", "m2": "macaulay2", + "skill.md": "skill", + "skills.md": "skill", + "instructions.md": "instructions", + "instruction.md": "instructions", "cljx": "clojure", "clojure": "clojure", "edn": "clojure", @@ -10363,7 +10570,6 @@ "jmx": "xml", "launch": "xml", "menu": "xml", - "nuspec": "xml", "opml": "xml", "owl": "xml", "proj": "xml", @@ -11094,7 +11300,18 @@ "rstest.config.ts": "rstack", "rstest.config.mts": "rstack", "rstest.config.cts": "rstack", + "rspress.config.js": "rstack", + "rspress.config.mjs": "rstack", + "rspress.config.cjs": "rstack", "rspress.config.ts": "rstack", + "rspress.config.mts": "rstack", + "rspress.config.cts": "rstack", + "rslint.config.js": "rstack", + "rslint.config.mjs": "rstack", + "rslint.config.cjs": "rstack", + "rslint.config.ts": "rstack", + "rslint.config.mts": "rstack", + "rslint.config.cts": "rstack", "rslint.json": "rstack", "rslint.jsonc": "rstack", "lynx.config.js": "lynx", @@ -11141,6 +11358,8 @@ ".env.dist": "tune", ".env.prod": "tune", ".env.production": "tune", + ".env.prod.example": "tune", + ".env.production.example": "tune", ".env.stg": "tune", ".env.stage": "tune", ".env.staging": "tune", @@ -11850,6 +12069,12 @@ "velite.config.ts": "velite", "velite.config.mts": "velite", "velite.config.cts": "velite", + "rolldown.config.js": "rolldown", + "rolldown.config.mjs": "rolldown", + "rolldown.config.cjs": "rolldown", + "rolldown.config.ts": "rolldown", + "rolldown.config.mts": "rolldown", + "rolldown.config.cts": "rolldown", "lerna.json": "lerna", "windi.config.js": "windicss", "windi.config.cjs": "windicss", @@ -12352,6 +12577,7 @@ "lefthookrc": "lefthook", ".github/labeler.yml": "label", ".github/labeler.yaml": "label", + "tags": "label", "zeabur.json": "zeabur", "zeabur.jsonc": "zeabur", "zeabur.json5": "zeabur", @@ -12427,6 +12653,7 @@ ".oxfmtrc.json": "oxc", ".oxfmtrc.jsonc": "oxc", "oxlint.config.ts": "oxc", + "oxfmt.config.ts": "oxc", "claude.md": "claude", "claude.local.md": "claude", ".cursorignore": "cursor", @@ -12450,6 +12677,14 @@ ".shellcheckrc": "shellcheck", "shellcheckrc": "shellcheck", "warp.md": "warp", + "skill.md": "skill", + "instructions.md": "instructions", + "instruction.md": "instructions", + "appwrite.json": "appwrite", + "appwrite.js": "appwrite", + "appwrite.ts": "appwrite", + "eas.json": "expo", + ".easignore": "expo", "language-configuration.json": "jsonc", "icon-theme.json": "jsonc", "color-theme.json": "jsonc", @@ -12653,7 +12888,9 @@ "helm": "helm", "nginx": "nginx", "cue": "cue", - "lean": "lean" + "lean": "lean", + "skill": "skill", + "instructions": "instructions" }, "light": { "fileExtensions": { @@ -12862,7 +13099,9 @@ "src/bashly-strings.yml": "bashly-strings_light", ".shellcheckrc": "shellcheck_light", "shellcheckrc": "shellcheck_light", - "warp.md": "warp_light" + "warp.md": "warp_light", + "eas.json": "expo_light", + ".easignore": "expo_light" }, "languageIds": { "toml": "toml_light", diff --git a/options/fileicon/material-icon-svgs.json b/options/fileicon/material-icon-svgs.json index 4e47681621..5c673ad3d2 100644 --- a/options/fileicon/material-icon-svgs.json +++ b/options/fileicon/material-icon-svgs.json @@ -31,10 +31,11 @@ "applescript": "", "apps-script": "", "appveyor": "", + "appwrite": "", "architecture": "", "arduino": "", "asciidoc": "", - "assembly": "", + "assembly": "", "astro-config": "", "astro": "", "astyle": "", @@ -186,6 +187,8 @@ "eslint": "", "excalidraw": "", "exe": "", + "expo": "", + "expo_light": "", "fastlane": "", "favicon": "", "figma": "", @@ -209,8 +212,12 @@ "folder-apollo": "", "folder-app-open": "", "folder-app": "", + "folder-appwrite-open": "", + "folder-appwrite": "", "folder-archive-open": "", "folder-archive": "", + "folder-assembly-open": "", + "folder-assembly": "", "folder-astro-open": "", "folder-astro": "", "folder-atom-open": "", @@ -319,6 +326,8 @@ "folder-decorators": "", "folder-delta-open": "", "folder-delta": "", + "folder-deprecated-open.clone": "", + "folder-deprecated.clone": "", "folder-desktop-open": "", "folder-desktop": "", "folder-development-open.clone": "", @@ -337,6 +346,8 @@ "folder-drizzle": "", "folder-dump-open": "", "folder-dump": "", + "folder-eas-open.clone": "", + "folder-eas.clone": "", "folder-element-open": "", "folder-element": "", "folder-enum-open": "", @@ -390,6 +401,8 @@ "folder-git-open": "", "folder-git": "", "folder-gitea-open": "", + "folder-gitea-workflows-open.clone": "", + "folder-gitea-workflows.clone": "", "folder-gitea": "", "folder-github-open": "", "folder-github": "", @@ -397,6 +410,8 @@ "folder-gitlab": "", "folder-global-open": "", "folder-global": "", + "folder-go-open": "", + "folder-go": "", "folder-godot-open": "", "folder-godot": "", "folder-gradle-open": "", @@ -427,6 +442,8 @@ "folder-include": "", "folder-input-open": "", "folder-input": "", + "folder-instructions-open.clone": "", + "folder-instructions.clone": "", "folder-intellij-open": "", "folder-intellij-open_light": "", "folder-intellij": "", @@ -453,6 +470,8 @@ "folder-jupyter": "", "folder-keys-open": "", "folder-keys": "", + "folder-kotlin-open": "", + "folder-kotlin": "", "folder-kubernetes-open": "", "folder-kubernetes": "", "folder-kusto-open": "", @@ -517,6 +536,8 @@ "folder-netlify": "", "folder-next-open": "", "folder-next": "", + "folder-nginx-open": "", + "folder-nginx": "", "folder-ngrx-actions-open.clone": "", "folder-ngrx-actions.clone": "", "folder-ngrx-effects-open.clone": "", @@ -560,6 +581,8 @@ "folder-plugin": "", "folder-policy-open": "", "folder-policy": "", + "folder-postman-open": "", + "folder-postman": "", "folder-powershell-open": "", "folder-powershell": "", "folder-prisma-open": "", @@ -582,6 +605,8 @@ "folder-quasar": "", "folder-queue-open": "", "folder-queue": "", + "folder-r-open": "", + "folder-r": "", "folder-react-components-open": "", "folder-react-components": "", "folder-redux-actions-open.clone": "", @@ -622,6 +647,8 @@ "folder-scala": "", "folder-scons-open": "", "folder-scons": "", + "folder-scrap-open.clone": "", + "folder-scrap.clone": "", "folder-scripts-open": "", "folder-scripts": "", "folder-secure-open": "", @@ -638,6 +665,8 @@ "folder-shared": "", "folder-simulations-open": "", "folder-simulations": "", + "folder-skills-open": "", + "folder-skills": "", "folder-snapcraft-open": "", "folder-snapcraft": "", "folder-snippet-open": "", @@ -808,6 +837,7 @@ "image": "", "imba": "", "installation": "", + "instructions.clone": "", "ionic": "", "istanbul": "", "jar": "", @@ -821,6 +851,7 @@ "jinja_light": "", "jsconfig": "", "json": "", + "json_schema": "", "jsr": "", "jsr_light": "", "julia": "", @@ -1024,6 +1055,7 @@ "robots": "", "rocket": "", "rojo": "", + "rolldown": "", "rollup": "", "rome": "", "routing": "", @@ -1060,6 +1092,7 @@ "simulink": "", "siyuan": "", "sketch": "", + "skill": "", "slim": "", "slint": "", "slug": "", diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index b8270b23e6..cf500fcff2 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -122,6 +122,7 @@ "unpin": "Unpin", "artifacts": "Artifacts", "expired": "Expired", + "artifact_expires_at": "Expires at %s", "confirm_delete_artifact": "Are you sure you want to delete the artifact '%s'?", "archived": "Archived", "concept_system_global": "Global", @@ -173,6 +174,8 @@ "search.org_kind": "Search orgs…", "search.team_kind": "Search teams…", "search.code_kind": "Search code…", + "search.code_empty": "Start a code search.", + "search.code_empty_description": "Enter a keyword to search across the code.", "search.code_search_unavailable": "Code search is currently not available. Please contact the site administrator.", "search.code_search_by_git_grep": "Current code search results are provided by \"git grep\". There might be better results if site administrator enables Repository Indexer.", "search.package_kind": "Search packages…", @@ -213,11 +216,15 @@ "editor.buttons.switch_to_legacy.tooltip": "Use the legacy editor instead", "editor.buttons.enable_monospace_font": "Enable monospace font", "editor.buttons.disable_monospace_font": "Disable monospace font", + "editor.code_editor.command_palette": "Command Palette", + "editor.code_editor.find": "Find", + "editor.code_editor.placeholder": "Enter file content here", "filter.string.asc": "A–Z", "filter.string.desc": "Z–A", "error.occurred": "An error occurred", "error.report_message": "If you believe that this is a Gitea bug, please search for issues on GitHub or open a new issue if necessary.", "error.not_found": "The target couldn't be found.", + "error.permission_denied": "Permission denied.", "error.network_error": "Network error", "startpage.app_desc": "A painless, self-hosted Git service", "startpage.install": "Easy to install", @@ -264,7 +271,7 @@ "install.lfs_path": "Git LFS Root Path", "install.lfs_path_helper": "Files tracked by Git LFS will be stored in this directory. Leave empty to disable.", "install.run_user": "Run As Username", - "install.run_user_helper": "The operating system username that Gitea runs as. Note that this user must have access to the repository root path.", + "install.run_user_helper": "The operating system username that Gitea runs as, it must have write access to the data paths. This value is auto-detected and cannot be changed here. To use a different user, restart Gitea under that account.", "install.domain": "Server Domain", "install.domain_helper": "Domain or host address for the server.", "install.ssh_port": "SSH Server Port", @@ -306,12 +313,10 @@ "install.admin_email": "Email Address", "install.install_btn_confirm": "Install Gitea", "install.test_git_failed": "Could not test 'git' command: %v", - "install.sqlite3_not_available": "This Gitea version does not support SQLite3. Please download the official binary version from %s (not the 'gobuild' version).", "install.invalid_db_setting": "The database settings are invalid: %v", "install.invalid_db_table": "The database table \"%s\" is invalid: %v", "install.invalid_repo_path": "The repository root path is invalid: %v", "install.invalid_app_data_path": "The app data path is invalid: %v", - "install.run_user_not_match": "The 'run as' username is not the current username: %s -> %s", "install.internal_token_failed": "Failed to generate internal token: %v", "install.secret_key_failed": "Failed to generate secret key: %v", "install.save_config_failed": "Failed to save configuration: %v", @@ -633,14 +638,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:", @@ -1049,6 +1048,7 @@ "repo.forks": "Forks", "repo.stars": "Stars", "repo.reactions_more": "and %d more", + "repo.reactions": "Reactions", "repo.unit_disabled": "The site administrator has disabled this repository section.", "repo.language_other": "Other", "repo.adopt_search": "Enter username to search for unadopted repositories… (leave blank to find all)", @@ -1069,8 +1069,8 @@ "repo.transfer.accept_desc": "Transfer to \"%s\"", "repo.transfer.reject": "Reject Transfer", "repo.transfer.reject_desc": "Cancel transfer to \"%s\"", - "repo.transfer.no_permission_to_accept": "You do not have permission to accept this transfer.", - "repo.transfer.no_permission_to_reject": "You do not have permission to reject this transfer.", + "repo.transfer.is_transferring": "Transferring…", + "repo.transfer.is_transferring_prompt": "The repository is being transferred to %s", "repo.desc.private": "Private", "repo.desc.public": "Public", "repo.desc.public_access": "Public Access", @@ -1224,7 +1224,7 @@ "repo.ambiguous_runes_description": "This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.", "repo.invisible_runes_line": "This line has invisible unicode characters", "repo.ambiguous_runes_line": "This line has ambiguous unicode characters", - "repo.ambiguous_character": "%[1]c [U+%04[1]X] can be confused with %[2]c [U+%04[2]X]", + "repo.ambiguous_character": "%[1]s can be confused with %[2]s", "repo.escape_control_characters": "Escape", "repo.unescape_control_characters": "Unescape", "repo.file_copy_permalink": "Copy Permalink", @@ -1317,7 +1317,7 @@ "repo.editor.upload_file_is_locked": "File \"%s\" is locked by %s.", "repo.editor.upload_files_to_dir": "Upload files to \"%s\"", "repo.editor.cannot_commit_to_protected_branch": "Cannot commit to protected branch \"%s\".", - "repo.editor.no_commit_to_branch": "Unable to commit directly to branch because:", + "repo.editor.no_commit_to_branch": "Not allowed to commit directly to branch because:", "repo.editor.user_no_push_to_branch": "User cannot push to branch", "repo.editor.require_signed_commit": "Branch requires a signed commit", "repo.editor.cherry_pick": "Cherry-pick %s onto:", @@ -1333,7 +1333,7 @@ "repo.commits.desc": "Browse source code change history.", "repo.commits.commits": "Commits", "repo.commits.no_commits": "No commits in common. \"%s\" and \"%s\" have entirely different histories.", - "repo.commits.nothing_to_compare": "These branches are equal.", + "repo.commits.nothing_to_compare": "There are no differences to show.", "repo.commits.search.tooltip": "You can prefix keywords with \"author:\", \"committer:\", \"after:\", or \"before:\", e.g. \"revert author:Alice before:2019-01-13\".", "repo.commits.search_branch": "This Branch", "repo.commits.search_all": "All Branches", @@ -1365,10 +1365,13 @@ "repo.projects.desc": "Manage issues and pulls in projects.", "repo.projects.description": "Description (optional)", "repo.projects.description_placeholder": "Description", + "repo.projects.empty": "No projects yet.", + "repo.projects.empty_description": "Create a project to coordinate issues and pull requests.", "repo.projects.create": "Create Project", "repo.projects.title": "Title", "repo.projects.new": "New Project", "repo.projects.new_subheader": "Coordinate, track, and update your work in one place, so projects stay transparent and on schedule.", + "repo.projects.no_results": "No projects match your search.", "repo.projects.create_success": "The project \"%s\" has been created.", "repo.projects.deletion": "Delete Project", "repo.projects.deletion_desc": "Deleting a project removes it from all related issues. Continue?", @@ -1393,6 +1396,7 @@ "repo.projects.column.delete": "Delete Column", "repo.projects.column.deletion_desc": "Deleting a project column moves all related issues to the default column. Continue?", "repo.projects.column.color": "Color", + "repo.projects.column": "Column", "repo.projects.open": "Open", "repo.projects.close": "Close", "repo.projects.column.assigned_to": "Assigned to", @@ -1410,11 +1414,12 @@ "repo.issues.new": "New Issue", "repo.issues.new.title_empty": "Title cannot be empty", "repo.issues.new.labels": "Labels", - "repo.issues.new.no_label": "No Label", + "repo.issues.new.no_labels": "No labels", "repo.issues.new.clear_labels": "Clear labels", "repo.issues.new.projects": "Projects", "repo.issues.new.clear_projects": "Clear projects", - "repo.issues.new.no_projects": "No project", + "repo.issues.new.no_projects": "No projects", + "repo.issues.new.no_column": "No column", "repo.issues.new.open_projects": "Open Projects", "repo.issues.new.closed_projects": "Closed Projects", "repo.issues.new.no_items": "No items", @@ -1540,6 +1545,7 @@ "repo.issues.context.edit": "Edit", "repo.issues.context.delete": "Delete", "repo.issues.no_content": "No description provided.", + "repo.issues.comment_no_content": "No comment provided.", "repo.issues.close": "Close Issue", "repo.issues.comment_pull_merged_at": "merged commit %[1]s into %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "manually merged commit %[1]s into %[2]s %[3]s", @@ -1789,8 +1795,9 @@ "repo.pulls.select_commit_hold_shift_for_range": "Select commit. Hold Shift and click to select a range.", "repo.pulls.review_only_possible_for_full_diff": "Review is only possible when viewing the full diff", "repo.pulls.filter_changes_by_commit": "Filter by commit", - "repo.pulls.nothing_to_compare": "These branches are equal. There is no need to create a pull request.", - "repo.pulls.nothing_to_compare_have_tag": "The selected branches/tags are equal.", + "repo.pulls.nothing_to_compare": "There are no differences to show. There is no need to create a pull request.", + "repo.pulls.no_common_history": "These branches do not share a common merge base. Select a different base or compare branch.", + "repo.pulls.nothing_to_compare_have_tag": "There are no differences to show between the selected branches or tags.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "These branches are equal. This PR will be empty.", "repo.pulls.has_pull_request": "A pull request between these branches already exists: %[2]s#%[3]d", "repo.pulls.create": "Create Pull Request", @@ -1824,6 +1831,7 @@ "repo.pulls.required_status_check_failed": "Some required checks were not successful.", "repo.pulls.required_status_check_missing": "Some required checks are missing.", "repo.pulls.required_status_check_administrator": "As an administrator, you may still merge this pull request.", + "repo.pulls.required_status_check_bypass_allowlist": "You are allowed to bypass branch protection rules for this merge.", "repo.pulls.blocked_by_approvals": "This pull request doesn't have enough required approvals yet. %d of %d official approvals granted.", "repo.pulls.blocked_by_approvals_whitelisted": "This pull request doesn't have enough required approvals yet. %d of %d approvals granted from users or teams on the allowlist.", "repo.pulls.blocked_by_rejection": "This pull request has changes requested by an official reviewer.", @@ -1856,6 +1864,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", @@ -1961,7 +1970,6 @@ "repo.signing.wont_sign.headsigned": "The merge will not be signed as the head commit is not signed.", "repo.signing.wont_sign.commitssigned": "The merge will not be signed as all the associated commits are not signed.", "repo.signing.wont_sign.approved": "The merge will not be signed as the PR is not approved.", - "repo.signing.wont_sign.not_signed_in": "You are not signed in.", "repo.ext_wiki": "Access to External Wiki", "repo.ext_wiki.desc": "Link to an external wiki.", "repo.wiki": "Wiki", @@ -2143,7 +2151,9 @@ "repo.settings.pulls_desc": "Enable Repository Pull Requests", "repo.settings.pulls.ignore_whitespace": "Ignore Whitespace for Conflicts", "repo.settings.pulls.enable_autodetect_manual_merge": "Enable autodetect manual merge (Note: In some special cases, misjudgments can occur)", + "repo.settings.pulls.allow_merge_update": "Enable updating pull request branch by merge", "repo.settings.pulls.allow_rebase_update": "Enable updating pull request branch by rebase", + "repo.settings.pulls.default_update_style": "Default branch update style", "repo.settings.pulls.default_target_branch": "Default target branch for new pull requests", "repo.settings.pulls.default_target_branch_default": "Default branch (%s)", "repo.settings.pulls.default_delete_branch_after_merge": "Delete pull request branch after merge by default", @@ -2262,13 +2272,14 @@ "repo.settings.webhook.delivery.success": "An event has been added to the delivery queue. It may take few seconds before it shows up in the delivery history.", "repo.settings.githooks_desc": "Git Hooks are powered by Git itself. You can edit hook files below to set up custom operations.", "repo.settings.githook_edit_desc": "If the hook is inactive, sample content will be presented. Leaving content to an empty value will disable this hook.", - "repo.settings.githook_name": "Hook Name", - "repo.settings.githook_content": "Hook Content", "repo.settings.update_githook": "Update Hook", "repo.settings.add_webhook_desc": "Gitea will send POST requests with a specified content type to the target URL. Read more in the webhooks guide.", "repo.settings.payload_url": "Target URL", "repo.settings.http_method": "HTTP Method", "repo.settings.content_type": "POST Content Type", + "repo.settings.webhook.name": "Webhook name", + "repo.settings.webhook.name_helper": "Optionally give this webhook a friendly name", + "repo.settings.webhook.name_empty": "Unnamed Webhook", "repo.settings.secret": "Secret", "repo.settings.webhook_secret_desc": "If the webhook server supports using secret, you can follow the webhook's manual and fill in a secret here.", "repo.settings.slack_username": "Username", @@ -2417,6 +2428,11 @@ "repo.settings.protect_merge_whitelist_committers_desc": "Allow only allowlisted users or teams to merge pull requests into this branch.", "repo.settings.protect_merge_whitelist_users": "Allowlisted users for merging:", "repo.settings.protect_merge_whitelist_teams": "Allowlisted teams for merging:", + "repo.settings.protect_bypass_allowlist": "Bypass branch protection", + "repo.settings.protect_enable_bypass_allowlist": "Allow selected users or teams to bypass branch protection", + "repo.settings.protect_enable_bypass_allowlist_desc": "Allowlisted users or teams can merge or push even when required approvals, status checks, or protected-file rules would otherwise block them.", + "repo.settings.protect_bypass_allowlist_users": "Allowlisted users for bypassing protection:", + "repo.settings.protect_bypass_allowlist_teams": "Allowlisted teams for bypassing protection:", "repo.settings.protect_check_status_contexts": "Enable Status Check", "repo.settings.protect_status_check_patterns": "Status check patterns:", "repo.settings.protect_status_check_patterns_desc": "Enter patterns to specify which status checks must pass before branches can be merged into a branch that matches this rule. Each line specifies a pattern. Patterns cannot be empty.", @@ -2458,7 +2474,7 @@ "repo.settings.block_outdated_branch": "Block merge if pull request is outdated", "repo.settings.block_outdated_branch_desc": "Merging will not be possible when head branch is behind base branch.", "repo.settings.block_admin_merge_override": "Administrators must follow branch protection rules", - "repo.settings.block_admin_merge_override_desc": "Administrators must follow branch protection rules and cannot circumvent it.", + "repo.settings.block_admin_merge_override_desc": "Administrators must follow branch protection rules and cannot circumvent it. Users or teams in the bypass allowlist can still bypass these rules if bypass allowlist is enabled.", "repo.settings.default_branch_desc": "Select a default branch for code commits.", "repo.settings.default_target_branch_desc": "Pull requests can use different default target branch if it is set in the Pull Requests section of Repository Advance Settings.", "repo.settings.merge_style_desc": "Merge Styles", @@ -2728,6 +2744,8 @@ "org.members": "Members", "org.teams": "Teams", "org.code": "Code", + "org.repos.empty": "No repositories yet.", + "org.repos.empty_description": "Create a repository to share code with the organization.", "org.lower_members": "members", "org.lower_repositories": "repositories", "org.create_new_team": "New Team", @@ -2791,9 +2809,9 @@ "org.settings.labels_desc": "Add labels which can be used on issues for all repositories under this organization.", "org.members.membership_visibility": "Membership Visibility:", "org.members.public": "Visible", - "org.members.public_helper": "make hidden", + "org.members.public_helper": "Make hidden", "org.members.private": "Hidden", - "org.members.private_helper": "make visible", + "org.members.private_helper": "Make visible", "org.members.member_role": "Member Role:", "org.members.owner": "Owner", "org.members.member": "Member", @@ -2821,10 +2839,13 @@ "org.teams.no_desc": "This team has no description", "org.teams.settings": "Settings", "org.teams.owners_permission_desc": "Owners have full access to all repositories and have administrator access to the organization.", + "org.teams.owners_permission_suggestion": "You can create new teams for members to get fine-grained access control.", "org.teams.members": "Team Members", + "org.teams.manage_team_member": "Manage teams and members", + "org.teams.manage_team_member_prompt": "Members are managed through teams. Add users to a team to invite them to this organization.", "org.teams.update_settings": "Update Settings", "org.teams.delete_team": "Delete Team", - "org.teams.add_team_member": "Add Team Member", + "org.teams.add_team_member": "Add team member", "org.teams.invite_team_member": "Invite to %s", "org.teams.invite_team_member.list": "Pending Invitations", "org.teams.delete_team_title": "Delete Team", @@ -2862,6 +2883,8 @@ "org.worktime.date_range_end": "End date", "org.worktime.query": "Query", "org.worktime.time": "Time", + "org.worktime.empty": "No worktime data yet.", + "org.worktime.empty_description": "Adjust the date range to view tracked time.", "org.worktime.by_repositories": "By repositories", "org.worktime.by_milestones": "By milestones", "org.worktime.by_members": "By members", @@ -3176,6 +3199,8 @@ "admin.auths.oauth2_required_claim_name_helper": "Set this name to restrict login from this source to users with a claim with this name", "admin.auths.oauth2_required_claim_value": "Required Claim Value", "admin.auths.oauth2_required_claim_value_helper": "Set this value to restrict login from this source to users with a claim with this name and value", + "admin.auths.open_id_connect_external_id_claim": "External ID Claim Name (Optional)", + "admin.auths.open_id_connect_external_id_claim_helper": "Claim name to use as the user's external identity. Defaults to \"sub\". For Azure AD / Entra ID, set this to \"oid\" to maintain continuity when migrating from the Azure AD V2 provider. Note: the \"oid\" claim requires the \"profile\" scope to be included in the Scopes field above.", "admin.auths.oauth2_group_claim_name": "Claim name providing group names for this source. (Optional)", "admin.auths.oauth2_full_name_claim_name": "Full Name Claim Name. (Optional — if set, the user's full name will always be synchronized with this claim)", "admin.auths.oauth2_ssh_public_key_claim_name": "SSH Public Key Claim Name", @@ -3228,10 +3253,8 @@ "admin.config.server_config": "Server Configuration", "admin.config.app_name": "Site Title", "admin.config.app_ver": "Gitea Version", - "admin.config.app_url": "Gitea Base URL", "admin.config.custom_conf": "Configuration File Path", "admin.config.custom_file_root_path": "Custom File Root Path", - "admin.config.domain": "Server Domain", "admin.config.disable_router_log": "Disable Router Log", "admin.config.run_user": "Run As Username", "admin.config.run_mode": "Run Mode", @@ -3311,7 +3334,6 @@ "admin.config.cache_config": "Cache Configuration", "admin.config.cache_adapter": "Cache Adapter", "admin.config.cache_interval": "Cache Interval", - "admin.config.cache_conn": "Cache Connection", "admin.config.cache_item_ttl": "Cache Item TTL", "admin.config.cache_test": "Test Cache", "admin.config.cache_test_failed": "Failed to probe the cache: %v.", @@ -3326,7 +3348,6 @@ "admin.config.instance_web_banner.message_placeholder": "Banner message (supports markdown)", "admin.config.session_config": "Session Configuration", "admin.config.session_provider": "Session Provider", - "admin.config.provider_config": "Provider Config", "admin.config.cookie_name": "Cookie Name", "admin.config.gc_interval_time": "GC Interval Time", "admin.config.session_life_time": "Session Life Time", @@ -3512,6 +3533,7 @@ "packages.dependencies": "Dependencies", "packages.keywords": "Keywords", "packages.details": "Details", + "packages.name": "Package Name", "packages.details.author": "Author", "packages.details.project_site": "Project Site", "packages.details.repository_site": "Repository Site", @@ -3607,9 +3629,27 @@ "packages.swift.registry": "Set up this registry from the command line:", "packages.swift.install": "Add the package in your Package.swift file:", "packages.swift.install2": "and run the following command:", + "packages.terraform.install": "Set your state to use the HTTP backend", + "packages.terraform.install2": "and run the following command:", + "packages.terraform.lock_status": "Lock Status", + "packages.terraform.locked_by": "Locked by %s", + "packages.terraform.unlocked": "Unlocked", + "packages.terraform.lock": "Lock", + "packages.terraform.unlock": "Unlock", + "packages.terraform.lock.success": "Terraform state was successfully locked.", + "packages.terraform.unlock.success": "Terraform state was successfully unlocked.", + "packages.terraform.lock.error.already_locked": "Terraform state is already locked.", + "packages.terraform.delete.locked": "Terraform state is locked and cannot be deleted.", + "packages.terraform.delete.latest": "The latest version of a Terraform state cannot be deleted.", "packages.vagrant.install": "To add a Vagrant box, run the following command:", "packages.settings.link": "Link this package to a repository", - "packages.settings.link.description": "If you link a package with a repository, the package will appear in the repository's package list. Only repositories under the same owner can be linked. Leaving the field empty will remove the link.", + "packages.settings.link.description": "If you link a package with a repository, the package will appear in the repository's package list.", + "packages.settings.link.notice1": "Only repositories under the same owner can be linked.", + "packages.settings.link.notice2": "Linking a repository does not change the package visibility.", + "packages.settings.link.notice3": "Leaving the field empty will remove the link.", + "packages.settings.visibility": "Package visibility", + "packages.settings.visibility.inherit": "Package visibility is inherited from the owner and cannot be changed independently here. To change it, update the visibility settings of the user or organization that owns this package.", + "packages.settings.visibility.button": "Change owner visibility", "packages.settings.link.select": "Select Repository", "packages.settings.link.button": "Update Repository Link", "packages.settings.link.success": "Repository link was successfully updated.", @@ -3620,8 +3660,13 @@ "packages.settings.delete": "Delete package", "packages.settings.delete.description": "Deleting a package is permanent and cannot be undone.", "packages.settings.delete.notice": "You are about to delete %s (%s). This operation is irreversible, are you sure?", + "packages.settings.delete.notice.package": "You are about to delete %s and all its versions. This operation is irreversible, are you sure?", "packages.settings.delete.success": "The package has been deleted.", + "packages.settings.delete.version.success": "The package version has been deleted.", "packages.settings.delete.error": "Failed to delete the package.", + "packages.settings.delete.version": "Delete version", + "packages.settings.delete.confirm": "Enter package name to confirm", + "packages.settings.delete.invalid_package_name": "The package name you entered is incorrect.", "packages.owner.settings.cargo.title": "Cargo Registry Index", "packages.owner.settings.cargo.initialize": "Initialize Index", "packages.owner.settings.cargo.initialize.description": "A special index Git repository is needed to use the Cargo registry. Using this option will (re-)create the repository and configure it automatically.", @@ -3728,6 +3773,8 @@ "actions.runs.workflow_run_count_1": "%d workflow run", "actions.runs.workflow_run_count_n": "%d workflow runs", "actions.runs.commit": "Commit", + "actions.runs.run_details": "Run Details", + "actions.runs.workflow_file": "Workflow file", "actions.runs.scheduled": "Scheduled", "actions.runs.pushed_by": "pushed by", "actions.runs.invalid_workflow_helper": "Workflow config file is invalid. Please check your config file: %s", @@ -3750,9 +3797,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_fr-FR.json b/options/locale/locale_fr-FR.json index e0d6cb541e..aa295c98a2 100644 --- a/options/locale/locale_fr-FR.json +++ b/options/locale/locale_fr-FR.json @@ -81,6 +81,7 @@ "retry": "Réessayez", "rerun": "Relancer", "rerun_all": "Relancer toutes les tâches", + "rerun_failed": "Relancer les tâches échouées", "save": "Enregistrer", "add": "Ajouter", "add_all": "Tout Ajouter", @@ -121,6 +122,7 @@ "unpin": "Désépingler", "artifacts": "Artefacts", "expired": "Expiré", + "artifact_expires_at": "Expire le %s", "confirm_delete_artifact": "Êtes-vous sûr de vouloir supprimer l’artefact « %s » ?", "archived": "Archivé", "concept_system_global": "Global", @@ -168,9 +170,12 @@ "search.exact_tooltip": "Inclure uniquement les résultats qui correspondent exactement au terme de recherche", "search.repo_kind": "Chercher des dépôts…", "search.user_kind": "Chercher des utilisateurs…", + "search.badge_kind": "Chercher des badges…", "search.org_kind": "Chercher des organisations…", "search.team_kind": "Chercher des équipes…", "search.code_kind": "Chercher du code…", + "search.code_empty": "Lancer une recherche de code.", + "search.code_empty_description": "Entrer un mot clé pour rechercher dans le code.", "search.code_search_unavailable": "La recherche dans le code n’est pas disponible actuellement. Veuillez contacter l’administrateur de votre instance Gitea.", "search.code_search_by_git_grep": "Les résultats de recherche de code actuels sont fournis par « git grep ». L’administrateur peut activer l’indexeur de dépôt, qui pourrait fournir de meilleurs résultats.", "search.package_kind": "Chercher des paquets…", @@ -211,11 +216,15 @@ "editor.buttons.switch_to_legacy.tooltip": "Utiliser l’ancien éditeur à la place", "editor.buttons.enable_monospace_font": "Activer la police à chasse fixe", "editor.buttons.disable_monospace_font": "Désactiver la police à chasse fixe", + "editor.code_editor.command_palette": "Palette de commandes", + "editor.code_editor.find": "Rechercher", + "editor.code_editor.placeholder": "Saisissez ici le contenu du fichier", "filter.string.asc": "A–Z", "filter.string.desc": "Z–A", "error.occurred": "Une erreur s’est produite", "error.report_message": "Si vous pensez qu’il s’agit d’un bug Gitea, veuillez consulter notre board GitHub ou ouvrir un nouveau ticket si nécessaire.", "error.not_found": "La cible n'a pu être trouvée.", + "error.permission_denied": "Autorisation refusée.", "error.network_error": "Erreur réseau", "startpage.app_desc": "Un service Git auto-hébergé sans prise de tête", "startpage.install": "Facile à installer", @@ -262,7 +271,7 @@ "install.lfs_path": "Répertoire racine Git LFS", "install.lfs_path_helper": "Les fichiers suivis par Git LFS seront stockés dans ce dossier. Laissez vide pour désactiver LFS.", "install.run_user": "Exécuter avec le compte d'un autre utilisateur", - "install.run_user_helper": "Le nom d'utilisateur du système d'exploitation sous lequel Gitea fonctionne. Notez que cet utilisateur doit avoir accès au dossier racine du dépôt.", + "install.run_user_helper": "L’utilisateur système exécutant Gitea, devant avoir la permission d’écriture dans le répertoire data. Ce nom est automatiquement détecté et ne peux être modifié. Pour changer d’utilisateur, redémarrez Gitea avec son compte respectif.", "install.domain": "Domaine du serveur", "install.domain_helper": "Domaine ou adresse d'hôte pour le serveur.", "install.ssh_port": "Port du serveur SSH", @@ -309,7 +318,6 @@ "install.invalid_db_table": "La table \"%s\" de la base de données est invalide : %v", "install.invalid_repo_path": "Le chemin racine du dépôt est invalide : %v", "install.invalid_app_data_path": "Le chemin des données de l'application est invalide : %v", - "install.run_user_not_match": "Le nom d'utilisateur sous lequel Gitea est configuré n'est pas le nom d'utilisateur actuel: %s -> %s", "install.internal_token_failed": "Impossible de générer le jeton interne : %v", "install.secret_key_failed": "Impossible de générer la clé secrète : %v", "install.save_config_failed": "L'enregistrement de la configuration %v a échoué", @@ -542,6 +550,7 @@ "form.glob_pattern_error": " a un motif glob invalide : %s.", "form.regex_pattern_error": " a un motif regex invalide : %s.", "form.username_error": " ne peut contenir que des caractères alphanumériques « a-z, A-Z, 0-9 », des traits d'union « - », des tirets bas « _ » et des points « . » et ne peux ni commencer, ni finir par des symboles, ni contenir des symboles consécutifs.", + "form.invalid_slug_error": " n’est pas valide.", "form.invalid_group_team_map_error": " a une cartographie invalide : %s", "form.unknown_error": "Erreur inconnue :", "form.captcha_incorrect": "Le code CAPTCHA est incorrect.", @@ -630,14 +639,8 @@ "user.block.unblock.failure": "Impossible de débloquer l’utilisateur : %s", "user.block.blocked": "Vous avez bloqué cet utilisateur.", "user.block.title": "Bloquer un utilisateur", - "user.block.info": "Bloquer un utilisateur l’empêche d’interagir avec des dépôts, comme ouvrir ou commenter des demandes de fusion ou des tickets. Apprenez-en plus sur le blocage d’un utilisateur.", - "user.block.info_1": "Bloquer un utilisateur empêche les actions suivantes sur votre compte et vos dépôts :", - "user.block.info_2": "suivre votre compte", - "user.block.info_3": "vous envoyer des notifications en vous @mentionnant", - "user.block.info_4": "vous inviter en tant que collaborateur de son(ses) dépôt(s)", - "user.block.info_5": "aimer, bifurquer ou suivre vos dépôts", - "user.block.info_6": "ouvrir ou commenter vos tickets et demandes d’ajouts", - "user.block.info_7": "réagir à vos commentaires dans les tickets ou les demandes d’ajout", + "user.block.info": "Bloquer un utilisateur l’empêche d’interagir avec des dépôts, comme ouvrir ou commenter des demandes d’ajouts ou des tickets.", + "user.block.info.docs": "En savoir plus sur le blocage d’un utilisateur.", "user.block.user_to_block": "Utilisateur à bloquer", "user.block.note": "Note", "user.block.note.title": "Note facultative :", @@ -645,6 +648,7 @@ "user.block.note.edit": "Modifier la note", "user.block.list": "Utilisateurs bloqués", "user.block.list.none": "Vous n’avez bloqué aucun utilisateur.", + "settings.general": "Général", "settings.profile": "Profil", "settings.account": "Compte", "settings.appearance": "Apparence", @@ -965,7 +969,6 @@ "repo.visibility_description": "Seuls le propriétaire ou les membres de l'organisation, s'ils ont des droits, seront en mesure de le voir.", "repo.visibility_helper": "Rendre le dépôt privé", "repo.visibility_helper_forced": "L’administrateur requière que les nouveaux dépôts soient privés.", - "repo.visibility_fork_helper": "(Changer ceci affectera toutes les bifurcations.)", "repo.clone_helper": "Besoin d'aide pour dupliquer ? Visitez l'aide.", "repo.fork_repo": "Bifurquer le dépôt", "repo.fork_from": "Bifurquer depuis", @@ -1037,6 +1040,7 @@ "repo.forks": "Bifurcations", "repo.stars": "Favoris", "repo.reactions_more": "et %d de plus", + "repo.reactions": "Réactions", "repo.unit_disabled": "L'administrateur du site a désactivé cette section du dépôt.", "repo.language_other": "Autre", "repo.adopt_search": "Entrez un nom d’utilisateur pour rechercher les dépôts dépossédés… (laissez vide pour tous trouver)", @@ -1057,8 +1061,8 @@ "repo.transfer.accept_desc": "Transférer à « %s »", "repo.transfer.reject": "Refuser le transfert", "repo.transfer.reject_desc": "Annuler le transfert à « %s »", - "repo.transfer.no_permission_to_accept": "Vous n’êtes pas autorisé à accepter ce transfert.", - "repo.transfer.no_permission_to_reject": "Vous n’êtes pas autorisé à rejeter ce transfert.", + "repo.transfer.is_transferring": "Transfert en cours…", + "repo.transfer.is_transferring_prompt": "Le dépôt est en cours de transfert vers %s", "repo.desc.private": "Privé", "repo.desc.public": "Publique", "repo.desc.public_access": "Accès public", @@ -1209,7 +1213,7 @@ "repo.ambiguous_runes_description": "Ce fichier contient des caractères Unicode qui peuvent être confondus avec d'autres caractères. Si vous pensez que c'est intentionnel, vous pouvez ignorer cet avertissement. Utilisez le bouton Échappe pour les dévoiler.", "repo.invisible_runes_line": "Cette ligne contient des caractères Unicode invisibles.", "repo.ambiguous_runes_line": "Cette ligne contient des caractères Unicode ambigus.", - "repo.ambiguous_character": "%[1]c [U+%04[1]X] peut être confondu avec %[2]c [U+%04[2]X].", + "repo.ambiguous_character": "%[1]s peut être confondu avec %[2]s.", "repo.escape_control_characters": "Échapper", "repo.unescape_control_characters": "Annuler l'échappement", "repo.file_copy_permalink": "Copier le lien permanent", @@ -1350,10 +1354,13 @@ "repo.projects.desc": "Gérer les tickets et les demandes d’ajouts dans les projets.", "repo.projects.description": "Description (facultative)", "repo.projects.description_placeholder": "Description", + "repo.projects.empty": "Aucun projet pour le moment.", + "repo.projects.empty_description": "Créer un projet pour coordonner les tickets et les demandes d’ajout.", "repo.projects.create": "Créer un projet", "repo.projects.title": "Titre", "repo.projects.new": "Nouveau projet", "repo.projects.new_subheader": "Coordonnez, surveillez, et mettez à jour votre travail en un seul endroit, afin que les projets restent transparents et dans les délais.", + "repo.projects.no_results": "Aucun projet ne correspond à votre recherche.", "repo.projects.create_success": "Le projet \"%s\" a été créé.", "repo.projects.deletion": "Supprimer le projet", "repo.projects.deletion_desc": "Supprimer un projet efface également de tous les tickets liés. Voulez vous continuer?", @@ -1378,6 +1385,7 @@ "repo.projects.column.delete": "Supprimer la colonne", "repo.projects.column.deletion_desc": "La suppression d’une colonne déplace tous ses tickets dans la colonne par défaut. Continuer ?", "repo.projects.column.color": "Couleur", + "repo.projects.column": "Colonne", "repo.projects.open": "Ouvrir", "repo.projects.close": "Fermer", "repo.projects.column.assigned_to": "Assigné à", @@ -1395,11 +1403,12 @@ "repo.issues.new": "Nouveau ticket", "repo.issues.new.title_empty": "Le titre ne peut pas être vide", "repo.issues.new.labels": "Labels", - "repo.issues.new.no_label": "Sans labels", + "repo.issues.new.no_labels": "Pas d’étiquette", "repo.issues.new.clear_labels": "Effacer les labels", "repo.issues.new.projects": "Projets", "repo.issues.new.clear_projects": "Effacer les projets", "repo.issues.new.no_projects": "Sans projet", + "repo.issues.new.no_column": "Pas de colonne", "repo.issues.new.open_projects": "Projets ouverts", "repo.issues.new.closed_projects": "Projets clôturés", "repo.issues.new.no_items": "Pas d'élément", @@ -1525,6 +1534,7 @@ "repo.issues.context.edit": "Éditer", "repo.issues.context.delete": "Supprimer", "repo.issues.no_content": "Sans contenu.", + "repo.issues.comment_no_content": "Aucun commentaire fourni.", "repo.issues.close": "Fermer le ticket", "repo.issues.comment_pull_merged_at": "a fusionné la révision %[1]s dans %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "a fusionné manuellement la révision %[1]s dans %[2]s %[3]s", @@ -1775,6 +1785,7 @@ "repo.pulls.review_only_possible_for_full_diff": "Une évaluation n'est possible que lorsque vous affichez le différentiel complet.", "repo.pulls.filter_changes_by_commit": "Filtrer par révision", "repo.pulls.nothing_to_compare": "Ces branches sont identiques. Il n’y a pas besoin de créer une demande d'ajout.", + "repo.pulls.no_common_history": "Ces branches ne partagent pas de base de fusion commune. Sélectionnez une autre base ou branche de comparaison.", "repo.pulls.nothing_to_compare_have_tag": "Les branches/étiquettes sélectionnées sont équivalentes.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Ces branches sont égales. Cette demande d'ajout sera vide.", "repo.pulls.has_pull_request": "'Il existe déjà une demande d'ajout entre ces deux branches : %[2]s#%[3]d'", @@ -1841,6 +1852,7 @@ "repo.pulls.merge_manually": "Fusionner manuellement", "repo.pulls.merge_commit_id": "L'ID de la révision de fusion", "repo.pulls.require_signed_wont_sign": "La branche nécessite des révisions signées mais cette fusion ne sera pas signée", + "repo.pulls.require_signed_head_commits_unverified": "La branche nécessite des révisions signées, mais une ou plusieurs révisions de cette demande de fusion ne sont pas vérifiées", "repo.pulls.invalid_merge_option": "Vous ne pouvez pas utiliser cette option de fusion pour cette demande.", "repo.pulls.merge_conflict": "Échec de la fusion : il y a eu un conflit lors de la fusion. Conseil : Essayez une stratégie différente.", "repo.pulls.merge_conflict_summary": "Message d'erreur", @@ -1946,7 +1958,6 @@ "repo.signing.wont_sign.headsigned": "La fusion ne sera pas signée car la dernière révision n’est pas signée.", "repo.signing.wont_sign.commitssigned": "La fusion ne sera pas signée car ses révisions ne sont pas signées.", "repo.signing.wont_sign.approved": "La fusion ne sera pas signée car la demande d'ajout n'a pas été approuvée.", - "repo.signing.wont_sign.not_signed_in": "Vous n'êtes pas connecté.", "repo.ext_wiki": "Accès au wiki externe", "repo.ext_wiki.desc": "Lier un wiki externe.", "repo.wiki": "Wiki", @@ -2170,7 +2181,8 @@ "repo.settings.transfer_abort_invalid": "Vous ne pouvez pas annuler un transfert de dépôt inexistant.", "repo.settings.transfer_abort_success": "Le transfert du dépôt vers %s a bien été stoppé.", "repo.settings.transfer_desc": "Transférer ce dépôt à un autre utilisateur ou une organisation dont vous possédez des droits d'administrateur.", - "repo.settings.transfer_form_title": "Entrez le nom du dépôt pour confirmer :", + "repo.settings.enter_repo_name_to_confirm": "Entrez le nom du dépôt pour confirmer :", + "repo.settings.enter_repo_full_name_to_confirm": "Entrez le nom complet du dépôt (propriétaire/nom) pour confirmer :", "repo.settings.transfer_in_progress": "Il y a actuellement un transfert en cours. Veuillez l’annuler si vous souhaitez transférer ce dépôt à un autre utilisateur.", "repo.settings.transfer_notices_1": "- Vous perdrez l'accès à ce dépôt si vous le transférez à un autre utilisateur.", "repo.settings.transfer_notices_2": "- Vous conserverez l'accès à ce dépôt si vous le transférez à une organisation dont vous êtes (co-)propriétaire.", @@ -2246,13 +2258,14 @@ "repo.settings.webhook.delivery.success": "Un événement a été ajouté à la file d'attente. Cela peut prendre quelques secondes avant qu'il n'apparaisse dans l'historique de livraison.", "repo.settings.githooks_desc": "Les déclencheurs Git sont lancés par Git lui-même. Ils sont modifiables dans la liste ci-dessous afin de configurer des opérations personnalisées.", "repo.settings.githook_edit_desc": "Si un Hook est inactif, un exemple de contenu vous sera proposé. Un contenu laissé vide signifie un Hook inactif.", - "repo.settings.githook_name": "Nom du Hook", - "repo.settings.githook_content": "Contenu du Hook", "repo.settings.update_githook": "Mettre le Hook à jour", "repo.settings.add_webhook_desc": "Gitea enverra à l'URL cible des requêtes POST avec un type de contenu spécifié. Lire la suite dans le guide des webhooks.", "repo.settings.payload_url": "URL cible", "repo.settings.http_method": "Méthode HTTP", "repo.settings.content_type": "Type de contenu POST", + "repo.settings.webhook.name": "Nom du déclencheur web", + "repo.settings.webhook.name_helper": "Optionnellement donner un nom convivial à ce déclencheur web", + "repo.settings.webhook.name_empty": "Déclencheur web sans nom", "repo.settings.secret": "Secret", "repo.settings.webhook_secret_desc": "Si le serveur webhook supporte l’usage de secrets, vous pouvez indiquer un secret ici en vous basant sur leur documentation.", "repo.settings.slack_username": "Nom d'utilisateur", @@ -2316,7 +2329,7 @@ "repo.settings.event_workflow_run": "Exécution du flux de travail", "repo.settings.event_workflow_run_desc": "Tâche du flux de travail Gitea Actions ajoutée, en attente, en cours ou terminée.", "repo.settings.event_workflow_job": "Tâches du flux de travail", - "repo.settings.event_workflow_job_desc": "Travaux du flux de travail Gitea Actions en file d’attente, en attente, en cours ou terminée.", + "repo.settings.event_workflow_job_desc": "Tâches du flux de travail Gitea Actions en file d’attente, en attente, en cours ou terminée.", "repo.settings.event_package": "Paquet", "repo.settings.event_package_desc": "Paquet créé ou supprimé.", "repo.settings.branch_filter": "Filtre de branche", @@ -2473,7 +2486,10 @@ "repo.settings.visibility.private.text": "Rendre le dépôt privé rendra non seulement le dépôt visible uniquement aux membres autorisés, mais peut également rompre la relation entre lui et ses bifurcations, observateurs, et favoris.", "repo.settings.visibility.private.bullet_title": "Changer la visibilité en privé :", "repo.settings.visibility.private.bullet_one": "Rendra le dépôt visible uniquement aux membres autorisés.", - "repo.settings.visibility.private.bullet_two": "Peut supprimer la relation avec ses bifurcations, ses observateurs et ses favoris.", + "repo.settings.visibility.private.bullet_two": "Applique la visibilité aux bifurcation et retire les observateurs et les favoris.", + "repo.settings.visibility.private.stats_stars": "Il y a %d favori(s) sur ce dépôt qui pourrai(en)t être perdu(s).", + "repo.settings.visibility.private.stats_watchers": "Il y a %d observateur(s) sur ce dépôt qui pourrai(en)t être perdu(s).", + "repo.settings.visibility.private.stats_forks": "Il y a %d bifurcation(s) associée(s) à ce dépôt.", "repo.settings.visibility.public.button": "Rendre public", "repo.settings.visibility.public.text": "Rendre le dépôt public rendra le dépôt visible à tout le monde.", "repo.settings.visibility.public.bullet_title": "Changer la visibilité en public va :", @@ -2709,6 +2725,8 @@ "org.members": "Membres", "org.teams": "Équipes", "org.code": "Code", + "org.repos.empty": "Aucun dépôt pour le moment.", + "org.repos.empty_description": "Créez un dépôt pour partager du code avec cette organisation.", "org.lower_members": "Membres", "org.lower_repositories": "dépôts", "org.create_new_team": "Nouvelle équipe", @@ -2771,9 +2789,9 @@ "org.settings.labels_desc": "Ajoute des labels qui peuvent être utilisés sur les tickets pour tous les dépôts de cette organisation.", "org.members.membership_visibility": "Visibilité des membres:", "org.members.public": "Public", - "org.members.public_helper": "rendre caché", + "org.members.public_helper": "Cacher", "org.members.private": "Caché", - "org.members.private_helper": "rendre visible", + "org.members.private_helper": "Révéler", "org.members.member_role": "Rôle du membre :", "org.members.owner": "Propriétaire", "org.members.member": "Membre", @@ -2801,10 +2819,13 @@ "org.teams.no_desc": "Aucune description", "org.teams.settings": "Paramètres", "org.teams.owners_permission_desc": "Les propriétaires ont un accès complet à tous les dépôts et disposent d'un accès administrateur de l'organisation.", + "org.teams.owners_permission_suggestion": "Vous pouvez créer de nouvelles équipes pour les membres afin d’avoir un contrôle précis sur les droits d’accès.", "org.teams.members": "Membres de L'Équipe", + "org.teams.manage_team_member": "Gérer les équipes et les membres", + "org.teams.manage_team_member_prompt": "Les membres sont gérés par des équipes. Ajoutez des utilisateurs à une équipe pour les inviter dans cette organisation.", "org.teams.update_settings": "Appliquer les paramètres", "org.teams.delete_team": "Supprimer l'équipe", - "org.teams.add_team_member": "Ajouter un Membre", + "org.teams.add_team_member": "Ajouter un membre", "org.teams.invite_team_member": "Inviter à %s", "org.teams.invite_team_member.list": "Invitations en attente", "org.teams.delete_team_title": "Supprimer l'équipe", @@ -2842,6 +2863,8 @@ "org.worktime.date_range_end": "Date de fin", "org.worktime.query": "Demande", "org.worktime.time": "Durée", + "org.worktime.empty": "Aucun temps de travail pour le moment.", + "org.worktime.empty_description": "Ajuster la période pour consulter le temps imputé.", "org.worktime.by_repositories": "Par dépôts", "org.worktime.by_milestones": "Par jalons", "org.worktime.by_members": "Par membres", @@ -2856,6 +2879,30 @@ "admin.hooks": "Déclencheurs web", "admin.integrations": "Intégrations", "admin.authentication": "Sources d'authentification", + "admin.badges": "Badges", + "admin.badges.badges_manage_panel": "Gestion du badge", + "admin.badges.details": "Détails du badge", + "admin.badges.new_badge": "Créer un nouveau badge", + "admin.badges.slug": "Limace", + "admin.badges.slug_been_taken": "Cette limace existe déjà.", + "admin.badges.description": "Description", + "admin.badges.image_url": "URL de l’image", + "admin.badges.new_success": "Le badge « %s » a été créé.", + "admin.badges.update_success": "Le badge a été actualisé.", + "admin.badges.deletion_success": "Le badge a été supprimé.", + "admin.badges.edit_badge": "Modifier le badge", + "admin.badges.update_badge": "Mettre à jour le badge", + "admin.badges.delete_badge": "Supprimer le badge", + "admin.badges.delete_badge_desc": "Êtes-vous sûr de vouloir supprimer définitivement ce badge ?", + "admin.badges.users_with_badge": "Utilisateurs avec badge : %s", + "admin.badges.not_found": "Badge introuvable.", + "admin.badges.user_already_has": "Cet utilisateur a déjà ce badge.", + "admin.badges.user_add_success": "Le badge a bien été assigné à l‘utilisateur.", + "admin.badges.user_remove_success": "Le badge a bien été retiré de l‘utilisateur.", + "admin.badges.manage_users": "Gérer les utilisateurs", + "admin.badges.add_user": "Ajouter un utilisateur", + "admin.badges.remove_user": "Supprimer l’utilisateur", + "admin.badges.delete_user_desc": "Êtes-vous sûr de vouloir supprimer cet utilisateur du badge ?", "admin.emails": "Courriels de l’utilisateur", "admin.config": "Configuration", "admin.config_summary": "Résumé", @@ -2946,7 +2993,7 @@ "admin.dashboard.gc_lfs": "Purger les métaobjets LFS", "admin.dashboard.stop_zombie_tasks": "Arrêter les tâches zombies", "admin.dashboard.stop_endless_tasks": "Arrêter les tâches interminables", - "admin.dashboard.cancel_abandoned_jobs": "Annuler les travaux abandonnés", + "admin.dashboard.cancel_abandoned_jobs": "Annuler les actions des tâches abandonnés", "admin.dashboard.start_schedule_tasks": "Démarrer les tâches planifiées", "admin.dashboard.sync_branch.started": "Début de la synchronisation des branches", "admin.dashboard.sync_tag.started": "Synchronisation des étiquettes", @@ -3132,6 +3179,8 @@ "admin.auths.oauth2_required_claim_name_helper": "Définissez ce nom pour restreindre la connexion depuis cette source aux utilisateurs ayant une réclamation avec ce nom", "admin.auths.oauth2_required_claim_value": "Valeur de réclamation requise", "admin.auths.oauth2_required_claim_value_helper": "Restreindre la connexion depuis cette source aux utilisateurs ayant réclamé cette valeur.", + "admin.auths.open_id_connect_external_id_claim": "Nom déclaré de l’ID externe (facultatif)", + "admin.auths.open_id_connect_external_id_claim_helper": "Nom de la demande à utiliser en tant qu’identité externe de l’utilisateur, par défaut « sub ». Pour Azure AD / Entra ID, utilisez « oid » pour migrer depuis un fournisseur Azure AD V2. Remarque : la demande « oid » nécessite « profile » dans le champ Permissions ci-dessus.", "admin.auths.oauth2_group_claim_name": "Réclamer le nom fournissant les noms de groupe pour cette source. (facultatif)", "admin.auths.oauth2_full_name_claim_name": "Nom complet réclamé. (Optionnel. Si défini, le nom complet de l’utilisateur sera toujours synchronisé avec cette réclamation)", "admin.auths.oauth2_ssh_public_key_claim_name": "Nom réclamé de la clé publique SSH", @@ -3184,10 +3233,8 @@ "admin.config.server_config": "Configuration du serveur", "admin.config.app_name": "Titre du site", "admin.config.app_ver": "Version de Gitea", - "admin.config.app_url": "URL de base de Gitea", "admin.config.custom_conf": "Chemin du fichier de configuration", "admin.config.custom_file_root_path": "Emplacement personnalisé du fichier racine", - "admin.config.domain": "Domaine du serveur", "admin.config.disable_router_log": "Désactiver la Journalisation du Routeur", "admin.config.run_user": "Exécuter avec l'utilisateur", "admin.config.run_mode": "Mode d'Éxécution", @@ -3267,7 +3314,6 @@ "admin.config.cache_config": "Configuration du cache", "admin.config.cache_adapter": "Adaptateur du Cache", "admin.config.cache_interval": "Intervales du Cache", - "admin.config.cache_conn": "Liaison du Cache", "admin.config.cache_item_ttl": "Durée de vie des éléments dans le cache", "admin.config.cache_test": "Test du cache", "admin.config.cache_test_failed": "Impossible d’interroger le cache : %v.", @@ -3282,7 +3328,6 @@ "admin.config.instance_web_banner.message_placeholder": "Message de bannière (supporte markdown)", "admin.config.session_config": "Configuration de session", "admin.config.session_provider": "Fournisseur de session", - "admin.config.provider_config": "Configuration du fournisseur", "admin.config.cookie_name": "Nom du cookie", "admin.config.gc_interval_time": "Intervals GC", "admin.config.session_life_time": "Durée des sessions", @@ -3468,6 +3513,7 @@ "packages.dependencies": "Dépendances", "packages.keywords": "Mots-clés", "packages.details": "Détails", + "packages.name": "Nom du paquet", "packages.details.author": "Auteur", "packages.details.project_site": "Site du projet", "packages.details.repository_site": "Site du dépôt", @@ -3563,6 +3609,18 @@ "packages.swift.registry": "Configurez ce registre à partir d'un terminal :", "packages.swift.install": "Ajoutez le paquet dans votre fichier Package.swift:", "packages.swift.install2": "et exécutez la commande suivante :", + "packages.terraform.install": "Définissez votre état pour utiliser le backend HTTP", + "packages.terraform.install2": "et exécutez la commande suivante :", + "packages.terraform.lock_status": "Verrouiller le statut", + "packages.terraform.locked_by": "Verrouillé par %s.", + "packages.terraform.unlocked": "Déverrouillé", + "packages.terraform.lock": "Verrouiller", + "packages.terraform.unlock": "Déverrouiller", + "packages.terraform.lock.success": "L’état Terraform a été verrouillé avec succès.", + "packages.terraform.unlock.success": "L’état Terraform a été déverrouillé avec succès.", + "packages.terraform.lock.error.already_locked": "L’état Terraform est déjà verrouillé.", + "packages.terraform.delete.locked": "L’état Terraform est verrouillé et ne peut pas être supprimé.", + "packages.terraform.delete.latest": "La dernière version d’un état Terraform ne peut pas être supprimée.", "packages.vagrant.install": "Pour ajouter une machine Vagrant, exécutez la commande suivante :", "packages.settings.link": "Lier ce paquet à un dépôt", "packages.settings.link.description": "Si vous associez un paquet à un dépôt, le paquet sera inclus dans sa liste des paquets. Seul les dépôts d’un même propriétaire peuvent être associés. Laisser ce champ vide supprimera le lien.", @@ -3576,8 +3634,13 @@ "packages.settings.delete": "Supprimer le paquet", "packages.settings.delete.description": "Supprimer un paquet est permanent et irréversible.", "packages.settings.delete.notice": "Vous êtes sur le point de supprimer %s (%s). Cette opération est irréversible, êtes-vous sûr ?", + "packages.settings.delete.notice.package": "Vous êtes sur le point de supprimer %s dans toutes ses versions. C’est une opération irréversible, êtes-vous sûr ?", "packages.settings.delete.success": "Le paquet a été supprimé.", + "packages.settings.delete.version.success": "La version du paquet a été supprimée.", "packages.settings.delete.error": "Impossible de supprimer le paquet.", + "packages.settings.delete.version": "Supprimer la version", + "packages.settings.delete.confirm": "Saisissez le nom du paquet pour confirmer", + "packages.settings.delete.invalid_package_name": "Le nom de paquet saisi est incorrect.", "packages.owner.settings.cargo.title": "Index du Registre Cargo", "packages.owner.settings.cargo.initialize": "Initialiser l'index", "packages.owner.settings.cargo.initialize.description": "Un dépôt Git d’index spécial est nécessaire pour utiliser le registre Cargo. Utiliser cette option va (re)créer le dépôt et le configurer automatiquement.", @@ -3644,6 +3707,7 @@ "actions.runners.id": "ID", "actions.runners.name": "Nom", "actions.runners.owner_type": "Type", + "actions.runners.availability": "Disponibilité", "actions.runners.description": "Description", "actions.runners.labels": "Labels", "actions.runners.last_online": "Dernière fois en ligne", @@ -3659,6 +3723,12 @@ "actions.runners.update_runner": "Appliquer les modifications", "actions.runners.update_runner_success": "Exécuteur mis à jour avec succès", "actions.runners.update_runner_failed": "Impossible d'actualiser l'Exécuteur", + "actions.runners.enable_runner": "Activer cet exécuteur", + "actions.runners.enable_runner_success": "Exécuteur activé avec succès", + "actions.runners.enable_runner_failed": "Impossible d’activer l’exécuteur", + "actions.runners.disable_runner": "Désactiver cet exécuteur", + "actions.runners.disable_runner_success": "Exécuteur désactivé avec succès", + "actions.runners.disable_runner_failed": "Impossible de désactiver l’exécuteur", "actions.runners.delete_runner": "Supprimer cet exécuteur", "actions.runners.delete_runner_success": "Exécuteur supprimé avec succès", "actions.runners.delete_runner_failed": "Impossible de supprimer l'Exécuteur", @@ -3677,6 +3747,8 @@ "actions.runs.workflow_run_count_1": "%d exécution du workflow", "actions.runs.workflow_run_count_n": "%d exécutions du workflow", "actions.runs.commit": "Révision", + "actions.runs.run_details": "Détails de l’exécution", + "actions.runs.workflow_file": "Fichier de flux de travail", "actions.runs.scheduled": "Planifié", "actions.runs.pushed_by": "soumis par", "actions.runs.invalid_workflow_helper": "La configuration du flux de travail est invalide. Veuillez vérifier votre fichier %s.", @@ -3699,7 +3771,13 @@ "actions.runs.delete.description": "Êtes-vous sûr de vouloir supprimer définitivement cette exécution ? Cette action ne peut pas être annulée.", "actions.runs.not_done": "Cette exécution du flux de travail n’est pas terminée.", "actions.runs.view_workflow_file": "Voir le fichier du flux de travail", - "actions.runs.workflow_graph": "Graphique du flux", + "actions.runs.summary": "Résumé", + "actions.runs.all_jobs": "Toutes les tâches", + "actions.runs.attempt": "Tentative", + "actions.runs.latest": "Dernière", + "actions.runs.latest_attempt": "Dernière tentative", + "actions.runs.triggered_via": "Déclenché via %s", + "actions.runs.total_duration": "Durée totale :", "actions.workflow.disable": "Désactiver le flux de travail", "actions.workflow.disable_success": "Le flux de travail « %s » a bien été désactivé.", "actions.workflow.enable": "Activer le flux de travail", @@ -3749,5 +3827,24 @@ "git.filemode.normal_file": "Fichier normal", "git.filemode.executable_file": "Fichier exécutable", "git.filemode.symbolic_link": "Lien symbolique", - "git.filemode.submodule": "Sous-module" + "git.filemode.submodule": "Sous-module", + "org.repos.none": "Aucun dépôt.", + "actions.general.permissions": "Permissions du jeton des actions", + "actions.general.token_permissions.mode": "Permissions par défaut du jeton", + "actions.general.token_permissions.mode.desc": "Une tâche d’Actions utilisera les permissions par défaut si aucune n’est déclarée dans le fichier du flux de travail.", + "actions.general.token_permissions.mode.permissive": "Permissif", + "actions.general.token_permissions.mode.permissive.desc": "Permissions en lecture et écriture sur le dépôt de la tâche.", + "actions.general.token_permissions.mode.restricted": "Restreint", + "actions.general.token_permissions.mode.restricted.desc": "Permissions en lecture seule pour le contenu (code, publications) sur le dépôt de la tâche.", + "actions.general.token_permissions.override_owner": "Écraser la configuration faite par le propriétaire", + "actions.general.token_permissions.override_owner_desc": "Si actif, ce dépôt utilisera sa propre configuration pour les actions au lieu de respecter celle du propriétaire (utilisateur ou organisation).", + "actions.general.token_permissions.maximum": "Permissions maximales du jeton", + "actions.general.token_permissions.maximum.description": "Les permissions effectives de la tâche des actions seront limitées par les permissions maximales.", + "actions.general.token_permissions.fork_pr_note": "Si une tâche est démarrée par une demande de fusion depuis une bifurcation, ses permissions effectives ne dépasseront pas les permissions en lecture-seule.", + "actions.general.token_permissions.customize_max_permissions": "Personnaliser les permissions maximales", + "actions.general.cross_repo": "Accès inter-dépôt", + "actions.general.cross_repo_desc": "Permet aux dépôts sélectionnés d’être visible en lecture-seule par tous les dépôts de ce propriétaire à l’aide de GITEA_TOKEN lors de l’exécution des tâches d’actions.", + "actions.general.cross_repo_selected": "Dépôts sélectionnés", + "actions.general.cross_repo_target_repos": "Dépôts cibles", + "actions.general.cross_repo_add": "Ajouter un dépôt cible" } diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 663de75772..d648a83fa5 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -122,6 +122,7 @@ "unpin": "Díphoráil", "artifacts": "Déantáin", "expired": "Imithe in éag", + "artifact_expires_at": "Éagaíonn ag %s", "confirm_delete_artifact": "An bhfuil tú cinnte gur mian leat an déantán '%s' a scriosadh?", "archived": "Cartlann", "concept_system_global": "Domhanda", @@ -173,6 +174,8 @@ "search.org_kind": "Cuardaigh eagraíochtaí…", "search.team_kind": "Cuardaigh foirne…", "search.code_kind": "Cuardaigh cód…", + "search.code_empty": "Tosaigh cuardach cóid.", + "search.code_empty_description": "Cuir isteach eochairfhocal chun cuardach a dhéanamh ar fud an chóid.", "search.code_search_unavailable": "Níl cuardach cód ar fáil faoi láthair. Déan teagmháil le riarthóir an láithreáin.", "search.code_search_by_git_grep": "Soláthraíonn “git grep” torthaí cuardaigh cód reatha. D'fhéadfadh torthaí níos fearr a bheith ann má chuireann riarthóir an láithreáin ar chumas Innéacsaithe", "search.package_kind": "Cuardaigh pacáistí…", @@ -213,11 +216,15 @@ "editor.buttons.switch_to_legacy.tooltip": "Úsáid an eagarthóir oidhreachta ina ionad", "editor.buttons.enable_monospace_font": "Cumasaigh cló monospace", "editor.buttons.disable_monospace_font": "Díchumasaigh cló monospace", + "editor.code_editor.command_palette": "Pailéad Ordú", + "editor.code_editor.find": "Aimsigh", + "editor.code_editor.placeholder": "Cuir isteach ábhar an chomhaid anseo", "filter.string.asc": "A - Z", "filter.string.desc": "Z - A", "error.occurred": "Tharla earráid", "error.report_message": "Má chreideann tú gur fabht Gitea é seo, déan cuardach le haghaidh ceisteanna ar GitHub nó oscail eagrán nua más gá.", "error.not_found": "Ní raibh an sprioc in ann a fháil.", + "error.permission_denied": "Cead diúltaithe.", "error.network_error": "Earráid líonra", "startpage.app_desc": "Seirbhís Git gan phian, féin-óstáil", "startpage.install": "Éasca a shuiteáil", @@ -264,7 +271,7 @@ "install.lfs_path": "Cosán Fréamh Git LFS", "install.lfs_path_helper": "Stórálfar comhaid a rianóidh Git LFS san eolaire seo. Fág folamh le díchumasú.", "install.run_user": "Rith mar Ainm Úsáideora", - "install.run_user_helper": "An ainm úsáideora an chórais oibriúcháin a ritheann Gitea mar. Tabhair faoi deara go gcaithfidh rochtain a bheith ag an úsáideoir seo ar fhréamhchosán an taisclainne.", + "install.run_user_helper": "Ainm úsáideora an chórais oibriúcháin a ritheann Gitea mar, ní mór rochtain scríofa a bheith aige ar na cosáin sonraí. Braitear an luach seo go huathoibríoch agus ní féidir é a athrú anseo. Chun úsáideoir difriúil a úsáid, atosú Gitea faoin gcuntas sin.", "install.domain": "Fearann ​​Freastalaí", "install.domain_helper": "Seoladh fearainn nó óstach don fhreastalaí.", "install.ssh_port": "Port Freastalaí SSH", @@ -306,12 +313,10 @@ "install.admin_email": "Seoladh ríomhphoist", "install.install_btn_confirm": "Suiteáil Gitea", "install.test_git_failed": "Ní féidir ordú 'git' a thástáil: %v", - "install.sqlite3_not_available": "Ní thacaíonn an leagan Gitea seo le SQLite3. Íoslódáil an leagan dénártha oifigiúil ó %s (ní an leagan 'gobuild').", "install.invalid_db_setting": "Tá na socruithe bunachar sonraí neamhbhailí:%v", "install.invalid_db_table": "Tá an tábla bunachar sonraí \"%s\" neamhbhailí: %v", "install.invalid_repo_path": "Tá cosán fréimhe an stór neamhbhailí:%v", "install.invalid_app_data_path": "Tá cosán sonraí an aip neamhbhailí:%v", - "install.run_user_not_match": "Ní hé an t-ainm úsáideora 'rith mar' an t-ainm úsáideora reatha: %s -> %s", "install.internal_token_failed": "Theip ar chomhartha inmheánach a ghiniúint:%v", "install.secret_key_failed": "Theip ar an eochair rúnda a ghiniúint:%v", "install.save_config_failed": "Theip ar chumraíocht a shábháil:%v", @@ -633,14 +638,8 @@ "user.block.unblock.failure": "Theip ar an úsáideoir a díbhlocáil: %s", "user.block.blocked": "Chuir tú bac ar an úsáideoir seo.", "user.block.title": "Cuir bac ar úsáideoir", - "user.block.info": "Cuireann blocáil úsáideora cosc orthu idirghníomhú le stórais, mar shampla iarratais tarraingthe nó saincheisteanna a oscailt nó trácht a dhéanamh orthu. Níos mó a fhoghlaim faoi bhac úsáideora.", - "user.block.info_1": "Cuireann blocáil úsáideora cosc ar na gníomhartha seo a leanas ar do chuntas agus ar do stór:", - "user.block.info_2": "ag leanúint do chuntas", - "user.block.info_3": "seol fógraí chugat ag @mentioning d'ainm úsáideora", - "user.block.info_4": "ag tabhairt cuireadh duit mar chomhoibritheoir chuig a stórtha", - "user.block.info_5": "ag réaladh, ag forcáil nó ag féachaint ar stórais", - "user.block.info_6": "ceisteanna nó iarrataí tarraingthe a oscailt agus trácht", - "user.block.info_7": "ag freagairt do do thuairimí i saincheisteanna nó i n-iarratais tarraingthe", + "user.block.info": "Má chuireann bac ar úsáideoir, cuirtear cosc ​​​​orthu idirghníomhú le stórtha, amhail iarratais tarraingthe nó saincheisteanna a oscailt nó trácht a dhéanamh orthu.", + "user.block.info.docs": "Foghlaim tuilleadh faoi úsáideoir a bhlocáil.", "user.block.user_to_block": "Úsáideoir chun blocáil", "user.block.note": "Nóta", "user.block.note.title": "Nóta roghnach:", @@ -969,7 +968,6 @@ "repo.visibility_description": "Ní bheidh ach an t-úinéir nó baill na heagraíochta má tá cearta acu in ann é a fheiceáil.", "repo.visibility_helper": "Déan stóras príobháideach", "repo.visibility_helper_forced": "Cuireann riarthóir do shuíomh iallach ar stórais nua a bheith príobháideach.", - "repo.visibility_fork_helper": "(Beidh tionchar ag athrú seo ar gach forc.)", "repo.clone_helper": "Teastaíonn cabhair ó chlónáil? Tabhair cuairt ar Cabhair.", "repo.fork_repo": "Stóras Forc", "repo.fork_from": "Forc ó", @@ -1041,6 +1039,7 @@ "repo.forks": "Forcanna", "repo.stars": "Réaltaí", "repo.reactions_more": "agus %d níos mó", + "repo.reactions": "Imoibrithe", "repo.unit_disabled": "Tá an chuid stórais seo díchumasaithe ag riarthóir an láithreáin.", "repo.language_other": "Eile", "repo.adopt_search": "Cuir isteach ainm úsáideora chun cuardach a dhéanamh ar stórtha neamhuchtaithe… (fág bán chun gach ceann a aimsiú)", @@ -1061,8 +1060,8 @@ "repo.transfer.accept_desc": "Aistriú chuig “%s”", "repo.transfer.reject": "Diúltaigh aistriú", "repo.transfer.reject_desc": "Cealaigh aistriú chuig \"%s\"", - "repo.transfer.no_permission_to_accept": "Níl cead agat glacadh leis an aistriú seo.", - "repo.transfer.no_permission_to_reject": "Níl cead agat an aistriú seo a dhiúltú.", + "repo.transfer.is_transferring": "Ag aistriú…", + "repo.transfer.is_transferring_prompt": "Tá an stórlann á aistriú go %s", "repo.desc.private": "Príobháideach", "repo.desc.public": "Poiblí", "repo.desc.public_access": "Rochtain Phoiblí", @@ -1213,7 +1212,7 @@ "repo.ambiguous_runes_description": "Tá carachtair Unicode sa chomhad seo a d'fhéadfadh a bheith mearbhall le carachtair eile. Má cheapann tú go bhfuil sé seo d'aon ghnó, is féidir leat neamhaird a dhéanamh go sábháilte don rabhadh seo Úsáid an cnaipe Escape chun iad a nochtadh. ", "repo.invisible_runes_line": "Tá carachtair unicode dofheicthe ag an líne seo ", "repo.ambiguous_runes_line": "Tá carachtair unicode débhríoch ag an líne seo ", - "repo.ambiguous_character": "Is féidir %[1]c [U+%04[1]X] a mheascadh le %[2]c [U+%04[2]X]", + "repo.ambiguous_character": "Is féidir mearbhall a dhéanamh idir %[1]s agus %[2]s", "repo.escape_control_characters": "Éalú", "repo.unescape_control_characters": "Dí-Éalú", "repo.file_copy_permalink": "Cóipeáil Buan-nasc", @@ -1306,7 +1305,7 @@ "repo.editor.upload_file_is_locked": "Tá comhad \"%s\" faoi ghlas ag %s.", "repo.editor.upload_files_to_dir": "Uaslódáil comhaid go \"%s\"", "repo.editor.cannot_commit_to_protected_branch": "Ní féidir gealltanas a thabhairt don bhrainse faoi chosaint \"%s\".", - "repo.editor.no_commit_to_branch": "Ní féidir tiomantas a thabhairt go díreach don bhrainse mar:", + "repo.editor.no_commit_to_branch": "Ní cheadaítear gealltanas díreach a thabhairt don bhrainse mar gheall ar:", "repo.editor.user_no_push_to_branch": "Ní féidir leis an úsáideoir brúigh go dtí an brainse", "repo.editor.require_signed_commit": "Éilíonn an Brainse tiomantas sínithe", "repo.editor.cherry_pick": "Roghnaigh silíní %s ar:", @@ -1322,7 +1321,7 @@ "repo.commits.desc": "Brabhsáil stair athraithe cód foinse.", "repo.commits.commits": "Tiomáintí", "repo.commits.no_commits": "Níl aon ghealltanas i gcoiteann. Tá stair iomlán difriúil ag \"%s\" agus \"%s\".", - "repo.commits.nothing_to_compare": "Tá na brainsí seo cothrom.", + "repo.commits.nothing_to_compare": "Níl aon difríochtaí le taispeáint.", "repo.commits.search.tooltip": "Is féidir eochairfhocail a réamhfhostú le “údar:”, “committer:”, “after:”, nó “before:”, e.g. \"fill an t-údar:Alice roimh: 2019-01-13\".", "repo.commits.search_branch": "An Brainse seo", "repo.commits.search_all": "Gach Brainse", @@ -1354,10 +1353,13 @@ "repo.projects.desc": "Saincheisteanna a bhainistiú agus tionscadail a tharraingt isteach.", "repo.projects.description": "Cur síos (roghnach)", "repo.projects.description_placeholder": "Cur síos", + "repo.projects.empty": "Gan aon tionscadail go fóill.", + "repo.projects.empty_description": "Cruthaigh tionscadal chun saincheisteanna agus iarratais tarraingthe a chomhordú.", "repo.projects.create": "Cruthaigh Tionscadal", "repo.projects.title": "Teideal", "repo.projects.new": "Tionscadal Nua", "repo.projects.new_subheader": "Déan do chuid oibre a chomhordú, a rianú agus a nuashonrú in aon áit amháin, ionas go bhfanann na tionscadail trédhearcach agus de réir sceidil.", + "repo.projects.no_results": "Níl aon tionscadail a oireann do do chuardach.", "repo.projects.create_success": "Tá an tionscadal \"%s\" cruthaithe.", "repo.projects.deletion": "Scrios tionscadal", "repo.projects.deletion_desc": "Má scriostar tionscadal, bainfear de gach saincheist a bhaineann leis é. Lean ort?", @@ -1382,6 +1384,7 @@ "repo.projects.column.delete": "Scrios Colún", "repo.projects.column.deletion_desc": "Ag scriosadh colún tionscadail aistríonn gach saincheist ghaolmhar chuig an gcolún. Lean ar aghaidh?", "repo.projects.column.color": "Dath", + "repo.projects.column": "Colún", "repo.projects.open": "Oscailte", "repo.projects.close": "Dún", "repo.projects.column.assigned_to": "Sannta do", @@ -1399,11 +1402,12 @@ "repo.issues.new": "Eagrán Nua", "repo.issues.new.title_empty": "Ní féidir leis an teideal a bheith folamh", "repo.issues.new.labels": "Lipéid", - "repo.issues.new.no_label": "Gan Lipéad", + "repo.issues.new.no_labels": "Gan lipéid", "repo.issues.new.clear_labels": "Lipéid shoiléir", "repo.issues.new.projects": "Tionscadail", "repo.issues.new.clear_projects": "Tionscadail soiléire", - "repo.issues.new.no_projects": "Gan aon tionscadal", + "repo.issues.new.no_projects": "Gan aon tionscadail", + "repo.issues.new.no_column": "Gan aon cholún", "repo.issues.new.open_projects": "Tionscadail Oscailte", "repo.issues.new.closed_projects": "Tionscadail Dúnta", "repo.issues.new.no_items": "Gan aon earraí", @@ -1529,6 +1533,7 @@ "repo.issues.context.edit": "Cuir in eagar", "repo.issues.context.delete": "Scrios", "repo.issues.no_content": "Níl aon tuairisc ar fáil.", + "repo.issues.comment_no_content": "Níor cuireadh aon trácht ar fáil.", "repo.issues.close": "Dún Eagrán", "repo.issues.comment_pull_merged_at": "cumasc tiomantas %[1]s le %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "cumasc tiomantas %[1]s le %[2]s %[3]s", @@ -1778,8 +1783,9 @@ "repo.pulls.select_commit_hold_shift_for_range": "Roghnaigh tiomantas. Coinnigh síos Shift agus cliceáil chun raon a roghnú.", "repo.pulls.review_only_possible_for_full_diff": "Ní féidir athbhreithniú a dhéanamh ach amháin nuair a bhreathnaítear ar an difríocht iomlán", "repo.pulls.filter_changes_by_commit": "Scagaigh de réir tiomantas", - "repo.pulls.nothing_to_compare": "Tá na brainsí seo cothrom. Ní gá iarratas tarraingthe a chruthú.", - "repo.pulls.nothing_to_compare_have_tag": "Tá na brainsí/clibeanna roghnaithe comhionann.", + "repo.pulls.nothing_to_compare": "Níl aon difríochtaí le taispeáint. Níl aon ghá iarratas tarraingthe a chruthú.", + "repo.pulls.no_common_history": "Níl bonn cumaisc coitianta ag na brainsí seo. Roghnaigh bonn difriúil nó cuir brainse i gcomparáid.", + "repo.pulls.nothing_to_compare_have_tag": "Níl aon difríochtaí le taispeáint idir na brainsí nó na clibeanna roghnaithe.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Tá na brainsí seo cothrom. Beidh an PR seo folamh.", "repo.pulls.has_pull_request": "Tá iarratas tarraingthe idir na brainsí seo ann cheana: %[2]s#%[3]d", "repo.pulls.create": "Cruthaigh Iarratas Tarraing", @@ -1845,6 +1851,7 @@ "repo.pulls.merge_manually": "Cumaisc de láimh", "repo.pulls.merge_commit_id": "ID an tiomantis cumaisc", "repo.pulls.require_signed_wont_sign": "Éilíonn an bhrainse tiomáintí shínithe, ach ní shínífear an cumasc seo", + "repo.pulls.require_signed_head_commits_unverified": "Teastaíonn gealltanais sínithe ón mbrainse ach ní dheimhnítear gealltanas amháin nó níos mó ar an iarratas tarraingte seo", "repo.pulls.invalid_merge_option": "Ní féidir leat an rogha cumaisc seo a úsáid don iarratas tarraingthe seo.", "repo.pulls.merge_conflict": "Theip ar an gCumasc: Bhí coimhlint ann agus an cumasc á dhéanamh. Leid: Bain triail as straitéis dhifriúil.", "repo.pulls.merge_conflict_summary": "Teachtaireacht Earráide", @@ -1950,7 +1957,6 @@ "repo.signing.wont_sign.headsigned": "Ní shínífear an cumasc toisc nach bhfuil an tiomantas ceann sínithe.", "repo.signing.wont_sign.commitssigned": "Ní shínífear an cumasc toisc nach bhfuil na tiomáintí gaolmhara go léir sínithe.", "repo.signing.wont_sign.approved": "Ní shíníofar an cumaisc toisc nach bhfuil an PR ceadaithe.", - "repo.signing.wont_sign.not_signed_in": "Níl tú sínithe isteach.", "repo.ext_wiki": "Rochtain ar Vicí Seachtrach", "repo.ext_wiki.desc": "Nasc le vicí seachtrach.", "repo.wiki": "Vicí", @@ -2174,7 +2180,8 @@ "repo.settings.transfer_abort_invalid": "Ní féidir leat aistriú stóras nach bhfuil ann a chealú.", "repo.settings.transfer_abort_success": "Cuireadh an t-aistriú stóras chuig %s ar ceal go rathúil.", "repo.settings.transfer_desc": "Aistrigh an stóras seo chuig úsáideoir nó chuig eagraíocht a bhfuil cearta riarthóra agat ina leith.", - "repo.settings.transfer_form_title": "Cuir isteach ainm an stóras mar dhearbhú:", + "repo.settings.enter_repo_name_to_confirm": "Cuir isteach ainm an stórais mar dheimhniú:", + "repo.settings.enter_repo_full_name_to_confirm": "Cuir isteach ainm iomlán an stórais (úinéir/ainm) mar dheimhniú:", "repo.settings.transfer_in_progress": "Tá aistriú ar siúl faoi láthair. Cealaigh é más mian leat an stóras seo a aistriú chuig úsáideoir eile.", "repo.settings.transfer_notices_1": "- Caillfidh tú rochtain ar an stóras má aistríonn tú é chuig úsáideoir aonair.", "repo.settings.transfer_notices_2": "- Coimeádfaidh tú rochtain ar an stóras má aistríonn tú é chuig eagraíocht a bhfuil (comh)úinéir agat.", @@ -2250,13 +2257,14 @@ "repo.settings.webhook.delivery.success": "Cuireadh imeacht leis an scuaine seachadta. D'fhéadfadh sé cúpla soicind a thógáil sula dtaispeántar sé sa stair seachadta.", "repo.settings.githooks_desc": "Tá Git Crúcaí faoi thiomáint ag Git féin. Is féidir leat comhaid crúca a chur in eagar thíos chun oibríochtaí saincheaptha a shocrú.", "repo.settings.githook_edit_desc": "Mura bhfuil an hook neamhghníomhach, cuirfear ábhar samplach i láthair. Má fhágann tú ábhar go luach folamh díchumasófar an crúca seo.", - "repo.settings.githook_name": "Ainm Crúca", - "repo.settings.githook_content": "Ábhar Crúca", "repo.settings.update_githook": "Nuashonraigh Crúca", "repo.settings.add_webhook_desc": "Seolfaidh Gitea iarratais POST le cineál ábhar sonraithe chuig an spriocURL. Léigh tuilleadh sa treoir Crúcaí Gréasán.", "repo.settings.payload_url": "URL spriocdhírithe", "repo.settings.http_method": "Modh HTTP", "repo.settings.content_type": "Cineál Ábhar POST", + "repo.settings.webhook.name": "Ainm an Crúca Gréasáin", + "repo.settings.webhook.name_helper": "Tabhair ainm cairdiúil don crúca gréasáin seo más mian leat", + "repo.settings.webhook.name_empty": "Crúca Gréasáin Gan Ainm", "repo.settings.secret": "Rúnda", "repo.settings.webhook_secret_desc": "Más féidir le freastalaí an webhook rún a úsáid, is féidir leat lámhleabhar an webhook a leanúint agus rún a líonadh isteach anseo.", "repo.settings.slack_username": "Ainm úsáideora", @@ -2474,10 +2482,13 @@ "repo.settings.matrix.room_id": "ID seomra", "repo.settings.matrix.message_type": "Cineál teachtaireachta", "repo.settings.visibility.private.button": "Déan Príobháideach", - "repo.settings.visibility.private.text": "Má athraítear an infheictheacht go príobháideach, ní bheidh an stór le feiceáil ach ag baill cheadaithe agus d’fhéadfadh sé go mbainfí an gaol idir é agus forcanna, faireoirí agus réaltaí atá ann cheana féin.", + "repo.settings.visibility.private.text": "Má athraítear an infheictheacht go príobháideach, ní bheidh an stór le feiceáil ach ag baill cheadaithe agus d’fhéadfadh sé go mbainfí an gaol idir é agus forcanna, breathnóirí agus réaltaí atá ann cheana féin.", "repo.settings.visibility.private.bullet_title": "An infheictheacht a athrú go toil phríobháide", "repo.settings.visibility.private.bullet_one": "Déan an stóras le feiceáil ag baill cheadaithe amháin.", - "repo.settings.visibility.private.bullet_two": "D’fhéadfadh sé an gaol idir é agus forcanna, faireoirí, agus réaltaí a bhaint.", + "repo.settings.visibility.private.bullet_two": "Cuir an infheictheacht i bhfeidhm ar a fhorcanna, agus bain na breathnóirí agus na réaltaí.", + "repo.settings.visibility.private.stats_stars": "Tá %d réalta(í) sa stórlann seo a d'fhéadfadh a bheith caillte.", + "repo.settings.visibility.private.stats_watchers": "Tá %d breathnóir(í) sa stórlann seo a d'fhéadfadh a bheith caillte.", + "repo.settings.visibility.private.stats_forks": "Tá %d forc(anna) bainteach leis an stórlann seo.", "repo.settings.visibility.public.button": "Déan Poiblí", "repo.settings.visibility.public.text": "Má athraíonn an infheictheacht don phobal, beidh an stóras le feiceáil do dhuine ar bith.", "repo.settings.visibility.public.bullet_title": "Athróidh an infheictheacht go poiblí:", @@ -2713,6 +2724,8 @@ "org.members": "Comhaltaí", "org.teams": "Foirne", "org.code": "Cód", + "org.repos.empty": "Gan aon stórtha fós.", + "org.repos.empty_description": "Cruthaigh stórlann chun cód a roinnt leis an eagraíocht.", "org.lower_members": "comhaltaí", "org.lower_repositories": "stórais", "org.create_new_team": "Foireann Nua", @@ -2775,9 +2788,9 @@ "org.settings.labels_desc": "Cuir lipéid leis ar féidir iad a úsáid ar shaincheisteanna do gach stóras faoin eagraíocht seo.", "org.members.membership_visibility": "Infheictheacht Ballraíochta:", "org.members.public": "Infheicthe", - "org.members.public_helper": "dhéanamh i bhfolach", + "org.members.public_helper": "Déan i bhfolach", "org.members.private": "I bhfolach", - "org.members.private_helper": "a dhéanamh le feiceáil", + "org.members.private_helper": "Déan infheicthe", "org.members.member_role": "Ról Comhalta:", "org.members.owner": "Úinéir", "org.members.member": "Comhalta", @@ -2805,10 +2818,13 @@ "org.teams.no_desc": "Níl aon tuairisc ag an bhfoireann seo", "org.teams.settings": "Socruithe", "org.teams.owners_permission_desc": "Tá rochtain iomlán ag úinéirí ar gach stórais agus tá rochtain ag an riarthóir ar an eagraíocht.", + "org.teams.owners_permission_suggestion": "Is féidir leat foirne nua a chruthú do bhaill chun rialú rochtana mionsonraithe a fháil.", "org.teams.members": "Baill Foirne", + "org.teams.manage_team_member": "Bainistigh foirne agus baill", + "org.teams.manage_team_member_prompt": "Déantar baill a bhainistiú trí fhoirne. Cuir úsáideoirí le foireann chun cuireadh a thabhairt dóibh chuig an eagraíocht seo.", "org.teams.update_settings": "Nuashonrú Socruithe", "org.teams.delete_team": "Scrios Foireann", - "org.teams.add_team_member": "Cuir Comhalta Foirne leis", + "org.teams.add_team_member": "Cuir ball foirne leis", "org.teams.invite_team_member": "Tabhair cuireadh chuig %s", "org.teams.invite_team_member.list": "Cuirí ar Feitheamh", "org.teams.delete_team_title": "Scrios Foireann", @@ -2846,6 +2862,8 @@ "org.worktime.date_range_end": "Dáta deiridh", "org.worktime.query": "Ceist", "org.worktime.time": "Am", + "org.worktime.empty": "Gan aon sonraí ama oibre go fóill.", + "org.worktime.empty_description": "Coigeartaigh an raon dáta chun an t-am rianaithe a fheiceáil.", "org.worktime.by_repositories": "De réir stórtha", "org.worktime.by_milestones": "De réir clocha míle", "org.worktime.by_members": "Ag baill", @@ -3160,6 +3178,8 @@ "admin.auths.oauth2_required_claim_name_helper": "Socraigh an t-ainm seo chun logáil isteach ón bhfoinse seo a shrianadh d'úsáideoirí a bhfuil éileamh acu leis an ainm seo", "admin.auths.oauth2_required_claim_value": "Luach Éilimh Riachtanach", "admin.auths.oauth2_required_claim_value_helper": "Socraigh an luach seo chun logáil isteach ón bhfoinse seo a shrianadh chuig úsáideoirí a bhfuil éileamh acu leis an ainm agus an luach seo", + "admin.auths.open_id_connect_external_id_claim": "Ainm Éilimh Aitheantais Sheachtraigh (Roghnach)", + "admin.auths.open_id_connect_external_id_claim_helper": "Ainm an éilimh le húsáid mar aitheantas seachtrach an úsáideora. Is é \"sub\" an rogha réamhshocraithe. I gcás Azure AD / Entra ID, socraigh é seo go \"oid\" chun leanúnachas a choinneáil agus aistriú á dhéanamh ón soláthraí Azure AD V2. Tabhair faoi deara: éilíonn an t-éileamh \"oid\" go gcuirfí an raon feidhme \"próifíl\" san áireamh sa réimse Scóipe thuas.", "admin.auths.oauth2_group_claim_name": "Ainm éileamh ag soláthar ainmneacha grúpa don fhoinse seo (Roghnach)", "admin.auths.oauth2_full_name_claim_name": "Ainm Iomlán Éilimh Ainm. (Roghnach — má shocraítear é, déanfar ainm iomlán an úsáideora a shioncrónú leis an éileamh seo i gcónaí)", "admin.auths.oauth2_ssh_public_key_claim_name": "Ainm Éilimh Eochrach Phoiblí SSH", @@ -3212,10 +3232,8 @@ "admin.config.server_config": "Cumraíocht Freastalaí", "admin.config.app_name": "Teideal an Láithreáin", "admin.config.app_ver": "Leagan Gitea", - "admin.config.app_url": "URL Bonn Gitea", "admin.config.custom_conf": "Cosán Comhad Cumraíochta", "admin.config.custom_file_root_path": "Cosán Fréamh Comhad Saincheaptha", - "admin.config.domain": "Fearann ​​Freastalaí", "admin.config.disable_router_log": "Díchumasaigh Loga an Ródaire", "admin.config.run_user": "Rith Mar Ainm úsáideora", "admin.config.run_mode": "Mód Rith", @@ -3295,7 +3313,6 @@ "admin.config.cache_config": "Cumraíocht taisce", "admin.config.cache_adapter": "Cuibheoir taisce", "admin.config.cache_interval": "Eatramh Taisce", - "admin.config.cache_conn": "Ceangal Taisce", "admin.config.cache_item_ttl": "Mír Taisce TTL", "admin.config.cache_test": "Taisce Tástáil", "admin.config.cache_test_failed": "Theip ar an taisce a thaiscéaladh: %v.", @@ -3310,7 +3327,6 @@ "admin.config.instance_web_banner.message_placeholder": "Teachtaireacht meirge (tacaíonn sé le Markdown)", "admin.config.session_config": "Cumraíocht Seisiúin", "admin.config.session_provider": "Soláthraí Seisiúin", - "admin.config.provider_config": "Cumraíocht Soláthraí", "admin.config.cookie_name": "Ainm Fianán", "admin.config.gc_interval_time": "Am Eatramh GC", "admin.config.session_life_time": "Am Saoil na Seisiúin", @@ -3496,6 +3512,7 @@ "packages.dependencies": "Spleithiúlachtaí", "packages.keywords": "Eochairfhocail", "packages.details": "Sonraí", + "packages.name": "Ainm an Phacáiste", "packages.details.author": "Údar", "packages.details.project_site": "Suíomh an Tionscadail", "packages.details.repository_site": "Suíomh Stóras", @@ -3591,9 +3608,27 @@ "packages.swift.registry": "Socraigh an clárlann seo ón líne ordaithe:", "packages.swift.install": "Cuir an pacáiste i do chomhad Package.swift:", "packages.swift.install2": "agus reáchtáil an t-ordú seo a leanas:", + "packages.terraform.install": "Socraigh do stát chun an cúltaca HTTP a úsáid", + "packages.terraform.install2": "agus reáchtáil an t-ordú seo a leanas:", + "packages.terraform.lock_status": "Stádas Glas", + "packages.terraform.locked_by": "Glasáilte ag %s", + "packages.terraform.unlocked": "Díghlasáilte", + "packages.terraform.lock": "Glas", + "packages.terraform.unlock": "Díghlasáil", + "packages.terraform.lock.success": "Glasáladh staid Terraform go rathúil.", + "packages.terraform.unlock.success": "Díghlasáladh staid Terraform go rathúil.", + "packages.terraform.lock.error.already_locked": "Tá staid Terraform faoi ghlas cheana féin.", + "packages.terraform.delete.locked": "Tá staid an Terraform faoi ghlas agus ní féidir é a scriosadh.", + "packages.terraform.delete.latest": "Ní féidir an leagan is déanaí de staid Terraform a scriosadh.", "packages.vagrant.install": "Chun bosca Vagrant a chur leis, reáchtáil an t-ordú seo a leanas:", "packages.settings.link": "Nasc an pacáiste seo le stóras", - "packages.settings.link.description": "Má nascann tú pacáiste le stórlann, beidh an pacáiste le feiceáil i liosta pacáistí an stórlainne. Ní féidir ach stórlanna faoin úinéir céanna a nascadh. Má fhágtar an réimse folamh, bainfear an nasc.", + "packages.settings.link.description": "Má nascann tú pacáiste le stórlann, beidh an pacáiste le feiceáil i liosta pacáistí an stórlainne.", + "packages.settings.link.notice1": "Ní féidir ach stórtha faoin úinéir céanna a nascadh.", + "packages.settings.link.notice2": "Ní athraíonn nascadh stórtha infheictheacht an phacáiste.", + "packages.settings.link.notice3": "Bainfear an nasc má fhágtar an réimse folamh.", + "packages.settings.visibility": "Infheictheacht an phacáiste", + "packages.settings.visibility.inherit": "Faightear infheictheacht an phacáiste ón úinéir agus ní féidir í a athrú go neamhspleách anseo. Chun í a athrú, nuashonraigh socruithe infheictheachta an úsáideora nó na heagraíochta ar leis an bpacáiste seo.", + "packages.settings.visibility.button": "Athraigh infheictheacht an úinéara", "packages.settings.link.select": "Roghnaigh Stóras", "packages.settings.link.button": "Nuashonraigh Nasc Stórais", "packages.settings.link.success": "D'éirigh le nasc an stórais a nuashonrú.", @@ -3604,8 +3639,13 @@ "packages.settings.delete": "Scrios pacáiste", "packages.settings.delete.description": "Tá pacáiste a scriosadh buan agus ní féidir é a chur ar ais.", "packages.settings.delete.notice": "Tá tú ar tí %s (%s) a scriosadh. Tá an oibríocht seo dochúlaithe, an bhfuil tú cinnte?", + "packages.settings.delete.notice.package": "Tá tú ar tí %s agus a leaganacha uile a scriosadh. Ní féidir an oibríocht seo a aisiompú, an bhfuil tú cinnte?", "packages.settings.delete.success": "Tá an pacáiste scriosta.", + "packages.settings.delete.version.success": "Scriosadh an leagan pacáiste.", "packages.settings.delete.error": "Theip ar an pacáiste a scriosadh.", + "packages.settings.delete.version": "Scrios an leagan", + "packages.settings.delete.confirm": "Cuir isteach ainm an phacáiste le deimhniú", + "packages.settings.delete.invalid_package_name": "Tá ainm an phacáiste a chuir tú isteach mícheart.", "packages.owner.settings.cargo.title": "Innéacs Clárlann Lasta", "packages.owner.settings.cargo.initialize": "Innéacs a chur i dtosach", "packages.owner.settings.cargo.initialize.description": "Tá gá le stóras innéacs speisialta Git chun an clárlann Cargo a úsáid. Tríd an rogha seo, cruthófar an stóras (nó athchruthófar é) agus cumrófar é go huathoibríoch.", @@ -3712,6 +3752,8 @@ "actions.runs.workflow_run_count_1": "%d rith sreabha oibre", "actions.runs.workflow_run_count_n": "%d rith sreabha oibre", "actions.runs.commit": "Tiomantas", + "actions.runs.run_details": "Sonraí Rith", + "actions.runs.workflow_file": "Comhad sreabhadh oibre", "actions.runs.scheduled": "Sceidealaithe", "actions.runs.pushed_by": "bhrú ag", "actions.runs.invalid_workflow_helper": "Tá comhad cumraíochta sreabhadh oibre nebhailí. Seiceáil do chomhad cumraithe le do thoil: %s", @@ -3734,9 +3776,11 @@ "actions.runs.delete.description": "An bhfuil tú cinnte gur mian leat an rith sreabha oibre seo a scriosadh go buan? Ní féidir an gníomh seo a chealú.", "actions.runs.not_done": "Níl an rith sreabha oibre seo críochnaithe.", "actions.runs.view_workflow_file": "Féach ar chomhad sreabha oibre", - "actions.runs.workflow_graph": "Graf Sreabhadh Oibre", "actions.runs.summary": "Achoimre", "actions.runs.all_jobs": "Gach post", + "actions.runs.attempt": "Iarracht", + "actions.runs.latest": "Is déanaí", + "actions.runs.latest_attempt": "An iarracht is déanaí", "actions.runs.triggered_via": "Spreagtha trí %s", "actions.runs.total_duration": "Fad iomlán:", "actions.workflow.disable": "Díchumasaigh sreabhadh oibre", diff --git a/options/locale/locale_ko-KR.json b/options/locale/locale_ko-KR.json index e4ea00f31c..a63298a2fe 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 서버 포트", @@ -306,12 +313,10 @@ "install.admin_email": "이메일 주소", "install.install_btn_confirm": "Gitea 설치하기", "install.test_git_failed": "'git' 명령 테스트 실패: %v", - "install.sqlite3_not_available": "해당 버전에서는 SQLite3를 지원하지 않습니다. %s에서 공식 버전을 다운로드해주세요. ('gobuild' 버전이 아닙니다.)", "install.invalid_db_setting": "데이터베이스 설정이 올바르지 않습니다: %v", "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 +639,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 +968,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 +1039,7 @@ "repo.forks": "포크", "repo.stars": "별점", "repo.reactions_more": "그리고 %d 더", + "repo.reactions": "리액션", "repo.unit_disabled": "사이트 운영자가 이 리포지토리 섹션을 비활성화했습니다.", "repo.language_other": "기타", "repo.adopt_search": "편입되지 않은 리포지토리를 찾을 사용자명을 입력하세요... (모두 찾으려면 비워두세요)", @@ -1061,8 +1060,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 +1212,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 +1353,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": "프로젝트를 삭제하여 모든 관련 이슈에서 제거합니다. 계속하시겠습니까?", @@ -1382,6 +1384,7 @@ "repo.projects.column.delete": "열 삭제", "repo.projects.column.deletion_desc": "프로젝트 열을 삭제하면 모든 관련 이슈가 기본 열로 이동합니다. 계속하시겠습니까?", "repo.projects.column.color": "색", + "repo.projects.column": "열", "repo.projects.open": "열기", "repo.projects.close": "닫기", "repo.projects.column.assigned_to": "다음에 할당", @@ -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에 수동 머지됨", @@ -1779,6 +1784,7 @@ "repo.pulls.review_only_possible_for_full_diff": "전체 diff를 볼 때만 검토가 가능합니다", "repo.pulls.filter_changes_by_commit": "커밋으로 필터링", "repo.pulls.nothing_to_compare": "이 브랜치들은 동일합니다. 풀 리퀘스트를 생성할 필요가 없습니다.", + "repo.pulls.no_common_history": "이 브랜치들은 공통된 머지 베이스를 공유하지 않습니다. 다른 베이스 브랜치 또는 비교 브랜치를 선택해 주세요.", "repo.pulls.nothing_to_compare_have_tag": "선택된 브랜치/태그는 동일합니다.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "이 브랜치들은 동일합니다. 이 PR은 비어 있을 것입니다.", "repo.pulls.has_pull_request": "이 브랜치들 사이의 풀 리퀘스트가 이미 존재합니다: %[2]s#%[3]d", @@ -1845,6 +1851,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": "오류 메시지", @@ -1950,7 +1957,6 @@ "repo.signing.wont_sign.headsigned": "헤드 커밋이 서명되지 않아 머지가 서명되지 않을 것입니다.", "repo.signing.wont_sign.commitssigned": "연관된 모든 커밋이 서명되지 않아 머지가 서명되지 않을 것입니다.", "repo.signing.wont_sign.approved": "PR이 승인되지 않아 머지가 서명되지 않을 것입니다.", - "repo.signing.wont_sign.not_signed_in": "로그인되어 있지 않습니다.", "repo.ext_wiki": "외부 위키 액세스", "repo.ext_wiki.desc": "외부 위키에 연결하기.", "repo.wiki": "위키", @@ -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": "실행 모드", @@ -3295,7 +3313,6 @@ "admin.config.cache_config": "캐시 설정", "admin.config.cache_adapter": "캐시 어댑터", "admin.config.cache_interval": "캐시 간격", - "admin.config.cache_conn": "캐시 연결", "admin.config.cache_item_ttl": "캐시 아이템 TTL", "admin.config.cache_test": "캐시 테스트", "admin.config.cache_test_failed": "캐시 검사에 실패했습니다: %v.", @@ -3310,7 +3327,6 @@ "admin.config.instance_web_banner.message_placeholder": "배너 메시지 (마크다운 지원)", "admin.config.session_config": "세션 설정", "admin.config.session_provider": "세션 공급자", - "admin.config.provider_config": "공급자 설정", "admin.config.cookie_name": "쿠키 이름", "admin.config.gc_interval_time": "GC 인터벌 시간", "admin.config.session_life_time": "세션 수명", @@ -3496,6 +3512,7 @@ "packages.dependencies": "의존성", "packages.keywords": "키워드", "packages.details": "상세 정보", + "packages.name": "패키지 이름", "packages.details.author": "작성자", "packages.details.project_site": "프로젝트 사이트", "packages.details.repository_site": "리포지토리 사이트", @@ -3591,9 +3608,27 @@ "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": "패키지를 리포지토리에 연결하면 리포지토리의 패키지 목록에 표시됩니다.", + "packages.settings.link.notice1": "동일한 소유자의 저장소만 연결할 수 있습니다.", + "packages.settings.link.notice2": "저장소를 연결해도 패키지 공개 범위는 변경되지 않습니다.", + "packages.settings.link.notice3": "입력란을 비워 두면 링크가 삭제됩니다.", + "packages.settings.visibility": "패키지 공개범위", + "packages.settings.visibility.inherit": "패키지 공개 범위는 소유자로부터 상속되므로 여기에서 직접 변경할 수 없습니다. 이를 변경하려면 이 패키지를 소유한 사용자 또는 조직의 공개 설정에서 업데이트하세요.", + "packages.settings.visibility.button": "소유자 공개범위 변경", "packages.settings.link.select": "리포지토리 선택", "packages.settings.link.button": "리포지토리 링크 업데이트", "packages.settings.link.success": "리포지토리 링크가 성공적으로 업데이트 되었습니다.", @@ -3604,8 +3639,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 +3752,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 +3776,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 8bdd8d7faf..6b9ba53878 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 服务端口", @@ -304,12 +313,10 @@ "install.admin_email": "邮箱地址", "install.install_btn_confirm": "立即安装", "install.test_git_failed": "无法识别 'git' 命令:%v", - "install.sqlite3_not_available": "您所使用的发行版不支持 SQLite3,请从 %s 下载官方构建版,而不是 gobuild 版本。", "install.invalid_db_setting": "数据库设置无效: %v", "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 +549,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 +582,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 +638,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 +647,7 @@ "user.block.note.edit": "编辑备注", "user.block.list": "已屏蔽用户", "user.block.list.none": "您没有已屏蔽的用户。", + "settings.general": "常规", "settings.profile": "个人信息", "settings.account": "账号", "settings.appearance": "外观", @@ -676,7 +679,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 +756,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 +853,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 +872,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 +896,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 +937,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 +968,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 +1038,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 +1060,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 +1212,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 +1255,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 +1353,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,11 +1379,12 @@ "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.column": "列", "repo.projects.open": "开启", "repo.projects.close": "关闭", "repo.projects.column.assigned_to": "指派给", @@ -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": "停止计时器", @@ -1775,6 +1784,7 @@ "repo.pulls.review_only_possible_for_full_diff": "只有在查看全部差异时才能进行审核", "repo.pulls.filter_changes_by_commit": "按提交筛选", "repo.pulls.nothing_to_compare": "分支内容相同,无需创建合并请求。", + "repo.pulls.no_common_history": "这些分支没有共同的合并基点。请选择不同的基点或比较分支。", "repo.pulls.nothing_to_compare_have_tag": "所选分支 / Git 标签相同。", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "这些分支是相等的,此合并请求将为空。", "repo.pulls.has_pull_request": "这些分支之间的合并请求已存在:%[2]s#%[3]d", @@ -1841,6 +1851,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 +1905,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 +1934,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": "里程碑已删除。", @@ -1946,7 +1957,6 @@ "repo.signing.wont_sign.headsigned": "合并将不会被签名,因为最新提交没有签名。", "repo.signing.wont_sign.commitssigned": "合并将不会被签名,因为所有相关的提交都没有签名。", "repo.signing.wont_sign.approved": "合并将不会被签名,因为合并请求未被批准。", - "repo.signing.wont_sign.not_signed_in": "您还没有登录。", "repo.ext_wiki": "访问外部百科", "repo.ext_wiki.desc": "链接到外部百科。", "repo.wiki": "百科", @@ -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": "删除分支保护", @@ -2462,7 +2474,7 @@ "repo.settings.tags.protection.allowed.noone": "无", "repo.settings.tags.protection.create": "保护 Git 标签", "repo.settings.tags.protection.none": "没有受保护的 Git 标签。", - "repo.settings.tags.protection.pattern.description": "您可以使用单个名称或 glob 表达式匹配或正则表达式来匹配多个 Git 标签。了解详情请访问 保护标签指南。", + "repo.settings.tags.protection.pattern.description": "您可以使用单个名称或 glob 表达式匹配或正则表达式来匹配多个 Git 标签。欲了解详情请访问 保护标签指南。", "repo.settings.bot_token": "Bot 令牌", "repo.settings.chat_id": "聊天 ID", "repo.settings.thread_id": "线程 ID", @@ -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": "您确定要更新该邮箱地址吗?", @@ -3053,11 +3099,11 @@ "admin.packages.size": "大小", "admin.packages.published": "已发布", "admin.defaulthooks": "默认 Web 钩子", - "admin.defaulthooks.desc": "当某些 Gitea 事件触发时,Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子是默认配置,将被复制到所有新的仓库中。详情请访问 Web 钩子指南。", + "admin.defaulthooks.desc": "当某些 Gitea 事件触发时,Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子是默认配置,将被复制到所有新的仓库中。欲了解详情请访问 Web 钩子指南。", "admin.defaulthooks.add_webhook": "添加默认 Web 钩子", "admin.defaulthooks.update_webhook": "更新默认 Web 钩子", "admin.systemhooks": "系统 Web 钩子", - "admin.systemhooks.desc": "当某些 Gitea 事件触发时,Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子将作用于系统上的所有仓库,所以请考虑这可能带来的任何性能影响。了解详情请访问 Web 钩子指南。", + "admin.systemhooks.desc": "当某些 Gitea 事件触发时,Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子将作用于系统上的所有仓库,所以请考虑这可能带来的任何性能影响。欲了解详情请访问 Web 钩子指南。", "admin.systemhooks.add_webhook": "添加系统 Web 钩子", "admin.systemhooks.update_webhook": "更新系统 Web 钩子", "admin.auths.auth_manage_panel": "认证源管理", @@ -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": "运行模式", @@ -3267,7 +3313,6 @@ "admin.config.cache_config": "缓存配置", "admin.config.cache_adapter": "缓存适配器", "admin.config.cache_interval": "缓存周期", - "admin.config.cache_conn": "缓存连接字符串", "admin.config.cache_item_ttl": "缓存项目 TTL", "admin.config.cache_test": "测试缓存", "admin.config.cache_test_failed": "缓存测试失败:%v。", @@ -3282,7 +3327,6 @@ "admin.config.instance_web_banner.message_placeholder": "横幅消息(支持 Markdown)", "admin.config.session_config": "会话配置", "admin.config.session_provider": "会话提供者", - "admin.config.provider_config": "提供者配置", "admin.config.cookie_name": "Cookie 名称", "admin.config.gc_interval_time": "GC 周期", "admin.config.session_life_time": "会话生命周期", @@ -3468,6 +3512,7 @@ "packages.dependencies": "依赖", "packages.keywords": "关键词", "packages.details": "详情", + "packages.name": "软件包名称", "packages.details.author": "作者", "packages.details.project_site": "项目站点", "packages.details.repository_site": "仓库站点", @@ -3563,6 +3608,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 +3627,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 +3647,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 +3706,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 +3722,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 +3746,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 +3767,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 +3826,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 4dd3f14e06..19e22f65f1 100644 --- a/package.json +++ b/package.json @@ -1,147 +1,128 @@ { "type": "module", - "packageManager": "pnpm@10.30.3", + "packageManager": "pnpm@11.0.8", "engines": { - "node": ">= 22.6.0", - "pnpm": ">= 10.0.0" + "node": ">= 22.18.0", + "pnpm": ">= 11.0.0" }, "dependencies": { "@citation-js/core": "0.7.21", "@citation-js/plugin-bibtex": "0.7.21", "@citation-js/plugin-csl": "0.7.22", "@citation-js/plugin-software-formats": "0.6.2", + "@codemirror/autocomplete": "6.20.2", + "@codemirror/commands": "6.10.3", + "@codemirror/lang-json": "6.0.2", + "@codemirror/lang-markdown": "6.5.0", + "@codemirror/language": "6.12.3", + "@codemirror/language-data": "6.5.2", + "@codemirror/legacy-modes": "6.5.2", + "@codemirror/lint": "6.9.6", + "@codemirror/search": "6.7.0", + "@codemirror/state": "6.6.0", + "@codemirror/view": "6.42.0", + "@deltablot/dropzone": "7.4.3", "@github/markdown-toolbar-element": "2.2.3", "@github/paste-markdown": "1.5.3", "@github/text-expander-element": "2.9.4", - "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@mermaid-js/layout-elk": "0.2.0", - "@primer/octicons": "19.22.0", + "@lezer/highlight": "1.2.3", + "@mcaptcha/vanilla-glue": "0.1.0-rc2", + "@mermaid-js/layout-elk": "0.2.1", + "@primer/octicons": "19.25.0", + "@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", - "@techknowlogick/license-checker-webpack-plugin": "0.3.0", - "add-asset-webpack-plugin": "3.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.15", + "codemirror-lang-elixir": "4.0.1", "colord": "2.9.3", "compare-versions": "6.1.1", "cropperjs": "1.6.2", - "css-loader": "7.1.4", - "dayjs": "1.11.19", - "dropzone": "6.0.0-beta.2", - "easymde": "2.20.0", - "esbuild-loader": "4.4.2", - "htmx.org": "2.0.8", + "dayjs": "1.11.20", + "easymde": "2.21.0", + "esbuild": "0.28.0", "idiomorph": "0.7.4", "jquery": "4.0.0", "js-yaml": "4.1.1", - "katex": "0.16.37", - "mermaid": "11.12.3", - "mini-css-extract-plugin": "2.10.0", - "monaco-editor": "0.55.1", - "monaco-editor-webpack-plugin": "7.1.1", + "katex": "0.16.45", + "mermaid": "11.15.0", "online-3d-viewer": "0.18.0", "pdfobject": "2.3.1", "perfect-debounce": "2.1.0", - "postcss": "8.5.8", - "postcss-loader": "8.2.1", + "postcss": "8.5.14", + "rolldown-license-plugin": "3.0.4", "sortablejs": "1.15.7", - "swagger-ui-dist": "5.32.0", - "tailwindcss": "3.4.17", + "swagger-ui-dist": "5.32.5", + "tailwindcss": "3.4.19", "throttle-debounce": "5.0.2", "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", - "vue": "3.5.29", + "vite": "8.0.10", + "vite-string-plugin": "2.0.4", + "vue": "3.5.34", "vue-bar-graph": "2.2.0", - "vue-chartjs": "5.3.3", - "vue-loader": "17.4.2", - "webpack": "5.105.4", - "webpack-cli": "6.0.1", - "wrap-ansi": "10.0.0" + "vue-chartjs": "5.3.3" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.7.1", - "@eslint/json": "1.1.0", - "@playwright/test": "1.58.2", + "@eslint/json": "1.2.0", + "@playwright/test": "1.59.1", "@stylistic/eslint-plugin": "5.10.0", - "@stylistic/stylelint-plugin": "5.0.1", + "@stylistic/stylelint-plugin": "5.1.0", "@types/codemirror": "5.60.17", - "@types/dropzone": "5.7.9", "@types/jquery": "4.0.0", "@types/js-yaml": "4.0.9", "@types/katex": "0.16.8", - "@types/node": "25.3.5", + "@types/node": "25.6.0", "@types/pdfobject": "2.2.5", "@types/sortablejs": "1.15.9", "@types/swagger-ui-dist": "3.30.6", "@types/throttle-debounce": "5.0.2", "@types/toastify-js": "1.12.4", - "@typescript-eslint/parser": "8.57.1", - "@vitejs/plugin-vue": "6.0.4", - "@vitest/eslint-plugin": "1.6.12", - "eslint": "10.0.3", + "@typescript-eslint/parser": "8.59.2", + "@vitejs/plugin-vue": "6.0.6", + "@vitest/eslint-plugin": "1.6.16", + "eslint": "10.3.0", "eslint-import-resolver-typescript": "4.4.4", "eslint-plugin-array-func": "5.1.1", - "eslint-plugin-github": "6.0.0", "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-unicorn": "63.0.0", - "eslint-plugin-vue": "10.8.0", + "eslint-plugin-sonarjs": "4.0.3", + "eslint-plugin-unicorn": "64.0.0", + "eslint-plugin-vue": "10.9.1", "eslint-plugin-vue-scoped-css": "3.0.0", "eslint-plugin-wc": "3.1.0", - "globals": "17.4.0", - "happy-dom": "20.8.3", - "jiti": "2.6.1", + "globals": "17.6.0", + "happy-dom": "20.9.0", + "jiti": "2.7.0", "markdownlint-cli": "0.48.0", - "material-icon-theme": "5.32.0", + "material-icon-theme": "5.34.0", "nolyfill": "1.0.44", "postcss-html": "1.8.1", - "spectral-cli-bundle": "1.0.7", - "stylelint": "17.4.0", + "spectral-cli-bundle": "1.0.8", + "stylelint": "17.11.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": "5.9.3", - "typescript-eslint": "8.57.1", - "updates": "17.8.3", - "vite-string-plugin": "2.0.1", - "vitest": "4.0.18", - "vue-tsc": "3.2.5" - }, - "pnpm": { - "peerDependencyRules": { - "allowedVersions": { - "eslint-plugin-github>eslint": ">=9" - } - }, - "overrides": { - "array-includes": "npm:@nolyfill/array-includes@^1", - "array.prototype.findlastindex": "npm:@nolyfill/array.prototype.findlastindex@^1", - "array.prototype.flat": "npm:@nolyfill/array.prototype.flat@^1", - "array.prototype.flatmap": "npm:@nolyfill/array.prototype.flatmap@^1", - "es-aggregate-error": "npm:@nolyfill/es-aggregate-error@^1", - "hasown": "npm:@nolyfill/hasown@^1", - "is-core-module": "npm:@nolyfill/is-core-module@^1", - "object.assign": "npm:@nolyfill/object.assign@^1", - "object.fromentries": "npm:@nolyfill/object.fromentries@^1", - "object.groupby": "npm:@nolyfill/object.groupby@^1", - "object.values": "npm:@nolyfill/object.values@^1", - "safe-buffer": "npm:@nolyfill/safe-buffer@^1", - "safe-regex-test": "npm:@nolyfill/safe-regex-test@^1", - "safer-buffer": "npm:@nolyfill/safer-buffer@^1", - "string.prototype.includes": "npm:@nolyfill/string.prototype.includes@^1", - "string.prototype.trimend": "npm:@nolyfill/string.prototype.trimend@^1" - } + "typescript": "6.0.3", + "typescript-eslint": "8.59.2", + "updates": "17.16.9", + "vitest": "4.1.5", + "vue-tsc": "3.2.8" } } diff --git a/playwright.config.ts b/playwright.config.ts index f566054f78..9dc8a7c1b5 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,21 +1,26 @@ import {env} from 'node:process'; import {defineConfig, devices} from '@playwright/test'; +const timeoutFactor = Number(env.GITEA_TEST_E2E_TIMEOUT_FACTOR) || 1; +const timeout = 5000 * timeoutFactor; + export default defineConfig({ + workers: '50%', + fullyParallel: true, testDir: './tests/e2e/', outputDir: './tests/e2e-output/', testMatch: /.*\.test\.ts/, forbidOnly: Boolean(env.CI), reporter: 'list', - timeout: env.CI ? 12000 : 6000, + timeout: 2 * timeout, expect: { - timeout: env.CI ? 6000 : 3000, + timeout, }, use: { - baseURL: env.GITEA_TEST_E2E_URL?.replace?.(/\/$/g, ''), + baseURL: env.GITEA_TEST_E2E_URL?.replace?.(/\/$/, ''), locale: 'en-US', - actionTimeout: env.CI ? 6000 : 3000, - navigationTimeout: env.CI ? 12000 : 6000, + actionTimeout: timeout, + navigationTimeout: 2 * timeout, }, projects: [ { @@ -25,11 +30,11 @@ export default defineConfig({ permissions: ['clipboard-read', 'clipboard-write'], }, }, - ...env.CI ? [{ + { name: 'firefox', use: { ...devices['Desktop Firefox'], }, - }] : [], + }, ], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bb595bf0e..7647b7a77e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,10 @@ overrides: safer-buffer: npm:@nolyfill/safer-buffer@^1 string.prototype.includes: npm:@nolyfill/string.prototype.includes@^1 string.prototype.trimend: npm:@nolyfill/string.prototype.trimend@^1 + object-keys: npm:@nolyfill/object-keys@^1 + object.entries: npm:@nolyfill/object.entries@^1 + abab: npm:@nolyfill/abab@^1 + es-set-tostringtag: npm:@nolyfill/es-set-tostringtag@^1 importers: @@ -38,6 +42,42 @@ importers: '@citation-js/plugin-software-formats': specifier: 0.6.2 version: 0.6.2 + '@codemirror/autocomplete': + specifier: 6.20.2 + version: 6.20.2 + '@codemirror/commands': + specifier: 6.10.3 + version: 6.10.3 + '@codemirror/lang-json': + specifier: 6.0.2 + version: 6.0.2 + '@codemirror/lang-markdown': + specifier: 6.5.0 + version: 6.5.0 + '@codemirror/language': + specifier: 6.12.3 + version: 6.12.3 + '@codemirror/language-data': + specifier: 6.5.2 + version: 6.5.2 + '@codemirror/legacy-modes': + specifier: 6.5.2 + version: 6.5.2 + '@codemirror/lint': + specifier: 6.9.6 + version: 6.9.6 + '@codemirror/search': + specifier: 6.7.0 + version: 6.7.0 + '@codemirror/state': + specifier: 6.6.0 + version: 6.6.0 + '@codemirror/view': + specifier: 6.42.0 + version: 6.42.0 + '@deltablot/dropzone': + specifier: 7.4.3 + version: 7.4.3 '@github/markdown-toolbar-element': specifier: 2.2.3 version: 2.2.3 @@ -47,27 +87,36 @@ importers: '@github/text-expander-element': specifier: 2.9.4 version: 2.9.4 + '@lezer/highlight': + specifier: 1.2.3 + version: 1.2.3 '@mcaptcha/vanilla-glue': - specifier: 0.1.0-alpha-3 - version: 0.1.0-alpha-3 + specifier: 0.1.0-rc2 + version: 0.1.0-rc2 '@mermaid-js/layout-elk': - specifier: 0.2.0 - version: 0.2.0(mermaid@11.12.3) + specifier: 0.2.1 + version: 0.2.1(mermaid@11.15.0) '@primer/octicons': - specifier: 19.22.0 - version: 19.22.0 + specifier: 19.25.0 + version: 19.25.0 + '@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.42.0) + '@replit/codemirror-lang-nix': + specifier: 6.0.1 + version: 6.0.1(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.42.0)(@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.2)(@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.42.0)(@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.2)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.42.0) '@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.29(typescript@5.9.3)) - '@techknowlogick/license-checker-webpack-plugin': - specifier: 0.3.0 - version: 0.3.0(webpack@5.105.4) - add-asset-webpack-plugin: - specifier: 3.1.1 - version: 3.1.1(webpack@5.105.4) + '@vitejs/plugin-vue': + specifier: 6.0.6 + version: 6.0.6(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.34(typescript@6.0.3)) ansi_up: specifier: 6.0.6 version: 6.0.6 @@ -79,13 +128,16 @@ importers: version: 4.5.1 chartjs-adapter-dayjs-4: specifier: 1.0.4 - version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.19) + version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.20) chartjs-plugin-zoom: 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.15 + version: 4.1.15 + codemirror-lang-elixir: + specifier: 4.0.1 + version: 4.0.1 colord: specifier: 2.9.3 version: 2.9.3 @@ -95,24 +147,15 @@ importers: cropperjs: specifier: 1.6.2 version: 1.6.2 - css-loader: - specifier: 7.1.4 - version: 7.1.4(webpack@5.105.4) dayjs: - specifier: 1.11.19 - version: 1.11.19 - dropzone: - specifier: 6.0.0-beta.2 - version: 6.0.0-beta.2 + specifier: 1.11.20 + version: 1.11.20 easymde: - specifier: 2.20.0 - version: 2.20.0 - esbuild-loader: - specifier: 4.4.2 - version: 4.4.2(webpack@5.105.4) - htmx.org: - specifier: 2.0.8 - version: 2.0.8 + specifier: 2.21.0 + version: 2.21.0 + esbuild: + specifier: 0.28.0 + version: 0.28.0 idiomorph: specifier: 0.7.4 version: 0.7.4 @@ -123,20 +166,11 @@ importers: specifier: 4.1.1 version: 4.1.1 katex: - specifier: 0.16.37 - version: 0.16.37 + specifier: 0.16.45 + version: 0.16.45 mermaid: - specifier: 11.12.3 - version: 11.12.3 - mini-css-extract-plugin: - specifier: 2.10.0 - version: 2.10.0(webpack@5.105.4) - monaco-editor: - specifier: 0.55.1 - version: 0.55.1 - monaco-editor-webpack-plugin: - specifier: 7.1.1 - version: 7.1.1(monaco-editor@0.55.1)(webpack@5.105.4) + specifier: 11.15.0 + version: 11.15.0 online-3d-viewer: specifier: 0.18.0 version: 0.18.0 @@ -147,20 +181,20 @@ importers: specifier: 2.1.0 version: 2.1.0 postcss: - specifier: 8.5.8 - version: 8.5.8 - postcss-loader: - specifier: 8.2.1 - version: 8.2.1(postcss@8.5.8)(typescript@5.9.3)(webpack@5.105.4) + specifier: 8.5.14 + version: 8.5.14 + rolldown-license-plugin: + specifier: 3.0.4 + version: 3.0.4(rolldown@1.0.0-rc.17) sortablejs: specifier: 1.15.7 version: 1.15.7 swagger-ui-dist: - specifier: 5.32.0 - version: 5.32.0 + specifier: 5.32.5 + version: 5.32.5 tailwindcss: - specifier: 3.4.17 - version: 3.4.17 + specifier: 3.4.19 + version: 3.4.19 throttle-debounce: specifier: 5.0.2 version: 5.0.2 @@ -179,49 +213,40 @@ importers: vanilla-colorful: specifier: 0.7.2 version: 0.7.2 + vite: + specifier: 8.0.10 + version: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0) + vite-string-plugin: + specifier: 2.0.4 + version: 2.0.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)) vue: - specifier: 3.5.29 - version: 3.5.29(typescript@5.9.3) + specifier: 3.5.34 + version: 3.5.34(typescript@6.0.3) vue-bar-graph: specifier: 2.2.0 - version: 2.2.0(typescript@5.9.3) + 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.29(typescript@5.9.3)) - vue-loader: - specifier: 17.4.2 - version: 17.4.2(vue@3.5.29(typescript@5.9.3))(webpack@5.105.4) - webpack: - specifier: 5.105.4 - version: 5.105.4(webpack-cli@6.0.1) - webpack-cli: - specifier: 6.0.1 - version: 6.0.1(webpack@5.105.4) - wrap-ansi: - specifier: 10.0.0 - version: 10.0.0 + version: 5.3.3(chart.js@4.5.1)(vue@3.5.34(typescript@6.0.3)) devDependencies: '@eslint-community/eslint-plugin-eslint-comments': specifier: 4.7.1 - version: 4.7.1(eslint@10.0.3(jiti@2.6.1)) + version: 4.7.1(eslint@10.3.0(jiti@2.7.0)) '@eslint/json': - specifier: 1.1.0 - version: 1.1.0 + specifier: 1.2.0 + version: 1.2.0 '@playwright/test': - specifier: 1.58.2 - version: 1.58.2 + specifier: 1.59.1 + version: 1.59.1 '@stylistic/eslint-plugin': specifier: 5.10.0 - version: 5.10.0(eslint@10.0.3(jiti@2.6.1)) + version: 5.10.0(eslint@10.3.0(jiti@2.7.0)) '@stylistic/stylelint-plugin': - specifier: 5.0.1 - version: 5.0.1(stylelint@17.4.0(typescript@5.9.3)) + specifier: 5.1.0 + version: 5.1.0(stylelint@17.11.0(typescript@6.0.3)) '@types/codemirror': specifier: 5.60.17 version: 5.60.17 - '@types/dropzone': - specifier: 5.7.9 - version: 5.7.9 '@types/jquery': specifier: 4.0.0 version: 4.0.0 @@ -232,8 +257,8 @@ importers: specifier: 0.16.8 version: 0.16.8 '@types/node': - specifier: 25.3.5 - version: 25.3.5 + specifier: 25.6.0 + version: 25.6.0 '@types/pdfobject': specifier: 2.2.5 version: 2.2.5 @@ -250,68 +275,65 @@ importers: specifier: 1.12.4 version: 1.12.4 '@typescript-eslint/parser': - specifier: 8.57.1 - version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@vitejs/plugin-vue': - specifier: 6.0.4 - version: 6.0.4(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) + specifier: 8.59.2 + version: 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) '@vitest/eslint-plugin': - specifier: 1.6.12 - version: 1.6.12(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + specifier: 1.6.16 + version: 1.6.16(@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.5(@types/node@25.6.0)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0))) eslint: - specifier: 10.0.3 - version: 10.0.3(jiti@2.6.1) + specifier: 10.3.0 + version: 10.3.0(jiti@2.7.0) eslint-import-resolver-typescript: specifier: 4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.0.3(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.3.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.3.0(jiti@2.7.0)) eslint-plugin-array-func: specifier: 5.1.1 - version: 5.1.1(eslint@10.0.3(jiti@2.6.1)) + version: 5.1.1(eslint@10.3.0(jiti@2.7.0)) eslint-plugin-de-morgan: specifier: 2.1.1 - version: 2.1.1(eslint@10.0.3(jiti@2.6.1)) + version: 2.1.1(eslint@10.3.0(jiti@2.7.0)) eslint-plugin-github: specifier: 6.0.0 - version: 6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) + version: 6.0.0(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.7.0)) eslint-plugin-import-x: specifier: 4.16.2 - version: 4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)) + version: 4.16.2(@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.3.0(jiti@2.7.0)) eslint-plugin-playwright: - specifier: 2.10.1 - version: 2.10.1(eslint@10.0.3(jiti@2.6.1)) + specifier: 2.10.2 + version: 2.10.2(eslint@10.3.0(jiti@2.7.0)) eslint-plugin-regexp: specifier: 3.1.0 - version: 3.1.0(eslint@10.0.3(jiti@2.6.1)) + version: 3.1.0(eslint@10.3.0(jiti@2.7.0)) eslint-plugin-sonarjs: - specifier: 4.0.2 - version: 4.0.2(eslint@10.0.3(jiti@2.6.1)) + specifier: 4.0.3 + version: 4.0.3(eslint@10.3.0(jiti@2.7.0)) eslint-plugin-unicorn: - specifier: 63.0.0 - version: 63.0.0(eslint@10.0.3(jiti@2.6.1)) + specifier: 64.0.0 + version: 64.0.0(eslint@10.3.0(jiti@2.7.0)) eslint-plugin-vue: - specifier: 10.8.0 - version: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.0.3(jiti@2.6.1)))(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))) + specifier: 10.9.1 + version: 10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.3.0(jiti@2.7.0)))(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.7.0))) eslint-plugin-vue-scoped-css: specifier: 3.0.0 - version: 3.0.0(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))) + version: 3.0.0(eslint@10.3.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.7.0))) eslint-plugin-wc: specifier: 3.1.0 - version: 3.1.0(eslint@10.0.3(jiti@2.6.1)) + version: 3.1.0(eslint@10.3.0(jiti@2.7.0)) globals: - specifier: 17.4.0 - version: 17.4.0 + specifier: 17.6.0 + version: 17.6.0 happy-dom: - specifier: 20.8.3 - version: 20.8.3 + specifier: 20.9.0 + version: 20.9.0 jiti: - specifier: 2.6.1 - version: 2.6.1 + specifier: 2.7.0 + version: 2.7.0 markdownlint-cli: specifier: 0.48.0 version: 0.48.0 material-icon-theme: - specifier: 5.32.0 - version: 5.32.0 + specifier: 5.34.0 + version: 5.34.0 nolyfill: specifier: 1.0.44 version: 1.0.44 @@ -319,44 +341,41 @@ importers: specifier: 1.8.1 version: 1.8.1 spectral-cli-bundle: - specifier: 1.0.7 - version: 1.0.7 + specifier: 1.0.8 + version: 1.0.8 stylelint: - specifier: 17.4.0 - version: 17.4.0(typescript@5.9.3) + specifier: 17.11.0 + version: 17.11.0(typescript@6.0.3) stylelint-config-recommended: specifier: 18.0.0 - version: 18.0.0(stylelint@17.4.0(typescript@5.9.3)) + version: 18.0.0(stylelint@17.11.0(typescript@6.0.3)) stylelint-declaration-block-no-ignored-properties: specifier: 3.0.0 - version: 3.0.0(stylelint@17.4.0(typescript@5.9.3)) + version: 3.0.0(stylelint@17.11.0(typescript@6.0.3)) stylelint-declaration-strict-value: specifier: 1.11.1 - version: 1.11.1(stylelint@17.4.0(typescript@5.9.3)) + version: 1.11.1(stylelint@17.11.0(typescript@6.0.3)) stylelint-value-no-unknown-custom-properties: specifier: 6.1.1 - version: 6.1.1(stylelint@17.4.0(typescript@5.9.3)) + version: 6.1.1(stylelint@17.11.0(typescript@6.0.3)) svgo: specifier: 4.0.1 version: 4.0.1 typescript: - specifier: 5.9.3 - version: 5.9.3 + specifier: 6.0.3 + version: 6.0.3 typescript-eslint: - specifier: 8.57.1 - version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.59.2 + version: 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) updates: - specifier: 17.8.3 - version: 17.8.3 - vite-string-plugin: - specifier: 2.0.1 - version: 2.0.1(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + specifier: 17.16.9 + version: 17.16.9 vitest: - specifier: 4.0.18 - version: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + specifier: 4.1.5 + version: 4.1.5(@types/node@25.6.0)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)) vue-tsc: - specifier: 3.2.5 - version: 3.2.5(typescript@5.9.3) + specifier: 3.2.8 + version: 3.2.8(typescript@6.0.3) packages: @@ -379,13 +398,13 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.28.6': - resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} '@babel/types@7.29.0': @@ -398,24 +417,12 @@ packages: '@cacheable/memory@2.0.8': resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==} - '@cacheable/utils@2.4.0': - resolution: {integrity: sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==} - - '@chevrotain/cst-dts-gen@11.1.2': - resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==} - - '@chevrotain/gast@11.1.2': - resolution: {integrity: sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==} - - '@chevrotain/regexp-to-ast@11.1.2': - resolution: {integrity: sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==} + '@cacheable/utils@2.4.1': + resolution: {integrity: sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==} '@chevrotain/types@11.1.2': resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} - '@chevrotain/utils@11.1.2': - resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==} - '@citation-js/core@0.7.21': resolution: {integrity: sha512-Vobv2/Yfnn6C6BVO/pvj7madQ7Mfzl83/jAWwixbemGF6ZThhGMz8++FD9hWHyHXDMYuLGa6fK68c2VsolZmTA==} engines: {node: '>=16.0.0'} @@ -464,8 +471,98 @@ packages: resolution: {integrity: sha512-3XQOO3u4WXY/7AWZyQ+9SuBzS8bYTlJ+NF1uCgrZO64g36nK5iIc5YV9cBl2TL2QhHF6S36nvAsXsj5fX9FeHw==} engines: {node: '>=14.0.0'} - '@csstools/css-calc@3.1.1': - resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} + '@codemirror/autocomplete@6.20.2': + resolution: {integrity: sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==} + + '@codemirror/commands@6.10.3': + resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} + + '@codemirror/lang-angular@0.1.4': + resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==} + + '@codemirror/lang-cpp@6.0.3': + resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-go@6.0.1': + resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==} + + '@codemirror/lang-html@6.4.11': + resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==} + + '@codemirror/lang-java@6.0.2': + resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==} + + '@codemirror/lang-javascript@6.2.5': + resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + + '@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==} + + '@codemirror/lang-less@6.0.2': + resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==} + + '@codemirror/lang-liquid@6.3.2': + resolution: {integrity: sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==} + + '@codemirror/lang-markdown@6.5.0': + resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==} + + '@codemirror/lang-php@6.0.2': + resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==} + + '@codemirror/lang-python@6.2.1': + resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} + + '@codemirror/lang-rust@6.0.2': + resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==} + + '@codemirror/lang-sass@6.0.2': + resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==} + + '@codemirror/lang-sql@6.10.0': + resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==} + + '@codemirror/lang-vue@0.1.3': + resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==} + + '@codemirror/lang-wast@6.0.2': + resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==} + + '@codemirror/lang-xml@6.1.0': + resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==} + + '@codemirror/lang-yaml@6.1.3': + resolution: {integrity: sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==} + + '@codemirror/language-data@6.5.2': + resolution: {integrity: sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==} + + '@codemirror/language@6.12.3': + resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==} + + '@codemirror/legacy-modes@6.5.2': + resolution: {integrity: sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==} + + '@codemirror/lint@6.9.6': + resolution: {integrity: sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==} + + '@codemirror/search@6.7.0': + resolution: {integrity: sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==} + + '@codemirror/state@6.6.0': + resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} + + '@codemirror/view@6.42.0': + resolution: {integrity: sha512-+PJEyndSCrsS2oLH3DfWoLBcF3xfeyGXtLnpXqHY01kL3TogyCLD12hNvSu73ww2KFftrx3Rd0nGOigbSkU3Hw==} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 @@ -477,8 +574,13 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.0': - resolution: {integrity: sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==} + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true '@csstools/css-tokenizer@4.0.0': resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} @@ -503,171 +605,170 @@ packages: peerDependencies: postcss-selector-parser: ^7.1.1 - '@discoveryjs/json-ext@0.6.3': - resolution: {integrity: sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==} - engines: {node: '>=14.17.0'} + '@deltablot/dropzone@7.4.3': + resolution: {integrity: sha512-qTj4KEalPcYrocazuKYfhAS3hfgAu17KXw0DkpUj71Aw9E1/x5vTSNK5hvIJLkbpt/pBECU18JkjA3yguONcPA==} - '@emnapi/core@1.8.1': - resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -697,42 +798,46 @@ packages: eslint: optional: true - '@eslint/config-array@0.23.3': - resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==} + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.5.3': - resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} + '@eslint/config-helpers@0.5.5': + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/core@0.17.0': resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@1.1.1': - resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.3.4': - resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.3': - resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==} + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/json@1.1.0': - resolution: {integrity: sha512-noH9FUYqyhZSDf3Yq5HswsjDH/MWJAatMooWwT5YgQ0XHMekoFc/iyEufP+7kD1kaOj9qwFiXySqHsKii3zmlw==} + '@eslint/json@1.2.0': + resolution: {integrity: sha512-CEFEyNgvzu8zn5QwVYDg3FaG+ZKUeUsNYitFpMYJAqoAlnw68EQgNbUfheSmexZr4n0wZPrAkPLuvsLaXO6wRw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/object-schema@3.0.3': - resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/plugin-kit@0.6.1': resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@github/browserslist-config@1.0.0': resolution: {integrity: sha512-gIhjdJp/c2beaIWWIlsXdqXVRUz3r2BxBCpfz/F3JXHvSAQ1paMYjLH+maEATtENg+k5eLV7gA+9yPp762ieuw==} @@ -748,12 +853,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': @@ -771,8 +880,24 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - '@iconify/utils@3.1.0': - resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + '@iconify/utils@3.1.1': + resolution: {integrity: sha512-MwzoDtw9rO1x+qfgLTV/IVXsHDBqeYZoMIQC8SfxfYSlaSUG+oWiAcoiB1yajAda6mqblm4/1/w2E8tRu7a7Tw==} + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -781,9 +906,6 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.11': - resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -802,23 +924,83 @@ packages: '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} - '@mcaptcha/core-glue@0.1.0-alpha-5': - resolution: {integrity: sha512-16qWm5O5X0Y9LXULULaAks8Vf9FNlUUBcR5KDt49aWhFhG5++JzxNmCwQM9EJSHNU7y0U+FdyAWcGmjfKlkRLA==} + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} - '@mcaptcha/vanilla-glue@0.1.0-alpha-3': - resolution: {integrity: sha512-GT6TJBgmViGXcXiT5VOr+h/6iOnThSlZuCoOWncubyTZU9R3cgU5vWPkF7G6Ob6ee2CBe3yqBxxk24CFVGTVXw==} + '@lezer/cpp@1.1.5': + resolution: {integrity: sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==} - '@mermaid-js/layout-elk@0.2.0': - resolution: {integrity: sha512-vjjYGnCCjYlIA/rR7M//eFi0rHM6dsMyN1JQKfckpt30DTC/esrw36hcrvA2FNPHaqh3Q/SyBWzddyaky8EtUQ==} + '@lezer/css@1.3.3': + resolution: {integrity: sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==} + + '@lezer/go@1.0.1': + resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/html@1.3.13': + resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==} + + '@lezer/java@1.1.3': + resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@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==} + + '@lezer/php@1.0.5': + resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==} + + '@lezer/python@1.1.18': + resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==} + + '@lezer/rust@1.0.2': + resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} + + '@lezer/sass@1.1.0': + resolution: {integrity: sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==} + + '@lezer/xml@1.0.6': + resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==} + + '@lezer/yaml@1.0.4': + resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + + '@mcaptcha/core-glue@0.1.0-rc1': + resolution: {integrity: sha512-P4SgUioJDR38QpnP9sPY72NyaYex8MXD6RbzrfKra+ngamT26XjqVZEHBiZU2RT7u0SsWhuko4N1ntNOghsgpg==} + + '@mcaptcha/vanilla-glue@0.1.0-rc2': + resolution: {integrity: sha512-LDjn9lrKioJ3zwaQOfql7PXsnxCAHg7b1rPw7G0OxpvVE7xLB/a40SHfIIiocce2VS9TPI4MbcKm5pcuy8fU5g==} + + '@mermaid-js/layout-elk@0.2.1': + resolution: {integrity: sha512-MX9jwhMyd5zDcFsYcl3duDUkKhjVRUCGEQrdCeNV5hCIR6+3FuDDbRbFmvVbAu15K1+juzsYGG+K8MDvCY1Amg==} peerDependencies: mermaid: ^11.0.2 - '@mermaid-js/parser@1.0.0': - resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==} + '@mermaid-js/parser@1.1.1': + resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@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 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -831,6 +1013,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@nolyfill/abab@1.0.44': + resolution: {integrity: sha512-ZQFi9RSKHKnXM4lLtazYx7otLcqEpQ6sSzs7yzb6zlcimyFIv3CV4s2G+CaA3GnPDTmUFugCn2wWap4jynGQow==} + engines: {node: '>=12.4.0'} + '@nolyfill/array-includes@1.0.44': resolution: {integrity: sha512-IVEqpEgFbLaU0hUoMwJYXNSdi6lq+FxHdxd8xTKDLxh8k6u5YNGz4Bo6bT46l7p0x8PbJmHViBtngqhvE528fA==} engines: {node: '>=12.4.0'} @@ -847,6 +1033,10 @@ packages: resolution: {integrity: sha512-P6OsaEUrpBJ9NdNekFDQVM9LOFHPDKSJzwOWRBaC6LqREX+4lkZT2Q+to78R6aG6atuOQsxBVqPjMGCKjWdvyQ==} engines: {node: '>=12.4.0'} + '@nolyfill/es-set-tostringtag@1.0.44': + resolution: {integrity: sha512-Qfiv/3wI+mKSPCgU8Fg/7Auu4os00St4GwyLqCCZ/oBD4W00itWUkl9Rq1MCGS+VXYDnTobvrc6AvPagwnT2pg==} + engines: {node: '>=12.4.0'} + '@nolyfill/hasown@1.0.44': resolution: {integrity: sha512-GA/21lkTr2PAQuT6jGnhLuBD5IFd/AEhBXJ/tf33+/bVxPxg+5ejKx9jGQGnyV/P0eSmdup5E+s8b2HL6lOrwQ==} engines: {node: '>=12.4.0'} @@ -855,10 +1045,18 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@nolyfill/object-keys@1.0.44': + resolution: {integrity: sha512-k59MxUv8AFPd+GzZlGBhBaW6zu6t7f2euhctz7vdT3WsE3LVPANgE7uAN71IIpxaIn+bBEX1Xesf/rTwWqVO0Q==} + engines: {node: '>=12.4.0'} + '@nolyfill/object.assign@1.0.44': resolution: {integrity: sha512-cZoXq09YZXDgkxRMAP/TTb3kAsWm7p5OyBugWDe4fOfxf0XRI55mgDSkuyq41sV1qW1zVC5aSsKEh1hQo1KOvA==} engines: {node: '>=12.4.0'} + '@nolyfill/object.entries@1.0.44': + resolution: {integrity: sha512-RCxO6EH9YbvxQWGYLKOd7MjNi7vKzPkXv1VDWNsy1C8BksQxXNPQrddlu3INi1O2fexk82WXpCCeaCtpU/y21w==} + engines: {node: '>=12.4.0'} + '@nolyfill/object.fromentries@1.0.44': resolution: {integrity: sha512-/LrsCtpLmByZ6GwP/NeXULSgMyNsVr5d6FlgQy1HZatAiBc8c+WZ1VmFkK19ZLXCNNXBedXDultrp0x4Nz+QQw==} engines: {node: '>=12.4.0'} @@ -890,6 +1088,9 @@ packages: resolution: {integrity: sha512-3dsKlf4Ma7o+uxLIg5OI1Tgwfet2pE8WTbPjEGWvOe6CSjMtK0skJnnSVHaEVX4N4mYU81To0qDeZOPqjaUotg==} engines: {node: '>=12.4.0'} + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@package-json/types@0.0.12': resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} @@ -897,161 +1098,165 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.58.2': - resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} engines: {node: '>=18'} hasBin: true '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@primer/octicons@19.22.0': - resolution: {integrity: sha512-nWoh9PlE6u7xbiZF3KcUm3ktLpN2rQPt11trwp/t4EsKuYRNVWVbBp1LkCBsvZq7ScckNKUURLigIU0wS1FQdw==} + '@primer/octicons@19.25.0': + resolution: {integrity: sha512-E0eMV8nXexrs7Vro7PdS8v/JfvvYCMh8HN6CXJ9l8fk9atZaY05fVUcyiAh5KjEJu7IxdFy4URfHGpM7+iOl1A==} + + '@replit/codemirror-indentation-markers@6.5.3': + resolution: {integrity: sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==} + peerDependencies: + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + + '@replit/codemirror-lang-nix@6.0.1': + resolution: {integrity: sha512-lvzjoYn9nfJzBD5qdm3Ut6G3+Or2wEacYIDJ49h9+19WSChVnxv4ojf+rNmQ78ncuxIt/bfbMvDLMeMP0xze6g==} + peerDependencies: + '@codemirror/autocomplete': ^6.0.0 + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 + '@lezer/highlight': ^1.0.0 + '@lezer/lr': ^1.0.0 + + '@replit/codemirror-lang-svelte@6.0.0': + resolution: {integrity: sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==} + peerDependencies: + '@codemirror/autocomplete': ^6.0.0 + '@codemirror/lang-css': ^6.0.1 + '@codemirror/lang-html': ^6.2.0 + '@codemirror/lang-javascript': ^6.1.1 + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 + '@lezer/highlight': ^1.0.0 + '@lezer/javascript': ^1.2.0 + '@lezer/lr': ^1.0.0 + + '@replit/codemirror-vscode-keymap@6.0.2': + resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==} + peerDependencies: + '@codemirror/autocomplete': ^6.0.0 + '@codemirror/commands': ^6.0.0 + '@codemirror/language': ^6.0.0 + '@codemirror/lint': ^6.0.0 + '@codemirror/search': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 '@resvg/resvg-wasm@2.6.2': resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} engines: {node: '>= 10'} - '@rolldown/pluginutils@1.0.0-rc.2': - resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} - - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} - cpu: [x64] - os: [win32] + '@rolldown/pluginutils@1.0.0-rc.13': + resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==} + + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1059,20 +1264,22 @@ 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==} + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@solid-primitives/refs@1.1.3': resolution: {integrity: sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA==} peerDependencies: @@ -1097,22 +1304,21 @@ packages: peerDependencies: eslint: ^9.0.0 || ^10.0.0 - '@stylistic/stylelint-plugin@5.0.1': - resolution: {integrity: sha512-NaVwCNVZ2LyPA3TnUwvjO9c6P6VUjgRB8UP8SOW+cAOJBVqPPuOIDawsvvtql/LhkuR3JuTdGvr/RM3dUl8l2Q==} + '@stylistic/stylelint-plugin@5.1.0': + resolution: {integrity: sha512-TFvKCbJUEWUYCD+rDv45qhnStO6nRtbBngaCblS2JGh8c95S3jJi3fIotfF6EDo4IVM15UPa65WP+kp6GNvXRA==} engines: {node: '>=20.19.0'} peerDependencies: - stylelint: ^17.0.0 + stylelint: ^17.6.0 - '@swc/helpers@0.2.14': - resolution: {integrity: sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA==} + '@swc/helpers@0.5.21': + resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} - '@techknowlogick/license-checker-webpack-plugin@0.3.0': - resolution: {integrity: sha512-gqht/3IzjYttWGwVO5L+oPiQaO0SrPzpZCy/XGEcwTY5fpKs959+YhOHMiltJkLEfae60tE6s2jeOsxF547/sA==} - peerDependencies: - webpack: ^4.4.0 || ^5.4.0 + '@tootallnate/once@2.0.1': + resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} + engines: {node: '>= 10'} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1213,21 +1419,12 @@ packages: '@types/d3@7.4.3': resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/dropzone@5.7.9': - resolution: {integrity: sha512-c6IlUz+DeQ4gANzJKn8fdP5rO6UyDNOyWLjfPbDRUHCNsXaAVKQOpuOv6LWEyvaK7pLqmoIpvSIlvBgGsk1vGw==} - - '@types/eslint-scope@3.7.7': - resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - - '@types/eslint@9.6.1': - resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} - '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -1240,12 +1437,24 @@ packages: '@types/hammerjs@2.0.46': resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/jquery@4.0.0': resolution: {integrity: sha512-Z+to+A2VkaHq1DfI2oSwsoCdhCHMpTSgjWzNcbNlRGYzksDBpPUgEcAL+RQjOBJRaLoEAOHXxqDGBVP+BblBwg==} '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/jsdom@20.0.1': + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1261,8 +1470,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@25.3.5': - resolution: {integrity: sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} '@types/pdfobject@2.2.5': resolution: {integrity: sha512-7gD5tqc/RUDq0PyoLemL0vEHxBYi+zY0WVaFAx/Y0jBsXFgot1vB9No1GhDZGwRGJMCIZbgAb74QG9MTyTNU/g==} @@ -1270,6 +1479,9 @@ packages: '@types/sortablejs@1.15.9': resolution: {integrity: sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==} + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/swagger-ui-dist@3.30.6': resolution: {integrity: sha512-FVxN7wjLYRtJsZBscOcOcf8oR++m38vbUFjT33Mr9HBuasX9bRDrJsp7iwixcOtKSHEEa2B7o2+4wEiXqC+Ebw==} @@ -1282,6 +1494,9 @@ packages: '@types/toastify-js@1.12.4': resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1294,115 +1509,69 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.56.1': - resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@typescript-eslint/eslint-plugin@8.59.2': + resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.56.1 + '@typescript-eslint/parser': ^8.59.2 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/eslint-plugin@8.57.1': - resolution: {integrity: sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.57.1 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/parser@8.57.1': - resolution: {integrity: sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==} + '@typescript-eslint/parser@8.59.2': + resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==} 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.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.56.1': - resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + '@typescript-eslint/project-service@8.59.2': + resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.57.1': - resolution: {integrity: sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==} + '@typescript-eslint/scope-manager@8.59.2': + resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.2': + resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.56.1': - resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/scope-manager@8.57.1': - resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.56.1': - resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/tsconfig-utils@8.57.1': - resolution: {integrity: sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/type-utils@8.56.1': - resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + '@typescript-eslint/type-utils@8.59.2': + resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==} 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.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.57.1': - resolution: {integrity: sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==} + '@typescript-eslint/types@8.59.2': + resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.2': + resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.2': + resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} 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.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.56.1': - resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/types@8.57.1': - resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.56.1': - resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/typescript-estree@8.57.1': - resolution: {integrity: sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.56.1': - resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} - 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.0.0' - - '@typescript-eslint/utils@8.57.1': - resolution: {integrity: sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==} - 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.0.0' - - '@typescript-eslint/visitor-keys@8.56.1': - resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.57.1': - resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==} + '@typescript-eslint/visitor-keys@8.59.2': + resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -1508,54 +1677,60 @@ packages: cpu: [x64] os: [win32] - '@vitejs/plugin-vue@6.0.4': - resolution: {integrity: sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==} + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + + '@vitejs/plugin-vue@6.0.6': + resolution: {integrity: sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 - '@vitest/eslint-plugin@1.6.12': - resolution: {integrity: sha512-4kI47BJNFE+EQ5bmPbHzBF+ibNzx2Fj0Jo9xhWsTPxMddlHwIWl6YAxagefh461hrwx/W0QwBZpxGS404kBXyg==} + '@vitest/eslint-plugin@1.6.16': + resolution: {integrity: sha512-2pBN1F1JXq6zTSaYC58CMJa7pGxXIRsLfOioeZM4cPE3pRdSh1ySTSoHPQlOTEF5WgoVzWZQxhGQ3ygT78hOVg==} engines: {node: '>=18'} peerDependencies: + '@typescript-eslint/eslint-plugin': '*' eslint: '>=8.57.0' typescript: '>=5.0.0' vitest: '*' peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true typescript: optional: true vitest: optional: true - '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} - '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.0.18': - resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} - '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} - '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} - '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} - '@vitest/utils@4.0.18': - resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} '@volar/language-core@2.4.28': resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} @@ -1566,157 +1741,64 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} - '@vue/compiler-core@3.5.29': - resolution: {integrity: sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==} + '@vue/compiler-core@3.5.34': + resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==} - '@vue/compiler-dom@3.5.29': - resolution: {integrity: sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==} + '@vue/compiler-dom@3.5.34': + resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==} - '@vue/compiler-sfc@3.5.29': - resolution: {integrity: sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==} + '@vue/compiler-sfc@3.5.34': + resolution: {integrity: sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==} - '@vue/compiler-ssr@3.5.29': - resolution: {integrity: sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==} + '@vue/compiler-ssr@3.5.34': + resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==} - '@vue/language-core@3.2.5': - resolution: {integrity: sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==} + '@vue/language-core@3.2.8': + resolution: {integrity: sha512-9OiSPQFiAAWNVnXb0d2dcTmcKnFQamhuNES6ayyISrb/mwPWVgoGdAqSfCWqKhQpa3D5gDTcYD+w7ObiheZ81g==} - '@vue/reactivity@3.5.29': - resolution: {integrity: sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==} + '@vue/reactivity@3.5.34': + resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==} - '@vue/runtime-core@3.5.29': - resolution: {integrity: sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==} + '@vue/runtime-core@3.5.34': + resolution: {integrity: sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==} - '@vue/runtime-dom@3.5.29': - resolution: {integrity: sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==} + '@vue/runtime-dom@3.5.34': + resolution: {integrity: sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==} - '@vue/server-renderer@3.5.29': - resolution: {integrity: sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==} + '@vue/server-renderer@3.5.34': + resolution: {integrity: sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==} peerDependencies: - vue: 3.5.29 + vue: 3.5.34 - '@vue/shared@3.5.29': - resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} + '@vue/shared@3.5.34': + resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} - '@webassemblyjs/ast@1.14.1': - resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - - '@webassemblyjs/floating-point-hex-parser@1.13.2': - resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - - '@webassemblyjs/helper-api-error@1.13.2': - resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - - '@webassemblyjs/helper-buffer@1.14.1': - resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - - '@webassemblyjs/helper-numbers@1.13.2': - resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': - resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - - '@webassemblyjs/helper-wasm-section@1.14.1': - resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - - '@webassemblyjs/ieee754@1.13.2': - resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - - '@webassemblyjs/leb128@1.13.2': - resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - - '@webassemblyjs/utf8@1.13.2': - resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - - '@webassemblyjs/wasm-edit@1.14.1': - resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - - '@webassemblyjs/wasm-gen@1.14.1': - resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - - '@webassemblyjs/wasm-opt@1.14.1': - resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - - '@webassemblyjs/wasm-parser@1.14.1': - resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - - '@webassemblyjs/wast-printer@1.14.1': - resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - - '@webpack-cli/configtest@3.0.1': - resolution: {integrity: sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==} - engines: {node: '>=18.12.0'} - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - - '@webpack-cli/info@3.0.1': - resolution: {integrity: sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==} - engines: {node: '>=18.12.0'} - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - - '@webpack-cli/serve@3.0.1': - resolution: {integrity: sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==} - engines: {node: '>=18.12.0'} - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - webpack-dev-server: '*' - peerDependenciesMeta: - webpack-dev-server: - optional: true - - '@xtuc/ieee754@1.2.0': - resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - - '@xtuc/long@4.2.2': - resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - - acorn-import-phases@1.0.4: - resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} - engines: {node: '>=10.13.0'} - peerDependencies: - acorn: ^8.14.0 + acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true - add-asset-webpack-plugin@3.1.1: - resolution: {integrity: sha512-0WexE8uFq2hkC/rc+zVY8Hf5cKj/UwuBa0GSDKfCiKh6rrw/e7PYDgdxz1syHXIthTRlgt5q2vLvBIWcWtAvIQ==} - engines: {node: '>=18'} - peerDependencies: - webpack: '>=5' - peerDependenciesMeta: - webpack: - optional: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - ajv-keywords@5.1.0: - resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} - peerDependencies: - ajv: ^8.8.2 - - ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} alien-signals@3.1.2: resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} @@ -1733,9 +1815,9 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} ansi_up@6.0.6: resolution: {integrity: sha512-yIa1x3Ecf8jWP4UWEunNjqNX6gzE4vg2gGz+xqRGY+TBSucnYp6RRdPV4brmtg6bQ1ljD48mZ5iGSEj7QEpRKA==} @@ -1757,10 +1839,6 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} - array-find-index@1.0.2: - resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} - engines: {node: '>=0.10.0'} - asciinema-player@3.15.1: resolution: {integrity: sha512-agVYeNlPxthLyAb92l9AS7ypW0uhesqOuQzyR58Q4Sj+MvesQztZBgx86lHqNJkB8rQ6EP0LeA9czGytQUBpYw==} @@ -1775,8 +1853,11 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} - axe-core@4.11.1: - resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axe-core@4.11.4: + resolution: {integrity: sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==} engines: {node: '>=4'} axobject-query@4.1.0: @@ -1793,14 +1874,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.0: - resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + baseline-browser-mapping@2.10.27: + resolution: {integrity: sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==} engines: {node: '>=6.0.0'} hasBin: true - big.js@5.2.2: - resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1808,25 +1886,22 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -1834,16 +1909,16 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} - builtin-modules@5.0.0: - resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} + builtin-modules@5.1.0: + resolution: {integrity: sha512-c5JxaDrzwRjq3WyJkI1AGR5xy6Gr6udlt7sQPbl09+3ckB+Zo2qqQ2KhCTBr7Q8dHB43bENGYEk4xddrFH/b7A==} engines: {node: '>=18.20'} bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cacheable@2.3.3: - resolution: {integrity: sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==} + cacheable@2.3.4: + resolution: {integrity: sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==} callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -1853,8 +1928,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001777: - resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + caniuse-lite@1.0.30001791: + resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} @@ -1892,14 +1967,6 @@ packages: peerDependencies: chart.js: '>=3.2.0' - chevrotain-allstar@0.3.1: - resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} - peerDependencies: - chevrotain: ^11.0.0 - - chevrotain@11.1.2: - resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==} - chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1907,9 +1974,9 @@ packages: chroma-js@3.2.0: resolution: {integrity: sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==} - chrome-trace-event@1.0.4: - resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} - engines: {node: '>=6.0'} + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} ci-info@4.4.0: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} @@ -1922,12 +1989,11 @@ 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.15: + resolution: {integrity: sha512-K4z5MF32z7Gr1fUcfuUZvmrzpdNs5q8zAm2yqsWhA8mZ9Q6GL4HNdR2k3h3ubEEEGoyb8fvMA48LDr2MKYDASA==} - clone-deep@4.0.1: - resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} - engines: {node: '>=6'} + codemirror-lang-elixir@4.0.1: + resolution: {integrity: sha512-z6W/XB4b7TZrp9EZYBGVq93vQfvKbff+1iM8YZaVErL0dguBAeLmVRlEv1NuDZHOP1qjJ3NwyibkUkNWn7q9VQ==} codemirror-spell-checker@1.1.2: resolution: {integrity: sha512-2Tl6n0v+GJRsC9K3MLCdLaMOmvWL0uukajNJseorZJsslaxZyZMgENocPU8R0DyoTAiKsyqiemSOZo7kjGV0LQ==} @@ -1945,24 +2011,18 @@ packages: colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} - colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} - commander@12.1.0: - resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} - engines: {node: '>=18'} - commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1975,8 +2035,8 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - comment-parser@1.4.5: - resolution: {integrity: sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==} + comment-parser@1.4.6: + resolution: {integrity: sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==} engines: {node: '>= 12.0.0'} compare-versions@6.1.1: @@ -1988,8 +2048,11 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - core-js-compat@3.48.0: - resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} core-js@3.32.2: resolution: {integrity: sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==} @@ -2009,6 +2072,9 @@ packages: typescript: optional: true + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cropperjs@1.6.2: resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==} @@ -2020,18 +2086,6 @@ packages: resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==} engines: {node: '>=12'} - css-loader@7.1.4: - resolution: {integrity: sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==} - engines: {node: '>= 18.12.0'} - peerDependencies: - '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 - webpack: ^5.27.0 - peerDependenciesMeta: - '@rspack/core': - optional: true - webpack: - optional: true - css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} @@ -2056,6 +2110,16 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + + cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -2069,8 +2133,8 @@ packages: peerDependencies: cytoscape: ^3.2.0 - cytoscape@3.33.1: - resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} + cytoscape@3.33.3: + resolution: {integrity: sha512-Gej7U+OKR+LZ8kvX7rb2HhCYJ0IhvEFsnkud4SB1PR+BUY/TsSO0dmOW59WEVLu51b1Rm+gQRKoz4bLYxGSZ2g==} engines: {node: '>=0.10'} d3-array@2.12.1: @@ -2212,14 +2276,18 @@ packages: resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} engines: {node: '>=12'} - dagre-d3-es@7.0.13: - resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - dayjs@1.11.19: - resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -2238,6 +2306,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -2252,13 +2323,21 @@ packages: resolution: {integrity: sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==} engines: {node: '>=0.10.0'} - delaunator@5.0.1: - resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -2281,28 +2360,26 @@ packages: domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.2.7: - resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} - - dompurify@3.3.2: - resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==} - engines: {node: '>=20'} + dompurify@3.4.2: + resolution: {integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dropzone@6.0.0-beta.2: - resolution: {integrity: sha512-k44yLuFFhRk53M8zP71FaaNzJYIzr99SKmpbO/oZKNslDjNXQsBTdfLs+iONd0U0L94zzlFzRnFdqbLcs7h9fQ==} + easymde@2.21.0: + resolution: {integrity: sha512-5uE7I/DEN8gvGRwxaqAv7h1PMEK2ykNXVX5zL0dK3nCYROGja3AMbdQz8eCEELnfvCfy7tRkTmLuvyJG8uSWjQ==} - easymde@2.20.0: - resolution: {integrity: sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==} - - electron-to-chromium@1.5.307: - resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + electron-to-chromium@1.5.349: + resolution: {integrity: sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==} elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} @@ -2313,18 +2390,14 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - emojis-list@3.0.0: - resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} - engines: {node: '>= 4'} - - enhanced-resolve@5.20.0: - resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} - engines: {node: '>=10.13.0'} - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -2333,27 +2406,21 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} - envinfo@7.21.0: - resolution: {integrity: sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==} - engines: {node: '>=4'} - hasBin: true - error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} - es-module-lexer@2.0.0: - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - esbuild-loader@4.4.2: - resolution: {integrity: sha512-8LdoT9sC7fzfvhxhsIAiWhzLJr9yT3ggmckXxsgvM07wgrRxhuT98XhLn3E7VczU5W5AFsPKv9DdWcZIubbWkQ==} - peerDependencies: - webpack: ^4.40.0 || ^5.0.0 + es-toolkit@1.46.1: + resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} hasBin: true @@ -2365,10 +2432,19 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + eslint-config-prettier@10.1.8: resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true @@ -2384,8 +2460,8 @@ packages: unrs-resolver: optional: true - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} eslint-import-resolver-typescript@4.4.4: resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} @@ -2489,12 +2565,12 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-no-only-tests@3.3.0: - resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==} + eslint-plugin-no-only-tests@3.4.0: + resolution: {integrity: sha512-4S3/9Nb7A2tiMcpzEQE9bQSlpeOz6WJkgryBuou/SA8W2x2c8Zf4j0NvTKBjv6qNhF9T79tmkecm/0CHqV0UGg==} 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' @@ -2519,13 +2595,13 @@ 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 - eslint-plugin-unicorn@63.0.0: - resolution: {integrity: sha512-Iqecl9118uQEXYh7adylgEmGfkn5es3/mlQTLLkd4pXkIk9CTGrAbeUux+YljSa2ohXCBmQQ0+Ej1kZaFgcfkA==} + eslint-plugin-unicorn@64.0.0: + resolution: {integrity: sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==} engines: {node: ^20.10.0 || >=21.0.0} peerDependencies: eslint: '>=9.38.0' @@ -2544,14 +2620,14 @@ packages: postcss-styl: optional: true - eslint-plugin-vue@10.8.0: - resolution: {integrity: sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==} + eslint-plugin-vue@10.9.1: + resolution: {integrity: sha512-cHB0Tf4Duvzwecwd/AqWzZvF/QszE13BhjVUpVXWCy9AeMR5GjkAjP3i85vqgLgOuTmkHR1OJ5oMeqLHtuw8zg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - vue-eslint-parser: ^10.0.0 + vue-eslint-parser: ^10.3.0 peerDependenciesMeta: '@stylistic/eslint-plugin': optional: true @@ -2567,14 +2643,6 @@ packages: resolution: {integrity: sha512-pWReu3fkohwyvztx/oQWWgld2iad25TfUdi6wvhhaDPIQjHU/pyvlKgXFw1kX31SQK2Nq9MH+vRDWB0ZLy8fYw==} engines: {node: '>=4.0.0'} - eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} - - eslint-scope@9.1.1: - resolution: {integrity: sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint-scope@9.1.2: resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -2591,8 +2659,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.0.3: - resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==} + eslint@10.3.0: + resolution: {integrity: sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2605,10 +2673,15 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@11.1.1: - resolution: {integrity: sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==} + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} @@ -2617,10 +2690,6 @@ packages: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} - estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -2702,10 +2771,6 @@ packages: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2714,18 +2779,15 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flat-cache@6.1.20: - resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} + flat-cache@6.1.22: + resolution: {integrity: sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==} - flat@5.0.2: - resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} - hasBin: true + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - flatted@3.3.4: - resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} @@ -2744,8 +2806,8 @@ packages: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2755,13 +2817,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} engines: {node: '>=6'} @@ -2778,12 +2833,12 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} - globals@17.4.0: - resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} engines: {node: '>=18'} - globby@16.1.1: - resolution: {integrity: sha512-dW7vl+yiAJSp6aCekaVnVJxurRv7DCOLyXqEG3RYMYUg7AuJ2jCqPkZTA8ooqC2vtnkaMcV5WfFBMuEnTu1OQg==} + globby@16.2.0: + resolution: {integrity: sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==} engines: {node: '>=20'} globjoin@0.1.4: @@ -2799,8 +2854,8 @@ packages: resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} engines: {node: '>=0.8.0'} - happy-dom@20.8.3: - resolution: {integrity: sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ==} + happy-dom@20.9.0: + resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -2811,16 +2866,20 @@ packages: resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} engines: {node: '>=12'} - hash-sum@2.0.0: - resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} - - hashery@1.5.0: - resolution: {integrity: sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==} + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + + html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + html-tags@5.1.0: resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==} engines: {node: '>=20.10'} @@ -2828,19 +2887,18 @@ packages: htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - htmx.org@2.0.8: - resolution: {integrity: sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==} + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - icss-utils@5.1.0: - resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - idiomorph@0.7.4: resolution: {integrity: sha512-uCdSpLo3uMfqOmrwXTpR1k/sq4sSmKC7l4o/LdJOEU+MMMq+wkevRqOQYn3lP7vfz9Mv+USBEqPvi0XhdL9ENw==} @@ -2859,11 +2917,6 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} - hasBin: true - import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -2875,13 +2928,6 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -2896,10 +2942,6 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - interpret@3.1.1: - resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} - engines: {node: '>=10.13.0'} - is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -2949,10 +2991,6 @@ packages: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} - is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} - is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -2966,20 +3004,33 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} + jest-environment-jsdom@29.7.0: + resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true - jest-worker@27.5.1: - resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} - engines: {node: '>= 10.13.0'} + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true jquery@4.0.0: @@ -2998,10 +3049,19 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsdoc-type-pratt-parser@7.1.1: - resolution: {integrity: sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA==} + jsdoc-type-pratt-parser@7.2.0: + resolution: {integrity: sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==} engines: {node: '>=20.0.0'} + jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -3026,11 +3086,6 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -3046,11 +3101,8 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - just-extend@5.1.1: - resolution: {integrity: sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==} - - katex@0.16.37: - resolution: {integrity: sha512-TIGjO2cCGYono+uUzgkE7RFF329mLLWGuHUlSr6cwIVj9O8f0VQZ783rsanmJpFUo32vvtj7XT04NGRPh+SZFg==} + katex@0.16.45: + resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} hasBin: true keyv@4.5.4: @@ -3070,10 +3122,6 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - langium@4.2.1: - resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==} - engines: {node: '>=20.10.0', npm: '>=10.2.3'} - language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -3091,6 +3139,83 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lezer-elixir@1.1.3: + resolution: {integrity: sha512-Ymc58/WhxdZS9yEOlnKbF3rdeBdFcPm4OEm26KMqA1Za9vztXi7I5qwGw1KxYmm3Nv0iDHq//EQyBwSEzKG9Mg==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -3101,24 +3226,12 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - loader-runner@4.3.1: - resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} - engines: {node: '>=6.11.5'} - - loader-utils@2.0.4: - resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} - engines: {node: '>=8.9.0'} - - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.23: - resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -3138,8 +3251,8 @@ packages: lodash.upperfirst@4.3.1: resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3157,11 +3270,6 @@ packages: resolution: {integrity: sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg==} engines: {node: '>=20'} - marked@14.0.0: - resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} - engines: {node: '>= 18'} - hasBin: true - marked@16.4.2: resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} engines: {node: '>= 20'} @@ -3172,8 +3280,8 @@ packages: engines: {node: '>= 12'} hasBin: true - material-icon-theme@5.32.0: - resolution: {integrity: sha512-SxJxCcnk6cJIbd+AxmoeghXJ24joXGmUzjiGci16sX4mXZdXprGEzM6ZZ0VHGAofxNlMqznEbExINwFLsxf8eQ==} + material-icon-theme@5.34.0: + resolution: {integrity: sha512-m+ZgtdtJMkfOwxpfUH8FqyJbfnAJuxMBNv4XRvz3m808Is42RbeYc/5W0bk5qGJJLxzF5Cupj6Or52aQISwz3A==} engines: {vscode: ^1.55.0} mathml-tag-names@4.0.0: @@ -3192,15 +3300,12 @@ packages: resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} engines: {node: '>=20'} - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.12.3: - resolution: {integrity: sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==} + mermaid@11.15.0: + resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==} micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -3289,14 +3394,8 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mini-css-extract-plugin@2.10.0: - resolution: {integrity: sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^5.0.0 - - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: @@ -3305,17 +3404,8 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - mlly@1.8.1: - resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} - - monaco-editor-webpack-plugin@7.1.1: - resolution: {integrity: sha512-WxdbFHS3Wtz4V9hzhe/Xog5hQRSMxmDLkEEYZwqMDHgJlkZo00HVFZR0j5d0nKypjTUkkygH3dDSXERLG4757A==} - peerDependencies: - monaco-editor: '>= 0.31.0' - webpack: ^4.5.0 || 5.x - - monaco-editor@0.55.1: - resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} moo@0.5.3: resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==} @@ -3329,8 +3419,8 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -3345,8 +3435,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} node-fetch@2.6.13: resolution: {integrity: sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==} @@ -3366,8 +3457,8 @@ packages: encoding: optional: true - node-releases@2.0.36: - resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} nolyfill@1.0.44: resolution: {integrity: sha512-PoggwVLiJUn0MnodpftsiC7EuknW5+6v62ntTOQ6T6l7g2r6aoaOwgk0tQW2BxGLYw9bF298LL8jDFTmEFuzlA==} @@ -3381,6 +3472,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3392,9 +3486,6 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - online-3d-viewer@0.18.0: resolution: {integrity: sha512-y7ZlV/zkakNUyjqcXz6XecA7vXgLEUnaAey9tyx8o6/wcdV64RfjXAQOjGXGY2JOZoDi4Cg1ic9icSWMWAvRQA==} @@ -3402,26 +3493,14 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} @@ -3436,6 +3515,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -3446,10 +3528,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -3469,12 +3547,12 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} pify@2.3.0: @@ -3485,20 +3563,16 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - playwright-core@1.58.2: - resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} engines: {node: '>=18'} hasBin: true - playwright@1.58.2: - resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} engines: {node: '>=18'} hasBin: true @@ -3528,54 +3602,23 @@ packages: peerDependencies: postcss: ^8.4.21 - postcss-load-config@4.0.2: - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} - engines: {node: '>= 14'} + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} peerDependencies: + jiti: '>=1.21.0' postcss: '>=8.0.9' - ts-node: '>=9.0.0' + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: + jiti: + optional: true postcss: optional: true - ts-node: + tsx: optional: true - - postcss-loader@8.2.1: - resolution: {integrity: sha512-k98jtRzthjj3f76MYTs9JTpRqV1RaaMhEU0Lpw9OTmQZQdppg4B30VZ74BojuBHt3F4KyubHJoXCMUeM8Bqeow==} - engines: {node: '>= 18.12.0'} - peerDependencies: - '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 - postcss: ^7.0.0 || ^8.0.1 - webpack: ^5.0.0 - peerDependenciesMeta: - '@rspack/core': + yaml: optional: true - webpack: - optional: true - - postcss-modules-extract-imports@3.1.0: - resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-local-by-default@4.2.0: - resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-scope@3.2.1: - resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-values@4.0.0: - resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 postcss-nested@6.2.0: resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} @@ -3606,8 +3649,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -3618,11 +3661,18 @@ packages: resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} hasBin: true + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -3631,13 +3681,19 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qified@0.6.0: - resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==} + qified@0.9.1: + resolution: {integrity: sha512-n7mar4T0xQ+39dE2vGTAlbxUEpndwPANH0kDef1/MYsB8Bba9wshkybIRx74qgcvKQPEWErf9AqAdYjhzY2Ilg==} engines: {node: '>=20'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -3645,10 +3701,6 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - rechoir@0.8.0: - resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} - engines: {node: '>= 10.13.0'} - refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -3661,8 +3713,8 @@ packages: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true - regjsparser@0.13.0: - resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} + regjsparser@0.13.1: + resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} hasBin: true rename-keys@1.2.0: @@ -3673,23 +3725,23 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.6: + resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} engines: {node: '>= 0.4'} hasBin: true @@ -3697,12 +3749,17 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - robust-predicates@3.0.2: - resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + rolldown-license-plugin@3.0.4: + resolution: {integrity: sha512-TSn+oxPSJLh5xtzPZlbTftFCqPA/DUhYewh4Pik8GJlsue53pJGV/HKipLgTE+TIsvgQDyS+LUgBPXFLvcasxQ==} + peerDependencies: + rolldown: '*' + + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true roughjs@4.6.6: @@ -3718,13 +3775,13 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - sax@1.5.0: - resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} - schema-utils@4.3.3: - resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} - engines: {node: '>= 10.13.0'} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} @@ -3734,25 +3791,21 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} engines: {node: '>=10'} hasBin: true - seroval-plugins@1.5.0: - resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==} + seroval-plugins@1.5.3: + resolution: {integrity: sha512-LhVh4KjjkKmCxOUjoaUwtqbDjyMfnA535yEmmGDuwZcIYtw8ns6tZmeszNTECeUg/3sJpnEjsz/KhQrcPXPw1Q==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 - seroval@1.5.0: - resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} + seroval@1.5.3: + resolution: {integrity: sha512-BXe0x4buEeYiIKaRUnth1WqCILQ3k4O67KP/B4pC3pVz0Mv2c96ngA9QDREUYxWY1sb2RZVRqwI9RcpVMyHCVw==} engines: {node: '>=10'} - shallow-clone@3.0.1: - resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} - engines: {node: '>=8'} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3768,6 +3821,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + slash@5.1.0: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} @@ -3776,12 +3833,12 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} - smol-toml@1.6.0: - resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} - solid-js@1.9.11: - resolution: {integrity: sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==} + solid-js@1.9.12: + resolution: {integrity: sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==} solid-transition-group@0.2.3: resolution: {integrity: sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg==} @@ -3792,43 +3849,16 @@ packages: sortablejs@1.15.7: resolution: {integrity: sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==} - source-list-map@2.0.1: - resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - spdx-compare@1.0.0: - resolution: {integrity: sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==} - - spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - - spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - - spdx-expression-validate@2.0.0: - resolution: {integrity: sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg==} - - spdx-license-ids@3.0.23: - resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} - - spdx-ranges@2.1.1: - resolution: {integrity: sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==} - - spdx-satisfies@5.0.1: - resolution: {integrity: sha512-Nwor6W6gzFp8XX4neaKQ7ChV4wmpSh2sSDemMFSzHxpTw460jxFYeOn+jq4ybnSSw/5sc3pjka9MQPouksQNpw==} - - spectral-cli-bundle@1.0.7: - resolution: {integrity: sha512-vIUC0nwv9tYxWV1xHdR3CTVDOEEtLKaDCcQpARZgO0Db7VmSpSWJ4xrnVPNSmO59hBtGwW2CVzHf0OimJBaKAA==} + spectral-cli-bundle@1.0.8: + resolution: {integrity: sha512-wxHPXFy5bdtqT/flJRjWbSdPoDjIiXzmLvxE7RO9bbs/B2DuavW2UIG8HYa7m7F6RlQaK7rhoiIRoYEEeBP5Jg==} engines: {node: '>=20'} hasBin: true @@ -3836,11 +3866,15 @@ packages: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + 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==} @@ -3850,8 +3884,8 @@ packages: resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} engines: {node: '>=20'} - string-width@8.2.0: - resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} engines: {node: '>=20'} strip-ansi@6.0.1: @@ -3874,6 +3908,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-search@0.1.0: resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} @@ -3901,22 +3938,19 @@ packages: peerDependencies: stylelint: '>=16' - stylelint@17.4.0: - resolution: {integrity: sha512-3kQ2/cHv3Zt8OBg+h2B8XCx9evEABQIrv4hh3uXahGz/ZEHrTR80zxBiK2NfXNaSoyBzxO1pjsz1Vhdzwn5XSw==} + stylelint@17.11.0: + resolution: {integrity: sha512-/3czzmbF9XdGWvReDF3Ex4R23Ajolo7j8RB2bFNEqk6Ht356nlpVV+G5bG2Qt8AW1ofJzXztBRDnAtd7cgowWA==} 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==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true - superstruct@0.10.13: - resolution: {integrity: sha512-W4SitSZ9MOyMPbHreoZVEneSZyPEeNGbdfJo/7FkJyRs/M3wQRFzq+t3S/NBwlrFSWdx1ONLjLb9pB+UKe4IqQ==} - supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -3925,10 +3959,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - supports-hyperlinks@4.4.0: resolution: {integrity: sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==} engines: {node: '>=20'} @@ -3951,8 +3981,11 @@ packages: svgson@5.3.1: resolution: {integrity: sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==} - swagger-ui-dist@5.32.0: - resolution: {integrity: sha512-nKZB0OuDvacB0s/lC2gbge+RigYvGRGpLLMWMFxaTUwfM+CfndVk9Th2IaTinqXiz6Mn26GK2zriCpv6/+5m3Q==} + swagger-ui-dist@5.32.5: + resolution: {integrity: sha512-7/FQfWe9A4qoyYFdAwy0chD0uDYidDp/ZT9VQ9LZlgD4AnnHJk8/+ytAA1HkJYOPySmK6helPDdJQMlcumt7HA==} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} sync-fetch@0.4.5: resolution: {integrity: sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==} @@ -3966,36 +3999,11 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tailwindcss@3.4.17: - resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} hasBin: true - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} - - terser-webpack-plugin@5.3.17: - resolution: {integrity: sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - - terser@5.46.0: - resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} - engines: {node: '>=10'} - hasBin: true - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -4013,16 +4021,16 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} tippy.js@6.3.7: @@ -4035,14 +4043,22 @@ packages: toastify-js@1.12.0: resolution: {integrity: sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + tributejs@5.1.3: resolution: {integrity: sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==} - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -4064,37 +4080,50 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.57.1: - resolution: {integrity: sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==} + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + typescript-eslint@8.59.2: + resolution: {integrity: sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==} 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.0.0' + typescript: '>=4.8.4 <6.1.0' typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + typo-js@1.3.1: resolution: {integrity: sha512-elJkpCL6Z77Ghw0Lv0lGnhBAjSTOQ5FhiVOCfOuxhaoTT2xtLVbqikYItK5HHchzPbHEUFAcjOH669T2ZzeCbg==} uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} uint8-to-base64@0.2.1: resolution: {integrity: sha512-uO/84GaoDUfiAxpa8EksjVLE77A9Kc7ZTziN4zRpq4de9yLaLcZn3jx1/sVjyupsywcVX6RKWbqLe7gUNyzH+Q==} - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} unicorn-magic@0.4.0: resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} engines: {node: '>=20'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -4104,38 +4133,42 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - updates@17.8.3: - resolution: {integrity: sha512-YTCzRy4rGdrDCuX8wrnDKn2KXYp5kJS1T94k/JKQED60+lMAxdxoasX61EG0zbWYBxQQpTvC3uAzyDu3U+2zZA==} + updates@17.16.9: + resolution: {integrity: sha512-fH8FlkdS3afc1pnI3KUBnLrixHg4iaRBSL0J2FK8C1BXZTufqJEj4S1HhshugsHouth2lB3lOYYU5rIGbfHx5g==} engines: {node: '>=22'} hasBin: true uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true vanilla-colorful@0.7.2: resolution: {integrity: sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg==} - vite-string-plugin@2.0.1: - resolution: {integrity: sha512-L5B86yQkYrqH5d966w1vI91B0d+0vmICgB6tqjINvtBIGU9qhFY7izqjytED/ApggFC4QTDWNjfF6nWMqY/fQg==} + vite-string-plugin@2.0.4: + resolution: {integrity: sha512-uahQl15I6hsHGzbjCmllVoKZBmZavXAp8GXBmq5omNQM52wgBv7e//98A6cONbq0Of6F/5uJ30OjF5ggNY6reQ==} peerDependencies: vite: '*' - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@8.0.10: + resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -4146,12 +4179,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -4167,20 +4202,23 @@ packages: yaml: optional: true - vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.18 - '@vitest/browser-preview': 4.0.18 - '@vitest/browser-webdriverio': 4.0.18 - '@vitest/ui': 4.0.18 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -4194,6 +4232,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -4201,23 +4243,6 @@ packages: jsdom: optional: true - vscode-jsonrpc@8.2.0: - resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} - engines: {node: '>=14.0.0'} - - vscode-languageserver-protocol@3.17.5: - resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - - vscode-languageserver-textdocument@1.0.12: - resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} - - vscode-languageserver-types@3.17.5: - resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - - vscode-languageserver@9.0.1: - resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} - hasBin: true - vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -4236,78 +4261,47 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - vue-loader@17.4.2: - resolution: {integrity: sha512-yTKOA4R/VN4jqjw4y5HrynFL8AK0Z3/Jt7eOJXEitsm0GMRHDBjCfCiuTiLP7OESvsZYo2pATCWhDqxC5ZrM6w==} - peerDependencies: - '@vue/compiler-sfc': '*' - vue: '*' - webpack: ^4.1.0 || ^5.0.0-0 - peerDependenciesMeta: - '@vue/compiler-sfc': - optional: true - vue: - optional: true - - vue-tsc@3.2.5: - resolution: {integrity: sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==} + vue-tsc@3.2.8: + resolution: {integrity: sha512-27vTLJ6Q2370obOd0PFYoYoKnmXJ521uUIedrs3Zhhhg/8YG10VOCMmwt+JQslatpAMTDbnWiitLnoD5VlIvog==} hasBin: true peerDependencies: typescript: '>=5.0.0' - vue@3.5.29: - resolution: {integrity: sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==} + vue@3.5.34: + resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true - watchpack@2.5.1: - resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} - engines: {node: '>=10.13.0'} + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webpack-cli@6.0.1: - resolution: {integrity: sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==} - engines: {node: '>=18.12.0'} - hasBin: true - peerDependencies: - webpack: ^5.82.0 - webpack-bundle-analyzer: '*' - webpack-dev-server: '*' - peerDependenciesMeta: - webpack-bundle-analyzer: - optional: true - webpack-dev-server: - optional: true + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} - webpack-merge@6.0.1: - resolution: {integrity: sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==} - engines: {node: '>=18.0.0'} - - webpack-sources@1.4.3: - resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} - - webpack-sources@3.3.4: - resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} - engines: {node: '>=10.13.0'} - - webpack@5.105.4: - resolution: {integrity: sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true + whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} + whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4325,30 +4319,16 @@ packages: engines: {node: '>=8'} hasBin: true - wildcard@2.0.1: - resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} - word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrap-ansi@10.0.0: - resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} - engines: {node: '>=20'} - - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - write-file-atomic@7.0.1: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4369,10 +4349,8 @@ packages: xml-reader@2.4.3: resolution: {integrity: sha512-xWldrIxjeAMAu6+HSf9t50ot1uL5M+BtOidRCWHXIeewvSeIpscWCsp4Zxjk8kHHhdqFBrfK8U0EJeCcnyQ/gA==} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} - engines: {node: '>= 14.6'} - hasBin: true + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} @@ -4385,7 +4363,7 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 - tinyexec: 1.0.2 + tinyexec: 1.1.2 '@babel/code-frame@7.29.0': dependencies: @@ -4397,11 +4375,11 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} - '@babel/parser@7.29.0': + '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 - '@babel/runtime@7.28.6': {} + '@babel/runtime@7.29.2': {} '@babel/types@7.29.0': dependencies: @@ -4412,33 +4390,18 @@ snapshots: '@cacheable/memory@2.0.8': dependencies: - '@cacheable/utils': 2.4.0 + '@cacheable/utils': 2.4.1 '@keyv/bigmap': 1.3.1(keyv@5.6.0) hookified: 1.15.1 keyv: 5.6.0 - '@cacheable/utils@2.4.0': + '@cacheable/utils@2.4.1': dependencies: - hashery: 1.5.0 + hashery: 1.5.1 keyv: 5.6.0 - '@chevrotain/cst-dts-gen@11.1.2': - dependencies: - '@chevrotain/gast': 11.1.2 - '@chevrotain/types': 11.1.2 - lodash-es: 4.17.23 - - '@chevrotain/gast@11.1.2': - dependencies: - '@chevrotain/types': 11.1.2 - lodash-es: 4.17.23 - - '@chevrotain/regexp-to-ast@11.1.2': {} - '@chevrotain/types@11.1.2': {} - '@chevrotain/utils@11.1.2': {} - '@citation-js/core@0.7.21': dependencies: '@citation-js/date': 0.5.1 @@ -4497,7 +4460,258 @@ snapshots: '@citation-js/date': 0.5.1 '@citation-js/name': 0.4.2 - '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@codemirror/autocomplete@6.20.2': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.10.3': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + '@lezer/common': 1.5.2 + + '@codemirror/lang-angular@0.1.4': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.3 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-cpp@6.0.3': + dependencies: + '@codemirror/language': 6.12.3 + '@lezer/cpp': 1.1.5 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@lezer/common': 1.5.2 + '@lezer/css': 1.3.3 + + '@codemirror/lang-go@6.0.1': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@lezer/common': 1.5.2 + '@lezer/go': 1.0.1 + + '@codemirror/lang-html@6.4.11': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + '@lezer/common': 1.5.2 + '@lezer/css': 1.3.3 + '@lezer/html': 1.3.13 + + '@codemirror/lang-java@6.0.2': + dependencies: + '@codemirror/language': 6.12.3 + '@lezer/java': 1.1.3 + + '@codemirror/lang-javascript@6.2.5': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.6 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + '@lezer/common': 1.5.2 + '@lezer/javascript': 1.5.4 + + '@codemirror/lang-jinja@6.0.1': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.3 + '@lezer/json': 1.0.3 + + '@codemirror/lang-less@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.3 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-liquid@6.3.2': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-markdown@6.5.0': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + '@lezer/common': 1.5.2 + '@lezer/markdown': 1.6.3 + + '@codemirror/lang-php@6.0.2': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@lezer/common': 1.5.2 + '@lezer/php': 1.0.5 + + '@codemirror/lang-python@6.2.1': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@lezer/common': 1.5.2 + '@lezer/python': 1.1.18 + + '@codemirror/lang-rust@6.0.2': + dependencies: + '@codemirror/language': 6.12.3 + '@lezer/rust': 1.0.2 + + '@codemirror/lang-sass@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@lezer/common': 1.5.2 + '@lezer/sass': 1.1.0 + + '@codemirror/lang-sql@6.10.0': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-vue@0.1.3': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.3 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@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.10 + + '@codemirror/lang-xml@6.1.0': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + '@lezer/common': 1.5.2 + '@lezer/xml': 1.0.6 + + '@codemirror/lang-yaml@6.1.3': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@lezer/yaml': 1.0.4 + + '@codemirror/language-data@6.5.2': + dependencies: + '@codemirror/lang-angular': 0.1.4 + '@codemirror/lang-cpp': 6.0.3 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-go': 6.0.1 + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-java': 6.0.2 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/lang-jinja': 6.0.1 + '@codemirror/lang-json': 6.0.2 + '@codemirror/lang-less': 6.0.2 + '@codemirror/lang-liquid': 6.3.2 + '@codemirror/lang-markdown': 6.5.0 + '@codemirror/lang-php': 6.0.2 + '@codemirror/lang-python': 6.2.1 + '@codemirror/lang-rust': 6.0.2 + '@codemirror/lang-sass': 6.0.2 + '@codemirror/lang-sql': 6.10.0 + '@codemirror/lang-vue': 0.1.3 + '@codemirror/lang-wast': 6.0.2 + '@codemirror/lang-xml': 6.1.0 + '@codemirror/lang-yaml': 6.1.3 + '@codemirror/language': 6.12.3 + '@codemirror/legacy-modes': 6.5.2 + + '@codemirror/language@6.12.3': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/legacy-modes@6.5.2': + dependencies: + '@codemirror/language': 6.12.3 + + '@codemirror/lint@6.9.6': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + crelt: 1.0.6 + + '@codemirror/search@6.7.0': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + crelt: 1.0.6 + + '@codemirror/state@6.6.0': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/view@6.42.0': + dependencies: + '@codemirror/state': 6.6.0 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + + '@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)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 @@ -4506,7 +4720,9 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.0': {} + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 '@csstools/css-tokenizer@4.0.0': {} @@ -4523,144 +4739,146 @@ snapshots: dependencies: postcss-selector-parser: 7.1.1 - '@discoveryjs/json-ext@0.6.3': {} - - '@emnapi/core@1.8.1': + '@deltablot/dropzone@7.4.3': dependencies: - '@emnapi/wasi-threads': 1.1.0 + '@swc/helpers': 0.5.21 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.8.1': + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.1.0': + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.3': + '@esbuild/aix-ppc64@0.28.0': optional: true - '@esbuild/android-arm64@0.27.3': + '@esbuild/android-arm64@0.28.0': optional: true - '@esbuild/android-arm@0.27.3': + '@esbuild/android-arm@0.28.0': optional: true - '@esbuild/android-x64@0.27.3': + '@esbuild/android-x64@0.28.0': optional: true - '@esbuild/darwin-arm64@0.27.3': + '@esbuild/darwin-arm64@0.28.0': optional: true - '@esbuild/darwin-x64@0.27.3': + '@esbuild/darwin-x64@0.28.0': optional: true - '@esbuild/freebsd-arm64@0.27.3': + '@esbuild/freebsd-arm64@0.28.0': optional: true - '@esbuild/freebsd-x64@0.27.3': + '@esbuild/freebsd-x64@0.28.0': optional: true - '@esbuild/linux-arm64@0.27.3': + '@esbuild/linux-arm64@0.28.0': optional: true - '@esbuild/linux-arm@0.27.3': + '@esbuild/linux-arm@0.28.0': optional: true - '@esbuild/linux-ia32@0.27.3': + '@esbuild/linux-ia32@0.28.0': optional: true - '@esbuild/linux-loong64@0.27.3': + '@esbuild/linux-loong64@0.28.0': optional: true - '@esbuild/linux-mips64el@0.27.3': + '@esbuild/linux-mips64el@0.28.0': optional: true - '@esbuild/linux-ppc64@0.27.3': + '@esbuild/linux-ppc64@0.28.0': optional: true - '@esbuild/linux-riscv64@0.27.3': + '@esbuild/linux-riscv64@0.28.0': optional: true - '@esbuild/linux-s390x@0.27.3': + '@esbuild/linux-s390x@0.28.0': optional: true - '@esbuild/linux-x64@0.27.3': + '@esbuild/linux-x64@0.28.0': optional: true - '@esbuild/netbsd-arm64@0.27.3': + '@esbuild/netbsd-arm64@0.28.0': optional: true - '@esbuild/netbsd-x64@0.27.3': + '@esbuild/netbsd-x64@0.28.0': optional: true - '@esbuild/openbsd-arm64@0.27.3': + '@esbuild/openbsd-arm64@0.28.0': optional: true - '@esbuild/openbsd-x64@0.27.3': + '@esbuild/openbsd-x64@0.28.0': optional: true - '@esbuild/openharmony-arm64@0.27.3': + '@esbuild/openharmony-arm64@0.28.0': optional: true - '@esbuild/sunos-x64@0.27.3': + '@esbuild/sunos-x64@0.28.0': optional: true - '@esbuild/win32-arm64@0.27.3': + '@esbuild/win32-arm64@0.28.0': optional: true - '@esbuild/win32-ia32@0.27.3': + '@esbuild/win32-ia32@0.28.0': optional: true - '@esbuild/win32-x64@0.27.3': + '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.0.3(jiti@2.6.1))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.3.0(jiti@2.7.0))': dependencies: escape-string-regexp: 4.0.0 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) ignore: 7.0.5 - '@eslint-community/eslint-utils@4.9.1(eslint@10.0.3(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.3.0(jiti@2.7.0))': dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@1.4.1(eslint@10.0.3(jiti@2.6.1))': + '@eslint/compat@1.4.1(eslint@10.3.0(jiti@2.7.0))': dependencies: '@eslint/core': 0.17.0 optionalDependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) - '@eslint/config-array@0.23.3': + '@eslint/config-array@0.23.5': dependencies: - '@eslint/object-schema': 3.0.3 + '@eslint/object-schema': 3.0.5 debug: 4.4.3 - minimatch: 10.2.4 + minimatch: 10.2.5 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.5.3': + '@eslint/config-helpers@0.5.5': dependencies: - '@eslint/core': 1.1.1 + '@eslint/core': 1.2.1 '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/core@1.1.1': + '@eslint/core@1.2.1': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.4': + '@eslint/eslintrc@3.3.5': dependencies: - ajv: 6.14.0 + ajv: 6.15.0 debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 @@ -4672,20 +4890,25 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.39.3': {} + '@eslint/js@9.39.4': {} - '@eslint/json@1.1.0': + '@eslint/json@1.2.0': dependencies: - '@eslint/core': 1.1.1 + '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.6.1 '@humanwhocodes/momoa': 3.3.10 natural-compare: 1.4.0 - '@eslint/object-schema@3.0.3': {} + '@eslint/object-schema@3.0.5': {} '@eslint/plugin-kit@0.6.1': dependencies: - '@eslint/core': 1.1.1 + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@eslint/plugin-kit@0.7.1': + dependencies: + '@eslint/core': 1.2.1 levn: 0.4.1 '@github/browserslist-config@1.0.0': {} @@ -4701,13 +4924,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': {} @@ -4716,11 +4944,40 @@ snapshots: '@iconify/types@2.0.0': {} - '@iconify/utils@3.1.0': + '@iconify/utils@3.1.1': dependencies: '@antfu/install-pkg': 1.1.0 '@iconify/types': 2.0.0 - mlly: 1.8.1 + mlly: 1.8.2 + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + jest-mock: 29.7.0 + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 25.6.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.10 + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 25.6.0 + '@types/yargs': 17.0.35 + chalk: 4.1.2 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -4729,11 +4986,6 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/source-map@0.3.11': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -4743,7 +4995,7 @@ snapshots: '@keyv/bigmap@1.3.1(keyv@5.6.0)': dependencies: - hashery: 1.5.0 + hashery: 1.5.1 hookified: 1.15.1 keyv: 5.6.0 @@ -4751,27 +5003,135 @@ snapshots: '@kurkle/color@0.3.4': {} - '@mcaptcha/core-glue@0.1.0-alpha-5': {} + '@lezer/common@1.5.2': {} - '@mcaptcha/vanilla-glue@0.1.0-alpha-3': + '@lezer/cpp@1.1.5': dependencies: - '@mcaptcha/core-glue': 0.1.0-alpha-5 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 - '@mermaid-js/layout-elk@0.2.0(mermaid@11.12.3)': + '@lezer/css@1.3.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@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.10 + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/html@1.3.13': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@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.10 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@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.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/markdown@1.6.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + + '@lezer/php@1.0.5': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@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.10 + + '@lezer/rust@1.0.2': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@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.10 + + '@lezer/xml@1.0.6': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@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.10 + + '@marijn/find-cluster-break@1.0.2': {} + + '@mcaptcha/core-glue@0.1.0-rc1': {} + + '@mcaptcha/vanilla-glue@0.1.0-rc2': + dependencies: + '@mcaptcha/core-glue': 0.1.0-rc1 + jest-environment-jsdom: 29.7.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + + '@mermaid-js/layout-elk@0.2.1(mermaid@11.15.0)': dependencies: d3: 7.9.0 elkjs: 0.9.3 - mermaid: 11.12.3 + mermaid: 11.15.0 - '@mermaid-js/parser@1.0.0': + '@mermaid-js/parser@1.1.1': dependencies: - langium: 4.2.1 + '@chevrotain/types': 11.1.2 '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.8.1 - '@tybys/wasm-util': 0.10.1 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 optional: true '@nodelib/fs.scandir@2.1.5': @@ -4786,6 +5146,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@nolyfill/abab@1.0.44': {} + '@nolyfill/array-includes@1.0.44': dependencies: '@nolyfill/shared': 1.0.44 @@ -4802,14 +5164,24 @@ snapshots: dependencies: '@nolyfill/shared': 1.0.44 + '@nolyfill/es-set-tostringtag@1.0.44': {} + '@nolyfill/hasown@1.0.44': {} '@nolyfill/is-core-module@1.0.39': {} + '@nolyfill/object-keys@1.0.44': + dependencies: + '@nolyfill/shared': 1.0.44 + '@nolyfill/object.assign@1.0.44': dependencies: '@nolyfill/shared': 1.0.44 + '@nolyfill/object.entries@1.0.44': + dependencies: + '@nolyfill/shared': 1.0.44 + '@nolyfill/object.fromentries@1.0.44': dependencies: '@nolyfill/shared': 1.0.44 @@ -4836,167 +5208,181 @@ snapshots: dependencies: '@nolyfill/shared': 1.0.44 + '@oxc-project/types@0.127.0': {} + '@package-json/types@0.0.12': {} '@pkgr/core@0.2.9': {} - '@playwright/test@1.58.2': + '@playwright/test@1.59.1': dependencies: - playwright: 1.58.2 + playwright: 1.59.1 '@popperjs/core@2.11.8': {} - '@primer/octicons@19.22.0': + '@primer/octicons@19.25.0': 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.42.0)': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + + '@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.42.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10)': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.20.2)(@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.42.0)(@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.2 + '@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.42.0 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/javascript': 1.5.4 + '@lezer/lr': 1.4.10 + + '@replit/codemirror-vscode-keymap@6.0.2(@codemirror/autocomplete@6.20.2)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.42.0)': + dependencies: + '@codemirror/autocomplete': 6.20.2 + '@codemirror/commands': 6.10.3 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.6 + '@codemirror/search': 6.7.0 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.42.0 + '@resvg/resvg-wasm@2.6.2': {} - '@rolldown/pluginutils@1.0.0-rc.2': {} - - '@rollup/rollup-android-arm-eabi@4.59.0': + '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true - '@rollup/rollup-android-arm64@4.59.0': + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': optional: true - '@rollup/rollup-darwin-arm64@4.59.0': + '@rolldown/binding-darwin-x64@1.0.0-rc.17': optional: true - '@rollup/rollup-darwin-x64@4.59.0': + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': optional: true - '@rollup/rollup-freebsd-arm64@4.59.0': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': optional: true - '@rollup/rollup-freebsd-x64@4.59.0': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.59.0': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': optional: true - '@rollup/rollup-linux-arm64-gnu@4.59.0': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': optional: true - '@rollup/rollup-linux-arm64-musl@4.59.0': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': optional: true - '@rollup/rollup-linux-loong64-gnu@4.59.0': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': optional: true - '@rollup/rollup-linux-loong64-musl@4.59.0': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.59.0': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rollup/rollup-linux-ppc64-musl@4.59.0': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.59.0': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': optional: true - '@rollup/rollup-linux-riscv64-musl@4.59.0': - optional: true + '@rolldown/pluginutils@1.0.0-rc.13': {} - '@rollup/rollup-linux-s390x-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.59.0': - optional: true - - '@rollup/rollup-openbsd-x64@4.59.0': - optional: true - - '@rollup/rollup-openharmony-arm64@4.59.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.59.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.59.0': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.59.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.59.0': - optional: true + '@rolldown/pluginutils@1.0.0-rc.17': {} '@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.29(typescript@5.9.3))': - dependencies: - tippy.js: 6.3.7 - vue: 3.5.29(typescript@5.9.3) - '@simonwep/pickr@1.9.0': dependencies: core-js: 3.32.2 nanopop: 2.3.0 + '@sinclair/typebox@0.27.10': {} + '@sindresorhus/merge-streams@4.0.0': {} - '@solid-primitives/refs@1.1.3(solid-js@1.9.11)': + '@sinonjs/commons@3.0.1': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@1.9.11) - solid-js: 1.9.11 + type-detect: 4.0.8 - '@solid-primitives/transition-group@1.1.2(solid-js@1.9.11)': + '@sinonjs/fake-timers@10.3.0': dependencies: - solid-js: 1.9.11 + '@sinonjs/commons': 3.0.1 - '@solid-primitives/utils@6.4.0(solid-js@1.9.11)': + '@solid-primitives/refs@1.1.3(solid-js@1.9.12)': dependencies: - solid-js: 1.9.11 + '@solid-primitives/utils': 6.4.0(solid-js@1.9.12) + solid-js: 1.9.12 + + '@solid-primitives/transition-group@1.1.2(solid-js@1.9.12)': + dependencies: + solid-js: 1.9.12 + + '@solid-primitives/utils@6.4.0(solid-js@1.9.12)': + dependencies: + solid-js: 1.9.12 '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@5.10.0(eslint@10.0.3(jiti@2.6.1))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.3.0(jiti@2.7.0))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/types': 8.56.1 - eslint: 10.0.3(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0)) + '@typescript-eslint/types': 8.59.2 + eslint: 10.3.0(jiti@2.7.0) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 - picomatch: 4.0.3 + picomatch: 4.0.4 - '@stylistic/stylelint-plugin@5.0.1(stylelint@17.4.0(typescript@5.9.3))': + '@stylistic/stylelint-plugin@5.1.0(stylelint@17.11.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.8 + postcss: 8.5.14 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 style-search: 0.1.0 - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.11.0(typescript@6.0.3) - '@swc/helpers@0.2.14': {} - - '@techknowlogick/license-checker-webpack-plugin@0.3.0(webpack@5.105.4)': + '@swc/helpers@0.5.21': dependencies: - glob: 7.2.3 - lodash: 4.17.23 - minimatch: 3.1.5 - semver: 6.3.1 - spdx-expression-validate: 2.0.0 - spdx-satisfies: 5.0.1 - superstruct: 0.10.13 - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-sources: 1.4.3 - wrap-ansi: 6.2.0 + tslib: 2.8.1 - '@tybys/wasm-util@0.10.1': + '@tootallnate/once@2.0.1': {} + + '@tybys/wasm-util@0.10.2': dependencies: tslib: 2.8.1 optional: true @@ -5127,26 +5513,12 @@ snapshots: '@types/d3-transition': 3.0.9 '@types/d3-zoom': 3.0.8 - '@types/debug@4.1.12': + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 '@types/deep-eql@4.0.2': {} - '@types/dropzone@5.7.9': - dependencies: - '@types/jquery': 4.0.0 - - '@types/eslint-scope@3.7.7': - dependencies: - '@types/eslint': 9.6.1 - '@types/estree': 1.0.8 - - '@types/eslint@9.6.1': - dependencies: - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@types/esrecurse@4.3.1': {} '@types/estree@1.0.8': {} @@ -5155,10 +5527,26 @@ snapshots: '@types/hammerjs@2.0.46': {} + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + '@types/jquery@4.0.0': {} '@types/js-yaml@4.0.9': {} + '@types/jsdom@20.0.1': + dependencies: + '@types/node': 25.6.0 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -5169,14 +5557,16 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@25.3.5': + '@types/node@25.6.0': dependencies: - undici-types: 7.18.2 + undici-types: 7.19.2 '@types/pdfobject@2.2.5': {} '@types/sortablejs@1.15.9': {} + '@types/stack-utils@2.0.3': {} + '@types/swagger-ui-dist@3.30.6': {} '@types/tern@0.23.9': @@ -5187,6 +5577,8 @@ snapshots: '@types/toastify-js@1.12.4': {} + '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': optional: true @@ -5196,176 +5588,182 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.3.5 + '@types/node': 25.6.0 - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.56.1 - eslint: 10.0.3(jiti@2.6.1) + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/type-utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.2 + eslint: 10.3.0(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/type-utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 - eslint: 10.0.3(jiti@2.6.1) + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/type-utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.2 + eslint: 10.3.0(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + '@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + eslint: 10.3.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.59.2(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) + '@typescript-eslint/types': 8.59.2 debug: 4.4.3 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.56.1': + '@typescript-eslint/scope-manager@8.59.2': dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 - '@typescript-eslint/scope-manager@8.57.1': - dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 - - '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@6.0.3)': dependencies: - typescript: 5.9.3 + typescript: 6.0.3 - '@typescript-eslint/type-utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.3) + eslint: 10.3.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 + eslint: 10.3.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.56.1': {} + '@typescript-eslint/types@8.59.2': {} - '@typescript-eslint/types@8.57.1': {} - - '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.59.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/project-service': 8.59.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3 - minimatch: 10.2.4 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) + minimatch: 10.2.5 + semver: 7.8.0 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.57.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 + '@typescript-eslint/project-service': 8.59.2(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3 - minimatch: 10.2.4 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) + minimatch: 10.2.5 + semver: 7.8.0 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + eslint: 10.3.0(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) - typescript: 5.9.3 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + eslint: 10.3.0(jiti@2.7.0) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/visitor-keys@8.59.2': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.56.1': - dependencies: - '@typescript-eslint/types': 8.56.1 - eslint-visitor-keys: 5.0.1 - - '@typescript-eslint/visitor-keys@8.57.1': - dependencies: - '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/types': 8.59.2 eslint-visitor-keys: 5.0.1 '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -5427,61 +5825,69 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-vue@6.0.4(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': - dependencies: - '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) - vue: 3.5.29(typescript@5.9.3) - - '@vitest/eslint-plugin@1.6.12(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': - dependencies: - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) + '@upsetjs/venn.js@2.0.0': optionalDependencies: - typescript: 5.9.3 - vitest: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + '@vitejs/plugin-vue@6.0.6(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.34(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.13 + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0) + vue: 3.5.34(typescript@6.0.3) + + '@vitest/eslint-plugin@1.6.16(@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.5(@types/node@25.6.0)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)))': + dependencies: + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.3.0(jiti@2.7.0) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + typescript: 6.0.3 + vitest: 4.1.5(@types/node@25.6.0)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)) transitivePeerDependencies: - supports-color - '@vitest/expect@4.0.18': + '@vitest/expect@4.1.5': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': + '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0))': dependencies: - '@vitest/spy': 4.0.18 + '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0) - '@vitest/pretty-format@4.0.18': + '@vitest/pretty-format@4.1.5': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/runner@4.0.18': + '@vitest/runner@4.1.5': dependencies: - '@vitest/utils': 4.0.18 + '@vitest/utils': 4.1.5 pathe: 2.0.3 - '@vitest/snapshot@4.0.18': + '@vitest/snapshot@4.1.5': dependencies: - '@vitest/pretty-format': 4.0.18 + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.5': {} - '@vitest/utils@4.0.18': + '@vitest/utils@4.1.5': dependencies: - '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.0.3 + '@vitest/pretty-format': 4.1.5 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 '@volar/language-core@2.4.28': dependencies: @@ -5495,196 +5901,99 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue/compiler-core@3.5.29': + '@vue/compiler-core@3.5.34': dependencies: - '@babel/parser': 7.29.0 - '@vue/shared': 3.5.29 + '@babel/parser': 7.29.3 + '@vue/shared': 3.5.34 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.29': + '@vue/compiler-dom@3.5.34': dependencies: - '@vue/compiler-core': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/compiler-core': 3.5.34 + '@vue/shared': 3.5.34 - '@vue/compiler-sfc@3.5.29': + '@vue/compiler-sfc@3.5.34': dependencies: - '@babel/parser': 7.29.0 - '@vue/compiler-core': 3.5.29 - '@vue/compiler-dom': 3.5.29 - '@vue/compiler-ssr': 3.5.29 - '@vue/shared': 3.5.29 + '@babel/parser': 7.29.3 + '@vue/compiler-core': 3.5.34 + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.8 + postcss: 8.5.14 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.29': + '@vue/compiler-ssr@3.5.34': dependencies: - '@vue/compiler-dom': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/compiler-dom': 3.5.34 + '@vue/shared': 3.5.34 - '@vue/language-core@3.2.5': + '@vue/language-core@3.2.8': dependencies: '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/compiler-dom': 3.5.34 + '@vue/shared': 3.5.34 alien-signals: 3.1.2 muggle-string: 0.4.1 path-browserify: 1.0.1 - picomatch: 4.0.3 + picomatch: 4.0.4 - '@vue/reactivity@3.5.29': + '@vue/reactivity@3.5.34': dependencies: - '@vue/shared': 3.5.29 + '@vue/shared': 3.5.34 - '@vue/runtime-core@3.5.29': + '@vue/runtime-core@3.5.34': dependencies: - '@vue/reactivity': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/reactivity': 3.5.34 + '@vue/shared': 3.5.34 - '@vue/runtime-dom@3.5.29': + '@vue/runtime-dom@3.5.34': dependencies: - '@vue/reactivity': 3.5.29 - '@vue/runtime-core': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/reactivity': 3.5.34 + '@vue/runtime-core': 3.5.34 + '@vue/shared': 3.5.34 csstype: 3.2.3 - '@vue/server-renderer@3.5.29(vue@3.5.29(typescript@5.9.3))': + '@vue/server-renderer@3.5.34(vue@3.5.34(typescript@6.0.3))': dependencies: - '@vue/compiler-ssr': 3.5.29 - '@vue/shared': 3.5.29 - vue: 3.5.29(typescript@5.9.3) + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + vue: 3.5.34(typescript@6.0.3) - '@vue/shared@3.5.29': {} + '@vue/shared@3.5.34': {} - '@webassemblyjs/ast@1.14.1': - dependencies: - '@webassemblyjs/helper-numbers': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - - '@webassemblyjs/floating-point-hex-parser@1.13.2': {} - - '@webassemblyjs/helper-api-error@1.13.2': {} - - '@webassemblyjs/helper-buffer@1.14.1': {} - - '@webassemblyjs/helper-numbers@1.13.2': - dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.13.2 - '@webassemblyjs/helper-api-error': 1.13.2 - '@xtuc/long': 4.2.2 - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} - - '@webassemblyjs/helper-wasm-section@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/wasm-gen': 1.14.1 - - '@webassemblyjs/ieee754@1.13.2': - dependencies: - '@xtuc/ieee754': 1.2.0 - - '@webassemblyjs/leb128@1.13.2': - dependencies: - '@xtuc/long': 4.2.2 - - '@webassemblyjs/utf8@1.13.2': {} - - '@webassemblyjs/wasm-edit@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/helper-wasm-section': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-opt': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - '@webassemblyjs/wast-printer': 1.14.1 - - '@webassemblyjs/wasm-gen@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wasm-opt@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - - '@webassemblyjs/wasm-parser@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-api-error': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wast-printer@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@xtuc/long': 4.2.2 - - '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': - dependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) - - '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': - dependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) - - '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': - dependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) - - '@xtuc/ieee754@1.2.0': {} - - '@xtuc/long@4.2.2': {} - - acorn-import-phases@1.0.4(acorn@8.16.0): + acorn-globals@7.0.1: dependencies: acorn: 8.16.0 + acorn-walk: 8.3.5 acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + acorn@8.16.0: {} - add-asset-webpack-plugin@3.1.1(webpack@5.105.4): - optionalDependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - - ajv-formats@2.1.1(ajv@8.18.0): - optionalDependencies: - ajv: 8.18.0 - - ajv-keywords@5.1.0(ajv@8.18.0): + agent-base@6.0.2: dependencies: - ajv: 8.18.0 - fast-deep-equal: 3.1.3 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color - ajv@6.14.0: + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.18.0: + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 fast-uri: 3.1.0 @@ -5701,7 +6010,7 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.3: {} + ansi-styles@5.2.0: {} ansi_up@6.0.6: {} @@ -5710,7 +6019,7 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.2 arg@5.0.2: {} @@ -5718,13 +6027,11 @@ snapshots: aria-query@5.3.2: {} - array-find-index@1.0.2: {} - asciinema-player@3.15.1: dependencies: - '@babel/runtime': 7.28.6 - solid-js: 1.9.11 - solid-transition-group: 0.2.3(solid-js@1.9.11) + '@babel/runtime': 7.29.2 + solid-js: 1.9.12 + solid-transition-group: 0.2.3(solid-js@1.9.12) assertion-error@2.0.1: {} @@ -5732,7 +6039,9 @@ snapshots: astral-regex@2.0.0: {} - axe-core@4.11.1: {} + asynckit@0.4.0: {} + + axe-core@4.11.4: {} axobject-query@4.1.0: {} @@ -5742,20 +6051,18 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.0: {} - - big.js@5.2.2: {} + baseline-browser-mapping@2.10.27: {} binary-extensions@2.3.0: {} boolbase@1.0.0: {} - brace-expansion@1.1.12: + brace-expansion@1.1.14: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.4: + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -5763,15 +6070,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.1: + browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.0 - caniuse-lite: 1.0.30001777 - electron-to-chromium: 1.5.307 - node-releases: 2.0.36 - update-browserslist-db: 1.2.3(browserslist@4.28.1) - - buffer-from@1.1.2: {} + baseline-browser-mapping: 2.10.27 + caniuse-lite: 1.0.30001791 + electron-to-chromium: 1.5.349 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) buffer@5.7.1: dependencies: @@ -5780,23 +6085,23 @@ snapshots: builtin-modules@3.3.0: {} - builtin-modules@5.0.0: {} + builtin-modules@5.1.0: {} bytes@3.1.2: {} - cacheable@2.3.3: + cacheable@2.3.4: dependencies: '@cacheable/memory': 2.0.8 - '@cacheable/utils': 2.4.0 + '@cacheable/utils': 2.4.1 hookified: 1.15.1 keyv: 5.6.0 - qified: 0.6.0 + qified: 0.9.1 callsites@3.1.0: {} camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001777: {} + caniuse-lite@1.0.30001791: {} chai@6.2.2: {} @@ -5817,10 +6122,10 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 - chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.19): + chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.20): dependencies: chart.js: 4.5.1 - dayjs: 1.11.19 + dayjs: 1.11.20 chartjs-plugin-zoom@2.2.0(chart.js@4.5.1): dependencies: @@ -5828,20 +6133,6 @@ snapshots: chart.js: 4.5.1 hammerjs: 2.0.8 - chevrotain-allstar@0.3.1(chevrotain@11.1.2): - dependencies: - chevrotain: 11.1.2 - lodash-es: 4.17.23 - - chevrotain@11.1.2: - dependencies: - '@chevrotain/cst-dts-gen': 11.1.2 - '@chevrotain/gast': 11.1.2 - '@chevrotain/regexp-to-ast': 11.1.2 - '@chevrotain/types': 11.1.2 - '@chevrotain/utils': 11.1.2 - lodash-es: 4.17.23 - chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -5856,7 +6147,7 @@ snapshots: chroma-js@3.2.0: {} - chrome-trace-event@1.0.4: {} + ci-info@3.9.0: {} ci-info@4.4.0: {} @@ -5866,13 +6157,12 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 - clippie@4.1.10: {} + clippie@4.1.15: {} - clone-deep@4.0.1: + codemirror-lang-elixir@4.0.1: dependencies: - is-plain-object: 2.0.4 - kind-of: 6.0.3 - shallow-clone: 3.0.1 + '@codemirror/language': 6.12.3 + lezer-elixir: 1.1.3 codemirror-spell-checker@1.1.2: dependencies: @@ -5888,23 +6178,21 @@ snapshots: colord@2.9.3: {} - colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 commander@11.1.0: {} - commander@12.1.0: {} - commander@14.0.3: {} - commander@2.20.3: {} - commander@4.1.1: {} commander@7.2.0: {} commander@8.3.0: {} - comment-parser@1.4.5: {} + comment-parser@1.4.6: {} compare-versions@6.1.1: {} @@ -5912,9 +6200,11 @@ snapshots: confbox@0.1.8: {} - core-js-compat@3.48.0: + convert-source-map@2.0.0: {} + + core-js-compat@3.49.0: dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 core-js@3.32.2: {} @@ -5926,14 +6216,16 @@ snapshots: dependencies: layout-base: 2.0.1 - cosmiconfig@9.0.1(typescript@5.9.3): + 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: 5.9.3 + typescript: 6.0.3 + + crelt@1.0.6: {} cropperjs@1.6.2: {} @@ -5945,19 +6237,6 @@ snapshots: css-functions-list@3.3.3: {} - css-loader@7.1.4(webpack@5.105.4): - dependencies: - icss-utils: 5.1.0(postcss@8.5.8) - postcss: 8.5.8 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.8) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.8) - postcss-modules-scope: 3.2.1(postcss@8.5.8) - postcss-modules-values: 4.0.0(postcss@8.5.8) - postcss-value-parser: 4.2.0 - semver: 7.7.4 - optionalDependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -5984,19 +6263,27 @@ snapshots: dependencies: css-tree: 2.2.1 + cssom@0.3.8: {} + + cssom@0.5.0: {} + + cssstyle@2.3.0: + dependencies: + cssom: 0.3.8 + csstype@3.2.3: {} - cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.3): dependencies: cose-base: 1.0.3 - cytoscape: 3.33.1 + cytoscape: 3.33.3 - cytoscape-fcose@2.2.0(cytoscape@3.33.1): + cytoscape-fcose@2.2.0(cytoscape@3.33.3): dependencies: cose-base: 2.2.0 - cytoscape: 3.33.1 + cytoscape: 3.33.3 - cytoscape@3.33.1: {} + cytoscape@3.33.3: {} d3-array@2.12.1: dependencies: @@ -6028,7 +6315,7 @@ snapshots: d3-delaunay@6.0.4: dependencies: - delaunator: 5.0.1 + delaunator: 5.1.0 d3-dispatch@3.0.1: {} @@ -6165,14 +6452,20 @@ snapshots: d3-transition: 3.0.1(d3-selection@3.0.0) d3-zoom: 3.0.0 - dagre-d3-es@7.0.13: + dagre-d3-es@7.0.14: dependencies: d3: 7.9.0 - lodash-es: 4.17.23 + lodash-es: 4.18.1 damerau-levenshtein@1.0.8: {} - dayjs@1.11.19: {} + data-urls@3.0.2: + dependencies: + abab: '@nolyfill/abab@1.0.44' + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + + dayjs@1.11.20: {} debug@3.2.7: dependencies: @@ -6182,6 +6475,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -6195,12 +6490,16 @@ snapshots: kind-of: 3.2.2 rename-keys: 1.2.0 - delaunator@5.0.1: + delaunator@5.1.0: dependencies: - robust-predicates: 3.0.2 + robust-predicates: 3.0.3 + + delayed-stream@1.0.0: {} dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -6223,15 +6522,15 @@ snapshots: domelementtype@2.3.0: {} + domexception@4.0.0: + dependencies: + webidl-conversions: 7.0.0 + domhandler@5.0.3: dependencies: domelementtype: 2.3.0 - dompurify@3.2.7: - optionalDependencies: - '@types/trusted-types': 2.0.7 - - dompurify@3.3.2: + dompurify@3.4.2: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -6241,12 +6540,7 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - dropzone@6.0.0-beta.2: - dependencies: - '@swc/helpers': 0.2.14 - just-extend: 5.1.1 - - easymde@2.20.0: + easymde@2.21.0: dependencies: '@types/codemirror': 5.60.17 '@types/marked': 4.3.2 @@ -6254,7 +6548,7 @@ snapshots: codemirror-spell-checker: 1.1.2 marked: 4.3.0 - electron-to-chromium@1.5.307: {} + electron-to-chromium@1.5.349: {} elkjs@0.9.3: {} @@ -6262,201 +6556,198 @@ snapshots: emoji-regex@9.2.2: {} - emojis-list@3.0.0: {} - - enhanced-resolve@5.20.0: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.0 - entities@4.5.0: {} + entities@6.0.1: {} + entities@7.0.1: {} env-paths@2.2.1: {} - envinfo@7.21.0: {} - error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 - es-module-lexer@1.7.0: {} + es-errors@1.3.0: {} - es-module-lexer@2.0.0: {} + es-module-lexer@2.1.0: {} - esbuild-loader@4.4.2(webpack@5.105.4): - dependencies: - esbuild: 0.27.3 - get-tsconfig: 4.13.6 - loader-utils: 2.0.4 - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-sources: 1.4.3 + es-toolkit@1.46.1: {} - esbuild@0.27.3: + esbuild@0.28.0: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 escalade@3.2.0: {} escape-string-regexp@1.0.5: {} + escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)): + escodegen@2.1.0: dependencies: - eslint: 10.0.3(jiti@2.6.1) + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@2.7.0)): + dependencies: + eslint: 10.3.0(jiti@2.7.0) eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: - get-tsconfig: 4.13.6 + get-tsconfig: 4.14.0 stable-hash-x: 0.2.0 optionalDependencies: unrs-resolver: 1.11.1 - eslint-import-resolver-node@0.3.9: + eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 is-core-module: '@nolyfill/is-core-module@1.0.39' - resolve: 1.22.11 + resolve: 2.0.0-next.6 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.0.3(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.3.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.3.0(jiti@2.7.0)): dependencies: debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) - get-tsconfig: 4.13.6 + get-tsconfig: 4.14.0 is-bun-module: 2.0.0 stable-hash-x: 0.2.0 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.7.0)) + eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.3.0(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.0.3(jiti@2.6.1)) + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.3.0(jiti@2.7.0) + 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.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.3.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.3.0(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-array-func@5.1.1(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-array-func@5.1.1(eslint@10.3.0(jiti@2.7.0)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) - eslint-plugin-de-morgan@2.1.1(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-de-morgan@2.1.1(eslint@10.3.0(jiti@2.7.0)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) - eslint-plugin-escompat@3.11.4(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-escompat@3.11.4(eslint@10.3.0(jiti@2.7.0)): dependencies: - browserslist: 4.28.1 - eslint: 10.0.3(jiti@2.6.1) + browserslist: 4.28.2 + eslint: 10.3.0(jiti@2.7.0) - eslint-plugin-eslint-comments@3.2.0(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-eslint-comments@3.2.0(eslint@10.3.0(jiti@2.7.0)): dependencies: escape-string-regexp: 1.0.5 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) ignore: 5.3.2 - eslint-plugin-filenames@1.3.2(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-filenames@1.3.2(eslint@10.3.0(jiti@2.7.0)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) 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(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-github@6.0.0(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.7.0)): dependencies: - '@eslint/compat': 1.4.1(eslint@10.0.3(jiti@2.6.1)) - '@eslint/eslintrc': 3.3.4 - '@eslint/js': 9.39.3 + '@eslint/compat': 1.4.1(eslint@10.3.0(jiti@2.7.0)) + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 '@github/browserslist-config': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3) aria-query: 5.3.2 - eslint: 10.0.3(jiti@2.6.1) - eslint-config-prettier: 10.1.8(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-escompat: 3.11.4(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-eslint-comments: 3.2.0(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-filenames: 1.3.2(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-i18n-text: 1.0.1(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)))(eslint@10.0.3(jiti@2.6.1))(prettier@3.8.1) + eslint: 10.3.0(jiti@2.7.0) + eslint-config-prettier: 10.1.8(eslint@10.3.0(jiti@2.7.0)) + eslint-plugin-escompat: 3.11.4(eslint@10.3.0(jiti@2.7.0)) + eslint-plugin-eslint-comments: 3.2.0(eslint@10.3.0(jiti@2.7.0)) + eslint-plugin-filenames: 1.3.2(eslint@10.3.0(jiti@2.7.0)) + eslint-plugin-i18n-text: 1.0.1(eslint@10.3.0(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.7.0)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@10.3.0(jiti@2.7.0)) + eslint-plugin-no-only-tests: 3.4.0 + eslint-plugin-prettier: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@2.7.0)))(eslint@10.3.0(jiti@2.7.0))(prettier@3.8.3) eslint-rule-documentation: 1.0.23 globals: 16.5.0 jsx-ast-utils: 3.3.5 - prettier: 3.8.1 + prettier: 3.8.3 svg-element-attributes: 1.3.1 typescript: 5.9.3 - typescript-eslint: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + typescript-eslint: 8.59.2(eslint@10.3.0(jiti@2.7.0))(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.0.3(jiti@2.6.1)): + eslint-plugin-i18n-text@1.0.1(eslint@10.3.0(jiti@2.7.0)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) - eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.3.0(jiti@2.7.0)): dependencies: '@package-json/types': 0.0.12 - '@typescript-eslint/types': 8.56.1 - comment-parser: 1.4.5 + '@typescript-eslint/types': 8.59.2 + comment-parser: 1.4.6 debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 - minimatch: 10.2.4 - semver: 7.7.4 + minimatch: 10.2.5 + semver: 7.8.0 stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - eslint-import-resolver-node: 0.3.9 + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: '@nolyfill/array-includes@1.0.44' @@ -6465,9 +6756,9 @@ snapshots: array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' debug: 3.2.7 doctrine: 2.1.0 - eslint: 10.0.3(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) + eslint: 10.3.0(jiti@2.7.0) + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.3.0(jiti@2.7.0)) hasown: '@nolyfill/hasown@1.0.44' is-core-module: '@nolyfill/is-core-module@1.0.39' is-glob: 4.0.3 @@ -6479,23 +6770,23 @@ snapshots: string.prototype.trimend: '@nolyfill/string.prototype.trimend@1.0.44' tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@10.3.0(jiti@2.7.0)): dependencies: aria-query: 5.3.2 array-includes: '@nolyfill/array-includes@1.0.44' array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' ast-types-flow: 0.0.8 - axe-core: 4.11.1 + axe-core: 4.11.4 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) hasown: '@nolyfill/hasown@1.0.44' jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -6504,114 +6795,101 @@ snapshots: safe-regex-test: '@nolyfill/safe-regex-test@1.0.44' string.prototype.includes: '@nolyfill/string.prototype.includes@1.0.44' - eslint-plugin-no-only-tests@3.3.0: {} + eslint-plugin-no-only-tests@3.4.0: {} - eslint-plugin-playwright@2.10.1(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-playwright@2.10.2(eslint@10.3.0(jiti@2.7.0)): dependencies: - eslint: 10.0.3(jiti@2.6.1) - globals: 17.4.0 + eslint: 10.3.0(jiti@2.7.0) + globals: 17.6.0 - eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)))(eslint@10.0.3(jiti@2.6.1))(prettier@3.8.1): + eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@2.7.0)))(eslint@10.3.0(jiti@2.7.0))(prettier@3.8.3): dependencies: - eslint: 10.0.3(jiti@2.6.1) - prettier: 3.8.1 + eslint: 10.3.0(jiti@2.7.0) + prettier: 3.8.3 prettier-linter-helpers: 1.0.1 synckit: 0.11.12 optionalDependencies: - '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@10.0.3(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@10.3.0(jiti@2.7.0)) - eslint-plugin-regexp@3.1.0(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-regexp@3.1.0(eslint@10.3.0(jiti@2.7.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 - comment-parser: 1.4.5 - eslint: 10.0.3(jiti@2.6.1) - jsdoc-type-pratt-parser: 7.1.1 + comment-parser: 1.4.6 + eslint: 10.3.0(jiti@2.7.0) + 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.0.3(jiti@2.6.1)): + eslint-plugin-sonarjs@4.0.3(eslint@10.3.0(jiti@2.7.0)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) functional-red-black-tree: 1.0.1 - globals: 17.4.0 + globals: 17.6.0 jsx-ast-utils-x: 0.1.0 lodash.merge: 4.6.2 - minimatch: 10.2.4 + minimatch: 10.2.5 scslre: 0.3.0 - semver: 7.7.4 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 + semver: 7.8.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 - eslint-plugin-unicorn@63.0.0(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-unicorn@64.0.0(eslint@10.3.0(jiti@2.7.0)): dependencies: '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0)) change-case: 5.4.4 ci-info: 4.4.0 clean-regexp: 1.0.0 - core-js-compat: 3.48.0 - eslint: 10.0.3(jiti@2.6.1) + core-js-compat: 3.49.0 + eslint: 10.3.0(jiti@2.7.0) find-up-simple: 1.0.1 - globals: 16.5.0 + globals: 17.6.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 jsesc: 3.1.0 pluralize: 8.0.0 regexp-tree: 0.1.27 - regjsparser: 0.13.0 - semver: 7.7.4 + regjsparser: 0.13.1 + semver: 7.8.0 strip-indent: 4.1.1 - eslint-plugin-vue-scoped-css@3.0.0(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))): + eslint-plugin-vue-scoped-css@3.0.0(eslint@10.3.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.7.0))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - eslint: 10.0.3(jiti@2.6.1) - lodash: 4.17.23 - postcss: 8.5.8 - postcss-safe-parser: 7.0.1(postcss@8.5.8) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0)) + eslint: 10.3.0(jiti@2.7.0) + lodash: 4.18.1 + postcss: 8.5.14 + postcss-safe-parser: 7.0.1(postcss@8.5.14) postcss-selector-parser: 7.1.1 - vue-eslint-parser: 10.4.0(eslint@10.0.3(jiti@2.6.1)) + vue-eslint-parser: 10.4.0(eslint@10.3.0(jiti@2.7.0)) - eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.0.3(jiti@2.6.1)))(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))): + eslint-plugin-vue@10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.3.0(jiti@2.7.0)))(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.7.0))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - eslint: 10.0.3(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0)) + eslint: 10.3.0(jiti@2.7.0) 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.0.3(jiti@2.6.1)) + semver: 7.8.0 + vue-eslint-parser: 10.4.0(eslint@10.3.0(jiti@2.7.0)) xml-name-validator: 4.0.0 optionalDependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.3.0(jiti@2.7.0)) + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-wc@3.1.0(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-wc@3.1.0(eslint@10.3.0(jiti@2.7.0)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.3.0(jiti@2.7.0) is-valid-element-name: 1.0.0 js-levenshtein-esm: 2.0.0 eslint-rule-documentation@1.0.23: {} - eslint-scope@5.1.1: - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - - eslint-scope@9.1.1: - dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 @@ -6625,25 +6903,25 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.0.3(jiti@2.6.1): + eslint@10.3.0(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.3 - '@eslint/config-helpers': 0.5.3 - '@eslint/core': 1.1.1 - '@eslint/plugin-kit': 0.6.1 - '@humanfs/node': 0.16.7 + '@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.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - ajv: 6.14.0 + ajv: 6.15.0 cross-spawn: 7.0.6 debug: 4.4.3 escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 - espree: 11.1.1 + espree: 11.2.0 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -6654,11 +6932,11 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.4 + minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.6.1 + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -6668,12 +6946,14 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 - espree@11.1.1: + espree@11.2.0: dependencies: acorn: 8.16.0 acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 5.0.1 + esprima@4.0.1: {} + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -6682,8 +6962,6 @@ snapshots: dependencies: estraverse: 5.3.0 - estraverse@4.3.0: {} - estraverse@5.3.0: {} estree-walker@2.0.2: {} @@ -6724,9 +7002,9 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 fetch-ponyfill@7.1.0: dependencies: @@ -6738,7 +7016,7 @@ snapshots: file-entry-cache@11.1.2: dependencies: - flat-cache: 6.1.20 + flat-cache: 6.1.22 file-entry-cache@8.0.0: dependencies: @@ -6750,11 +7028,6 @@ snapshots: find-up-simple@1.0.1: {} - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -6762,20 +7035,24 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.4 + flatted: 3.4.2 keyv: 4.5.4 - flat-cache@6.1.20: + flat-cache@6.1.22: dependencies: - cacheable: 2.3.3 - flatted: 3.3.4 + cacheable: 2.3.4 + flatted: 3.4.2 hookified: 1.15.1 - flat@5.0.2: {} + flatted@3.4.2: {} - flatted@3.3.4: {} - - fs.realpath@1.0.0: {} + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: '@nolyfill/es-set-tostringtag@1.0.44' + hasown: '@nolyfill/hasown@1.0.44' + mime-types: 2.1.35 fsevents@2.3.2: optional: true @@ -6787,7 +7064,7 @@ snapshots: get-east-asian-width@1.5.0: {} - get-tsconfig@4.13.6: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -6799,17 +7076,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-to-regexp@0.4.1: {} - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.5 - once: 1.4.0 - path-is-absolute: 1.0.1 - global-modules@2.0.0: dependencies: global-prefix: 3.0.0 @@ -6824,9 +7090,9 @@ snapshots: globals@16.5.0: {} - globals@17.4.0: {} + globals@17.6.0: {} - globby@16.1.1: + globby@16.2.0: dependencies: '@sindresorhus/merge-streams': 4.0.0 fast-glob: 3.3.3 @@ -6843,14 +7109,14 @@ snapshots: hammerjs@2.0.8: {} - happy-dom@20.8.3: + happy-dom@20.9.0: dependencies: - '@types/node': 25.3.5 + '@types/node': 25.6.0 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.19.0 + ws: 8.20.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -6859,14 +7125,18 @@ snapshots: has-flag@5.0.1: {} - hash-sum@2.0.0: {} - - hashery@1.5.0: + hashery@1.5.1: dependencies: hookified: 1.15.1 hookified@1.15.1: {} + hookified@2.2.0: {} + + html-encoding-sniffer@3.0.0: + dependencies: + whatwg-encoding: 2.0.0 + html-tags@5.1.0: {} htmlparser2@8.0.2: @@ -6876,16 +7146,25 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 - htmx.org@2.0.8: {} + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.1 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color iconv-lite@0.6.3: dependencies: safer-buffer: '@nolyfill/safer-buffer@1.0.44' - icss-utils@5.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - idiomorph@0.7.4: {} ieee754@1.2.1: {} @@ -6899,24 +7178,12 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-local@3.2.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - import-meta-resolve@4.2.0: {} imurmurhash@0.1.4: {} indent-string@5.0.0: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - ini@1.3.8: {} ini@4.1.3: {} @@ -6925,8 +7192,6 @@ snapshots: internmap@2.0.3: {} - interpret@3.1.1: {} - is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -6944,11 +7209,11 @@ snapshots: is-builtin-module@5.0.0: dependencies: - builtin-modules: 5.0.0 + builtin-modules: 5.1.0 is-bun-module@2.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.0 is-decimal@2.0.1: {} @@ -6966,10 +7231,6 @@ snapshots: is-path-inside@4.0.0: {} - is-plain-object@2.0.4: - dependencies: - isobject: 3.0.1 - is-plain-object@5.0.0: {} is-potential-custom-element-name@1.0.1: {} @@ -6980,17 +7241,51 @@ snapshots: isexe@2.0.0: {} - isobject@3.0.1: {} - - jest-worker@27.5.1: + jest-environment-jsdom@29.7.0: dependencies: - '@types/node': 25.3.5 - merge-stream: 2.0.0 - supports-color: 8.1.1 + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/jsdom': 20.0.1 + '@types/node': 25.6.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + jest-util: 29.7.0 + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.2 jiti@1.21.7: {} - jiti@2.6.1: {} + jiti@2.7.0: {} jquery@4.0.0: {} @@ -7004,7 +7299,40 @@ snapshots: dependencies: argparse: 2.0.1 - jsdoc-type-pratt-parser@7.1.1: {} + jsdoc-type-pratt-parser@7.2.0: {} + + jsdom@20.0.3: + dependencies: + abab: '@nolyfill/abab@1.0.44' + acorn: 8.16.0 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.6.0 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.5 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.20.0 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate jsesc@3.1.0: {} @@ -7022,8 +7350,6 @@ snapshots: dependencies: minimist: 1.2.8 - json5@2.2.3: {} - jsonc-parser@3.3.1: {} jsonpointer@5.0.1: {} @@ -7037,9 +7363,7 @@ snapshots: object.assign: '@nolyfill/object.assign@1.0.44' object.values: '@nolyfill/object.values@1.0.44' - just-extend@5.1.1: {} - - katex@0.16.37: + katex@0.16.45: dependencies: commander: 8.3.0 @@ -7059,14 +7383,6 @@ snapshots: kind-of@6.0.3: {} - langium@4.2.1: - dependencies: - chevrotain: 11.1.2 - chevrotain-allstar: 0.3.1(chevrotain@11.1.2) - vscode-languageserver: 9.0.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -7082,6 +7398,60 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lezer-elixir@1.1.3: + dependencies: + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -7090,23 +7460,11 @@ snapshots: dependencies: uc.micro: 2.1.0 - loader-runner@4.3.1: {} - - loader-utils@2.0.4: - dependencies: - big.js: 5.2.2 - emojis-list: 3.0.0 - json5: 2.2.3 - - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - locate-path@6.0.0: dependencies: p-locate: 5.0.0 - lodash-es@4.17.23: {} + lodash-es@4.18.1: {} lodash.camelcase@4.3.0: {} @@ -7120,7 +7478,7 @@ snapshots: lodash.upperfirst@4.3.1: {} - lodash@4.17.23: {} + lodash@4.18.1: {} magic-string@0.30.21: dependencies: @@ -7145,10 +7503,10 @@ snapshots: jsonpointer: 5.0.1 markdown-it: 14.1.1 markdownlint: 0.40.0 - minimatch: 10.2.4 + minimatch: 10.2.5 run-con: 1.3.2 - smol-toml: 1.6.0 - tinyglobby: 0.2.15 + smol-toml: 1.6.1 + tinyglobby: 0.2.16 transitivePeerDependencies: - supports-color @@ -7166,13 +7524,11 @@ snapshots: transitivePeerDependencies: - supports-color - marked@14.0.0: {} - marked@16.4.2: {} marked@4.3.0: {} - material-icon-theme@5.32.0: + material-icon-theme@5.34.0: dependencies: chroma-js: 3.2.0 events: 3.3.0 @@ -7189,32 +7545,31 @@ snapshots: meow@14.1.0: {} - merge-stream@2.0.0: {} - merge2@1.4.1: {} - mermaid@11.12.3: + mermaid@11.15.0: dependencies: '@braintree/sanitize-url': 7.1.2 - '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.0.0 + '@iconify/utils': 3.1.1 + '@mermaid-js/parser': 1.1.1 '@types/d3': 7.4.3 - cytoscape: 3.33.1 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) - cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.33.3 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.3) + cytoscape-fcose: 2.2.0(cytoscape@3.33.3) d3: 7.9.0 d3-sankey: 0.12.3 - dagre-d3-es: 7.0.13 - dayjs: 1.11.19 - dompurify: 3.3.2 - katex: 0.16.37 + dagre-d3-es: 7.0.14 + dayjs: 1.11.20 + dompurify: 3.4.2 + es-toolkit: 1.46.1 + katex: 0.16.45 khroma: 2.1.0 - lodash-es: 4.17.23 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 + uuid: 11.1.1 micromark-core-commonmark@2.0.3: dependencies: @@ -7275,7 +7630,7 @@ snapshots: dependencies: '@types/katex': 0.16.8 devlop: 1.1.0 - katex: 0.16.37 + katex: 0.16.45 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 @@ -7368,7 +7723,7 @@ snapshots: micromark@4.0.2: dependencies: - '@types/debug': 4.1.12 + '@types/debug': 4.1.13 debug: 4.4.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 @@ -7391,7 +7746,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 mime-db@1.52.0: {} @@ -7399,39 +7754,22 @@ snapshots: dependencies: mime-db: 1.52.0 - mini-css-extract-plugin@2.10.0(webpack@5.105.4): + minimatch@10.2.5: dependencies: - schema-utils: 4.3.3 - tapable: 2.3.0 - webpack: 5.105.4(webpack-cli@6.0.1) - - minimatch@10.2.4: - dependencies: - brace-expansion: 5.0.4 + brace-expansion: 5.0.5 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 1.1.14 minimist@1.2.8: {} - mlly@1.8.1: + mlly@1.8.2: dependencies: acorn: 8.16.0 pathe: 2.0.3 pkg-types: 1.3.1 - ufo: 1.6.3 - - monaco-editor-webpack-plugin@7.1.1(monaco-editor@0.55.1)(webpack@5.105.4): - dependencies: - loader-utils: 2.0.4 - monaco-editor: 0.55.1 - webpack: 5.105.4(webpack-cli@6.0.1) - - monaco-editor@0.55.1: - dependencies: - dompurify: 3.2.7 - marked: 14.0.0 + ufo: 1.6.4 moo@0.5.3: {} @@ -7445,7 +7783,7 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.11: {} + nanoid@3.3.12: {} nanopop@2.3.0: {} @@ -7453,7 +7791,12 @@ snapshots: natural-compare@1.4.0: {} - neo-async@2.6.2: {} + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' + es-errors: 1.3.0 + object.entries: '@nolyfill/object.entries@1.0.44' + semver: 6.3.1 node-fetch@2.6.13: dependencies: @@ -7463,7 +7806,7 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-releases@2.0.36: {} + node-releases@2.0.38: {} nolyfill@1.0.44: {} @@ -7473,16 +7816,14 @@ snapshots: dependencies: boolbase: 1.0.0 + nwsapi@2.2.23: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} obug@2.1.1: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - online-3d-viewer@0.18.0: dependencies: '@simonwep/pickr': 1.9.0 @@ -7498,24 +7839,14 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - p-locate@5.0.0: dependencies: p-limit: 3.1.0 - p-try@2.2.0: {} - package-manager-detector@1.6.0: {} parent-module@1.0.1: @@ -7539,14 +7870,16 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + path-browserify@1.0.1: {} path-data-parser@0.1.0: {} path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} - path-key@3.1.1: {} path-parse@1.0.7: {} @@ -7559,29 +7892,25 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} - picomatch@4.0.3: {} + picomatch@4.0.4: {} pify@2.3.0: {} pirates@4.0.7: {} - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - pkg-types@1.3.1: dependencies: confbox: 0.1.8 - mlly: 1.8.1 + mlly: 1.8.2 pathe: 2.0.3 - playwright-core@1.58.2: {} + playwright-core@1.59.1: {} - playwright@1.58.2: + playwright@1.59.1: dependencies: - playwright-core: 1.58.2 + playwright-core: 1.59.1 optionalDependencies: fsevents: 2.3.2 @@ -7598,72 +7927,40 @@ snapshots: dependencies: htmlparser2: 8.0.2 js-tokens: 9.0.1 - postcss: 8.5.8 - postcss-safe-parser: 6.0.0(postcss@8.5.8) + postcss: 8.5.14 + postcss-safe-parser: 6.0.0(postcss@8.5.14) - postcss-import@15.1.0(postcss@8.5.8): + postcss-import@15.1.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.11 + resolve: 1.22.12 - postcss-js@4.1.0(postcss@8.5.8): + postcss-js@4.1.0(postcss@8.5.14): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.8 + postcss: 8.5.14 - postcss-load-config@4.0.2(postcss@8.5.8): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.14): dependencies: lilconfig: 3.1.3 - yaml: 2.8.2 optionalDependencies: - postcss: 8.5.8 + jiti: 1.21.7 + postcss: 8.5.14 - postcss-loader@8.2.1(postcss@8.5.8)(typescript@5.9.3)(webpack@5.105.4): + postcss-nested@6.2.0(postcss@8.5.14): dependencies: - cosmiconfig: 9.0.1(typescript@5.9.3) - jiti: 2.6.1 - postcss: 8.5.8 - semver: 7.7.4 - optionalDependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - transitivePeerDependencies: - - typescript - - postcss-modules-extract-imports@3.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - postcss-modules-local-by-default@4.2.0(postcss@8.5.8): - dependencies: - icss-utils: 5.1.0(postcss@8.5.8) - postcss: 8.5.8 - postcss-selector-parser: 7.1.1 - postcss-value-parser: 4.2.0 - - postcss-modules-scope@3.2.1(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-selector-parser: 7.1.1 - - postcss-modules-values@4.0.0(postcss@8.5.8): - dependencies: - icss-utils: 5.1.0(postcss@8.5.8) - postcss: 8.5.8 - - postcss-nested@6.2.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-selector-parser: 6.1.2 - postcss-safe-parser@6.0.0(postcss@8.5.8): + postcss-safe-parser@6.0.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 - postcss-safe-parser@7.0.1(postcss@8.5.8): + postcss-safe-parser@7.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-selector-parser@6.1.2: dependencies: @@ -7677,9 +7974,9 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.8: + postcss@8.5.14: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -7689,29 +7986,39 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.8.1: {} + prettier@3.8.3: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + psl@1.15.0: + dependencies: + punycode: 2.3.1 punycode.js@2.3.1: {} punycode@2.3.1: {} - qified@0.6.0: + qified@0.9.1: dependencies: - hookified: 1.15.1 + hookified: 2.2.0 + + querystringify@2.2.0: {} queue-microtask@1.2.3: {} + react-is@18.3.1: {} + read-cache@1.0.0: dependencies: pify: 2.3.0 readdirp@3.6.0: dependencies: - picomatch: 2.3.1 - - rechoir@0.8.0: - dependencies: - resolve: 1.22.11 + picomatch: 2.3.2 refa@0.12.1: dependencies: @@ -7724,7 +8031,7 @@ snapshots: regexp-tree@0.1.27: {} - regjsparser@0.13.0: + regjsparser@0.13.1: dependencies: jsesc: 3.1.0 @@ -7732,56 +8039,56 @@ snapshots: require-from-string@2.0.2: {} - resolve-cwd@3.0.0: - dependencies: - resolve-from: 5.0.0 + requires-port@1.0.0: {} resolve-from@4.0.0: {} - resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} - resolve@1.22.11: + resolve@1.22.12: dependencies: + es-errors: 1.3.0 is-core-module: '@nolyfill/is-core-module@1.0.39' path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + resolve@2.0.0-next.6: + dependencies: + es-errors: 1.3.0 + is-core-module: '@nolyfill/is-core-module@1.0.39' + node-exports-info: 1.6.0 + object-keys: '@nolyfill/object-keys@1.0.44' + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} - robust-predicates@3.0.2: {} + robust-predicates@3.0.3: {} - rollup@4.59.0: + rolldown-license-plugin@3.0.4(rolldown@1.0.0-rc.17): dependencies: - '@types/estree': 1.0.8 + rolldown: 1.0.0-rc.17 + + rolldown@1.0.0-rc.17: + dependencies: + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 - fsevents: 2.3.3 + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 roughjs@4.6.6: dependencies: @@ -7803,14 +8110,11 @@ snapshots: rw@1.3.3: {} - sax@1.5.0: {} + sax@1.6.0: {} - schema-utils@4.3.3: + saxes@6.0.0: dependencies: - '@types/json-schema': 7.0.15 - ajv: 8.18.0 - ajv-formats: 2.1.1(ajv@8.18.0) - ajv-keywords: 5.1.0(ajv@8.18.0) + xmlchars: 2.2.0 scslre@0.3.0: dependencies: @@ -7820,17 +8124,13 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: {} + semver@7.8.0: {} - seroval-plugins@1.5.0(seroval@1.5.0): + seroval-plugins@1.5.3(seroval@1.5.3): dependencies: - seroval: 1.5.0 + seroval: 1.5.3 - seroval@1.5.0: {} - - shallow-clone@3.0.1: - dependencies: - kind-of: 6.0.3 + seroval@1.5.3: {} shebang-command@2.0.0: dependencies: @@ -7842,6 +8142,8 @@ snapshots: signal-exit@4.1.0: {} + slash@3.0.0: {} + slash@5.1.0: {} slice-ansi@4.0.0: @@ -7850,69 +8152,40 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - smol-toml@1.6.0: {} + smol-toml@1.6.1: {} - solid-js@1.9.11: + solid-js@1.9.12: dependencies: csstype: 3.2.3 - seroval: 1.5.0 - seroval-plugins: 1.5.0(seroval@1.5.0) + seroval: 1.5.3 + seroval-plugins: 1.5.3(seroval@1.5.3) - solid-transition-group@0.2.3(solid-js@1.9.11): + solid-transition-group@0.2.3(solid-js@1.9.12): dependencies: - '@solid-primitives/refs': 1.1.3(solid-js@1.9.11) - '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.11) - solid-js: 1.9.11 + '@solid-primitives/refs': 1.1.3(solid-js@1.9.12) + '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.12) + solid-js: 1.9.12 sortablejs@1.15.7: {} - source-list-map@2.0.1: {} - source-map-js@1.2.1: {} - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 + source-map@0.6.1: + optional: true - source-map@0.6.1: {} - - spdx-compare@1.0.0: - dependencies: - array-find-index: 1.0.2 - spdx-expression-parse: 3.0.1 - spdx-ranges: 2.1.1 - - spdx-exceptions@2.5.0: {} - - spdx-expression-parse@3.0.1: - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.23 - - spdx-expression-validate@2.0.0: - dependencies: - spdx-expression-parse: 3.0.1 - - spdx-license-ids@3.0.23: {} - - spdx-ranges@2.1.1: {} - - spdx-satisfies@5.0.1: - dependencies: - spdx-compare: 1.0.0 - spdx-expression-parse: 3.0.1 - spdx-ranges: 2.1.1 - - spectral-cli-bundle@1.0.7: + spectral-cli-bundle@1.0.8: optionalDependencies: fsevents: 2.3.3 stable-hash-x@0.2.0: {} + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + stackback@0.0.2: {} - std-env@3.10.0: {} + std-env@4.1.0: {} string-width@4.2.3: dependencies: @@ -7925,7 +8198,7 @@ snapshots: get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 - string-width@8.2.0: + string-width@8.2.1: dependencies: get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 @@ -7944,37 +8217,39 @@ snapshots: strip-json-comments@3.1.1: {} + style-mod@4.1.3: {} + style-search@0.1.0: {} - stylelint-config-recommended@18.0.0(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-recommended@18.0.0(stylelint@17.11.0(typescript@6.0.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.11.0(typescript@6.0.3) - stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.4.0(typescript@5.9.3)): + stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.11.0(typescript@6.0.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.11.0(typescript@6.0.3) - stylelint-declaration-strict-value@1.11.1(stylelint@17.4.0(typescript@5.9.3)): + stylelint-declaration-strict-value@1.11.1(stylelint@17.11.0(typescript@6.0.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.11.0(typescript@6.0.3) - stylelint-value-no-unknown-custom-properties@6.1.1(stylelint@17.4.0(typescript@5.9.3)): + stylelint-value-no-unknown-custom-properties@6.1.1(stylelint@17.11.0(typescript@6.0.3)): dependencies: postcss-value-parser: 4.2.0 - resolve: 1.22.11 - stylelint: 17.4.0(typescript@5.9.3) + resolve: 1.22.12 + stylelint: 17.11.0(typescript@6.0.3) - stylelint@17.4.0(typescript@5.9.3): + stylelint@17.11.0(typescript@6.0.3): dependencies: - '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@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) - '@csstools/css-syntax-patches-for-csstree': 1.1.0 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) '@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) '@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@5.9.3) + cosmiconfig: 9.0.1(typescript@6.0.3) css-functions-list: 3.3.3 css-tree: 3.2.1 debug: 4.4.3 @@ -7982,23 +8257,22 @@ snapshots: fastest-levenshtein: 1.0.16 file-entry-cache: 11.1.2 global-modules: 2.0.0 - globby: 16.1.1 + globby: 16.2.0 globjoin: 0.1.4 html-tags: 5.1.0 ignore: 7.0.5 import-meta-resolve: 4.2.0 - imurmurhash: 0.1.4 is-plain-object: 5.0.0 mathml-tag-names: 4.0.0 meow: 14.1.0 micromatch: 4.0.8 normalize-path: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.8 - postcss-safe-parser: 7.0.1(postcss@8.5.8) + postcss: 8.5.14 + postcss-safe-parser: 7.0.1(postcss@8.5.14) postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - string-width: 8.2.0 + string-width: 8.2.1 supports-hyperlinks: 4.4.0 svg-tags: 1.0.0 table: 6.9.0 @@ -8007,7 +8281,7 @@ snapshots: - supports-color - typescript - stylis@4.3.6: {} + stylis@4.4.0: {} sucrase@3.35.1: dependencies: @@ -8016,21 +8290,15 @@ snapshots: lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 ts-interface-checker: 0.1.13 - superstruct@0.10.13: {} - supports-color@10.2.2: {} supports-color@7.2.0: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - supports-hyperlinks@4.4.0: dependencies: has-flag: 5.0.1 @@ -8050,17 +8318,19 @@ snapshots: css-what: 6.2.2 csso: 5.0.5 picocolors: 1.1.1 - sax: 1.5.0 + sax: 1.6.0 svgson@5.3.1: dependencies: deep-rename-keys: 0.2.1 xml-reader: 2.4.3 - swagger-ui-dist@5.32.0: + swagger-ui-dist@5.32.5: dependencies: '@scarf/scarf': 1.4.0 + symbol-tree@3.2.4: {} + sync-fetch@0.4.5: dependencies: buffer: 5.7.1 @@ -8074,13 +8344,13 @@ snapshots: table@6.9.0: dependencies: - ajv: 8.18.0 + ajv: 8.20.0 lodash.truncate: 4.4.2 slice-ansi: 4.0.0 string-width: 4.2.3 strip-ansi: 6.0.1 - tailwindcss@3.4.17: + tailwindcss@3.4.19: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -8096,33 +8366,17 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.8 - postcss-import: 15.1.0(postcss@8.5.8) - postcss-js: 4.1.0(postcss@8.5.8) - postcss-load-config: 4.0.2(postcss@8.5.8) - postcss-nested: 6.2.0(postcss@8.5.8) + postcss: 8.5.14 + postcss-import: 15.1.0(postcss@8.5.14) + postcss-js: 4.1.0(postcss@8.5.14) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.14) + postcss-nested: 6.2.0(postcss@8.5.14) postcss-selector-parser: 6.1.2 - resolve: 1.22.11 + resolve: 1.22.12 sucrase: 3.35.1 transitivePeerDependencies: - - ts-node - - tapable@2.3.0: {} - - terser-webpack-plugin@5.3.17(webpack@5.105.4): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - terser: 5.46.0 - webpack: 5.105.4(webpack-cli@6.0.1) - - terser@5.46.0: - dependencies: - '@jridgewell/source-map': 0.3.11 - acorn: 8.16.0 - commander: 2.20.3 - source-map-support: 0.5.21 + - tsx + - yaml thenify-all@1.6.0: dependencies: @@ -8138,14 +8392,14 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.0.2: {} + tinyexec@1.1.2: {} - tinyglobby@0.2.15: + tinyglobby@0.2.16: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 - tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} tippy.js@6.3.7: dependencies: @@ -8157,14 +8411,29 @@ snapshots: toastify-js@1.12.0: {} + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tr46@0.0.3: {} + tr46@3.0.0: + dependencies: + punycode: 2.3.1 + tributejs@5.1.3: {} - ts-api-utils@2.4.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + ts-dedent@2.2.0: {} ts-interface-checker@0.1.13: {} @@ -8176,38 +8445,54 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3): + type-detect@4.0.8: {} + + typescript-eslint@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@5.9.3) + eslint: 10.3.0(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color + typescript-eslint@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.3.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} + typescript@6.0.3: {} + typo-js@1.3.1: {} uc.micro@2.1.0: {} - ufo@1.6.3: {} + ufo@1.6.4: {} uint8-to-base64@0.2.1: {} - undici-types@7.18.2: {} + undici-types@7.19.2: {} unicorn-magic@0.4.0: {} + universalify@0.2.0: {} + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -8232,219 +8517,137 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - update-browserslist-db@1.2.3(browserslist@4.28.1): + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 - updates@17.8.3: {} + updates@17.16.9: {} uri-js@4.4.1: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + util-deprecate@1.0.2: {} - uuid@11.1.0: {} + uuid@11.1.1: {} vanilla-colorful@0.7.2: {} - vite-string-plugin@2.0.1(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): + vite-string-plugin@2.0.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)): dependencies: - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0) - vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): + vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0): dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.8 - rollup: 4.59.0 - tinyglobby: 0.2.15 + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.14 + rolldown: 1.0.0-rc.17 + tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.6.0 + esbuild: 0.28.0 fsevents: 2.3.3 - jiti: 2.6.1 - terser: 5.46.0 - yaml: 2.8.2 + jiti: 2.7.0 - vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): + vitest@4.1.5(@types/node@25.6.0)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)): dependencies: - '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) - '@vitest/pretty-format': 4.0.18 - '@vitest/runner': 4.0.18 - '@vitest/snapshot': 4.0.18 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - es-module-lexer: 1.7.0 + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.3.5 - happy-dom: 20.8.3 + '@types/node': 25.6.0 + happy-dom: 20.9.0 + jsdom: 20.0.3 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - - vscode-jsonrpc@8.2.0: {} - - vscode-languageserver-protocol@3.17.5: - dependencies: - vscode-jsonrpc: 8.2.0 - vscode-languageserver-types: 3.17.5 - - vscode-languageserver-textdocument@1.0.12: {} - - vscode-languageserver-types@3.17.5: {} - - vscode-languageserver@9.0.1: - dependencies: - vscode-languageserver-protocol: 3.17.5 vscode-uri@3.1.0: {} - vue-bar-graph@2.2.0(typescript@5.9.3): + vue-bar-graph@2.2.0(typescript@6.0.3): dependencies: - vue: 3.5.29(typescript@5.9.3) + vue: 3.5.34(typescript@6.0.3) transitivePeerDependencies: - typescript - vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.29(typescript@5.9.3)): + vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.34(typescript@6.0.3)): dependencies: chart.js: 4.5.1 - vue: 3.5.29(typescript@5.9.3) + vue: 3.5.34(typescript@6.0.3) - vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1)): + vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.7.0)): dependencies: debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) - eslint-scope: 9.1.1 + eslint: 10.3.0(jiti@2.7.0) + eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 - espree: 11.1.1 + espree: 11.2.0 esquery: 1.7.0 - semver: 7.7.4 + semver: 7.8.0 transitivePeerDependencies: - supports-color - vue-loader@17.4.2(vue@3.5.29(typescript@5.9.3))(webpack@5.105.4): - dependencies: - chalk: 4.1.2 - hash-sum: 2.0.0 - watchpack: 2.5.1 - webpack: 5.105.4(webpack-cli@6.0.1) - optionalDependencies: - vue: 3.5.29(typescript@5.9.3) - - vue-tsc@3.2.5(typescript@5.9.3): + vue-tsc@3.2.8(typescript@6.0.3): dependencies: '@volar/typescript': 2.4.28 - '@vue/language-core': 3.2.5 - typescript: 5.9.3 + '@vue/language-core': 3.2.8 + typescript: 6.0.3 - vue@3.5.29(typescript@5.9.3): + vue@3.5.34(typescript@6.0.3): dependencies: - '@vue/compiler-dom': 3.5.29 - '@vue/compiler-sfc': 3.5.29 - '@vue/runtime-dom': 3.5.29 - '@vue/server-renderer': 3.5.29(vue@3.5.29(typescript@5.9.3)) - '@vue/shared': 3.5.29 + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-sfc': 3.5.34 + '@vue/runtime-dom': 3.5.34 + '@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@6.0.3)) + '@vue/shared': 3.5.34 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 - watchpack@2.5.1: + w3c-keyname@2.2.8: {} + + w3c-xmlserializer@4.0.0: dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 + xml-name-validator: 4.0.0 webidl-conversions@3.0.1: {} - webpack-cli@6.0.1(webpack@5.105.4): - dependencies: - '@discoveryjs/json-ext': 0.6.3 - '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) - '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) - '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) - colorette: 2.0.20 - commander: 12.1.0 - cross-spawn: 7.0.6 - envinfo: 7.21.0 - fastest-levenshtein: 1.0.16 - import-local: 3.2.0 - interpret: 3.1.1 - rechoir: 0.8.0 - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-merge: 6.0.1 + webidl-conversions@7.0.0: {} - webpack-merge@6.0.1: + whatwg-encoding@2.0.0: dependencies: - clone-deep: 4.0.1 - flat: 5.0.2 - wildcard: 2.0.1 - - webpack-sources@1.4.3: - dependencies: - source-list-map: 2.0.1 - source-map: 0.6.1 - - webpack-sources@3.3.4: {} - - webpack@5.105.4(webpack-cli@6.0.1): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.16.0 - acorn-import-phases: 1.0.4(acorn@8.16.0) - browserslist: 4.28.1 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.20.0 - es-module-lexer: 2.0.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.17(webpack@5.105.4) - watchpack: 2.5.1 - webpack-sources: 3.3.4 - optionalDependencies: - webpack-cli: 6.0.1(webpack@5.105.4) - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js + iconv-lite: 0.6.3 whatwg-mimetype@3.0.0: {} + whatwg-url@11.0.0: + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -8463,29 +8666,13 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - wildcard@2.0.1: {} - word-wrap@1.2.5: {} - wrap-ansi@10.0.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 8.2.0 - strip-ansi: 7.2.0 - - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrappy@1.0.2: {} - write-file-atomic@7.0.1: dependencies: signal-exit: 4.1.0 - ws@8.19.0: {} + ws@8.20.0: {} xml-lexer@0.2.2: dependencies: @@ -8498,6 +8685,6 @@ snapshots: eventemitter3: 2.0.3 xml-lexer: 0.2.2 - yaml@2.8.2: {} + xmlchars@2.2.0: {} yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000..0c8f7c63fe --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,37 @@ +packages: [ . ] # workaround for https://github.com/SukkaW/nolyfill/issues/119 +savePrefix: '' +dedupePeerDependents: false +updateNotifier: false +minimumReleaseAge: 0 + +peerDependencyRules: + allowedVersions: + eslint-plugin-github>eslint: '>=9' + +overrides: + array-includes: npm:@nolyfill/array-includes@^1 + array.prototype.findlastindex: npm:@nolyfill/array.prototype.findlastindex@^1 + array.prototype.flat: npm:@nolyfill/array.prototype.flat@^1 + array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1 + es-aggregate-error: npm:@nolyfill/es-aggregate-error@^1 + hasown: npm:@nolyfill/hasown@^1 + is-core-module: npm:@nolyfill/is-core-module@^1 + object.assign: npm:@nolyfill/object.assign@^1 + object.fromentries: npm:@nolyfill/object.fromentries@^1 + object.groupby: npm:@nolyfill/object.groupby@^1 + object.values: npm:@nolyfill/object.values@^1 + safe-buffer: npm:@nolyfill/safe-buffer@^1 + safe-regex-test: npm:@nolyfill/safe-regex-test@^1 + safer-buffer: npm:@nolyfill/safer-buffer@^1 + string.prototype.includes: npm:@nolyfill/string.prototype.includes@^1 + string.prototype.trimend: npm:@nolyfill/string.prototype.trimend@^1 + object-keys: npm:@nolyfill/object-keys@^1 + object.entries: npm:@nolyfill/object.entries@^1 + abab: npm:@nolyfill/abab@^1 + es-set-tostringtag: npm:@nolyfill/es-set-tostringtag@^1 + +allowBuilds: + '@scarf/scarf': false + core-js: false + esbuild: true + unrs-resolver: true diff --git a/public/assets/img/svg/gitea-terraform.svg b/public/assets/img/svg/gitea-terraform.svg new file mode 100644 index 0000000000..809b7e6fe1 --- /dev/null +++ b/public/assets/img/svg/gitea-terraform.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-lockup-github.svg b/public/assets/img/svg/octicon-lockup-github.svg new file mode 100644 index 0000000000..746317496c --- /dev/null +++ b/public/assets/img/svg/octicon-lockup-github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-logo-github.svg b/public/assets/img/svg/octicon-logo-github.svg index 8aae451ae5..cd09f6ac14 100644 --- a/public/assets/img/svg/octicon-logo-github.svg +++ b/public/assets/img/svg/octicon-logo-github.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-mark-github.svg b/public/assets/img/svg/octicon-mark-github.svg index 6d6dc40886..a46d882513 100644 --- a/public/assets/img/svg/octicon-mark-github.svg +++ b/public/assets/img/svg/octicon-mark-github.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-sandbox.svg b/public/assets/img/svg/octicon-sandbox.svg new file mode 100644 index 0000000000..a4c8e1df28 --- /dev/null +++ b/public/assets/img/svg/octicon-sandbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-stack-check.svg b/public/assets/img/svg/octicon-stack-check.svg new file mode 100644 index 0000000000..5d35f04ad6 --- /dev/null +++ b/public/assets/img/svg/octicon-stack-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-stack-remove.svg b/public/assets/img/svg/octicon-stack-remove.svg new file mode 100644 index 0000000000..0cba43490f --- /dev/null +++ b/public/assets/img/svg/octicon-stack-remove.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 20a10d1915..7db3ec5d04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.10" dev = [ "djlint==1.36.4", "yamllint==1.38.0", + "zizmor==1.25.1", ] [tool.djlint] diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 0000000000..c5d712dc0a --- /dev/null +++ b/renovate.json5 @@ -0,0 +1,123 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended", "helpers:pinGitHubActionDigests", "customManagers:githubActionsVersions"], + "configMigration": true, + "enabledManagers": ["github-actions", "gomod", "npm", "pep621", "nix", "custom.regex", "dockerfile"], + "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/dlclark/regexp2"], + "allowedVersions": "^1", // v2 fails to build on linux/386: https://github.com/dlclark/regexp2/issues/102 + }, + { + "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 + }, + { + "matchPackageNames": ["postgres"], + "allowedVersions": "/^14($|[.-])/", // pin to oldest supported major + }, + { + "matchPackageNames": ["bitnamilegacy/mysql"], + "allowedVersions": "/^8\\.4($|[.-])/", // pin to oldest LTS + }, + { + "matchPackageNames": ["mcr.microsoft.com/mssql/server"], + "allowedVersions": "/^2019($|[.-])/", // pin to oldest in extended support + }, + { + "groupName": "go dependencies", + "matchManagers": ["gomod"], + "postUpdateOptions": ["gomodUpdateImportPaths"], + "postUpgradeTasks": { + "commands": ["make tidy"], + "fileFilters": ["go.mod", "go.sum", "assets/go-licenses.json"], + "executionMode": "branch", + }, + }, + { + "groupName": "tool dependencies", + "matchManagers": ["custom.regex"], + "matchFileNames": ["**/Makefile"], + }, + { + "matchManagers": ["gomod"], + "matchDepNames": ["go"], + "matchDepTypes": ["golang"], + "rangeStrategy": "bump", + "schedule": ["at any time"], + "minimumReleaseAge": "0", + }, + { + "groupName": "npm dependencies", + "matchManagers": ["npm"], + "postUpdateOptions": ["pnpmDedupe"], + "postUpgradeTasks": { + "commands": ["make svg nolyfill"], + "fileFilters": ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", "public/assets/img/svg/**"], + "executionMode": "branch", + }, + }, + { + "groupName": "python dependencies", + "matchManagers": ["pep621"], + }, + { + "groupName": "nix dependencies", + "matchManagers": ["nix"], + }, + { + "groupName": "docker dependencies", + "matchManagers": ["dockerfile"], + }, + ], +} diff --git a/routers/api/actions/artifact.pb.go b/routers/api/actions/artifact.pb.go index 590eda9fb9..130e20301f 100644 --- a/routers/api/actions/artifact.pb.go +++ b/routers/api/actions/artifact.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 -// protoc v4.25.2 +// protoc-gen-go v1.36.11 +// protoc v7.34.0 // source: artifact.proto package actions @@ -12,6 +12,7 @@ package actions import ( reflect "reflect" sync "sync" + unsafe "unsafe" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -27,24 +28,22 @@ const ( ) type CreateArtifactRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` - WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` - Version int32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + Version int32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` + MimeType *wrapperspb.StringValue `protobuf:"bytes,6,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateArtifactRequest) Reset() { *x = CreateArtifactRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateArtifactRequest) String() string { @@ -55,7 +54,7 @@ func (*CreateArtifactRequest) ProtoMessage() {} func (x *CreateArtifactRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -105,22 +104,26 @@ func (x *CreateArtifactRequest) GetVersion() int32 { return 0 } -type CreateArtifactResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *CreateArtifactRequest) GetMimeType() *wrapperspb.StringValue { + if x != nil { + return x.MimeType + } + return nil +} - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"` +type CreateArtifactResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateArtifactResponse) Reset() { *x = CreateArtifactResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateArtifactResponse) String() string { @@ -131,7 +134,7 @@ func (*CreateArtifactResponse) ProtoMessage() {} func (x *CreateArtifactResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -161,24 +164,21 @@ func (x *CreateArtifactResponse) GetSignedUploadUrl() string { } type FinalizeArtifactRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` Hash *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=hash,proto3" json:"hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FinalizeArtifactRequest) Reset() { *x = FinalizeArtifactRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FinalizeArtifactRequest) String() string { @@ -189,7 +189,7 @@ func (*FinalizeArtifactRequest) ProtoMessage() {} func (x *FinalizeArtifactRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -240,21 +240,18 @@ func (x *FinalizeArtifactRequest) GetHash() *wrapperspb.StringValue { } type FinalizeArtifactResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` unknownFields protoimpl.UnknownFields - - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` + sizeCache protoimpl.SizeCache } func (x *FinalizeArtifactResponse) Reset() { *x = FinalizeArtifactResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FinalizeArtifactResponse) String() string { @@ -265,7 +262,7 @@ func (*FinalizeArtifactResponse) ProtoMessage() {} func (x *FinalizeArtifactResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -295,23 +292,20 @@ func (x *FinalizeArtifactResponse) GetArtifactId() int64 { } type ListArtifactsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` NameFilter *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=name_filter,json=nameFilter,proto3" json:"name_filter,omitempty"` IdFilter *wrapperspb.Int64Value `protobuf:"bytes,4,opt,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListArtifactsRequest) Reset() { *x = ListArtifactsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListArtifactsRequest) String() string { @@ -322,7 +316,7 @@ func (*ListArtifactsRequest) ProtoMessage() {} func (x *ListArtifactsRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -366,20 +360,17 @@ func (x *ListArtifactsRequest) GetIdFilter() *wrapperspb.Int64Value { } type ListArtifactsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"` unknownFields protoimpl.UnknownFields - - Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListArtifactsResponse) Reset() { *x = ListArtifactsResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListArtifactsResponse) String() string { @@ -390,7 +381,7 @@ func (*ListArtifactsResponse) ProtoMessage() {} func (x *ListArtifactsResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -413,25 +404,22 @@ func (x *ListArtifactsResponse) GetArtifacts() []*ListArtifactsResponse_Monolith } type ListArtifactsResponse_MonolithArtifact struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` DatabaseId int64 `protobuf:"varint,3,opt,name=database_id,json=databaseId,proto3" json:"database_id,omitempty"` Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"` CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListArtifactsResponse_MonolithArtifact) Reset() { *x = ListArtifactsResponse_MonolithArtifact{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListArtifactsResponse_MonolithArtifact) String() string { @@ -442,7 +430,7 @@ func (*ListArtifactsResponse_MonolithArtifact) ProtoMessage() {} func (x *ListArtifactsResponse_MonolithArtifact) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -500,22 +488,19 @@ func (x *ListArtifactsResponse_MonolithArtifact) GetCreatedAt() *timestamppb.Tim } type GetSignedArtifactURLRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` - WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetSignedArtifactURLRequest) Reset() { *x = GetSignedArtifactURLRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetSignedArtifactURLRequest) String() string { @@ -526,7 +511,7 @@ func (*GetSignedArtifactURLRequest) ProtoMessage() {} func (x *GetSignedArtifactURLRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -563,20 +548,17 @@ func (x *GetSignedArtifactURLRequest) GetName() string { } type GetSignedArtifactURLResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"` unknownFields protoimpl.UnknownFields - - SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetSignedArtifactURLResponse) Reset() { *x = GetSignedArtifactURLResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetSignedArtifactURLResponse) String() string { @@ -587,7 +569,7 @@ func (*GetSignedArtifactURLResponse) ProtoMessage() {} func (x *GetSignedArtifactURLResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -610,22 +592,19 @@ func (x *GetSignedArtifactURLResponse) GetSignedUrl() string { } type DeleteArtifactRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` - WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DeleteArtifactRequest) Reset() { *x = DeleteArtifactRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteArtifactRequest) String() string { @@ -636,7 +615,7 @@ func (*DeleteArtifactRequest) ProtoMessage() {} func (x *DeleteArtifactRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -673,21 +652,18 @@ func (x *DeleteArtifactRequest) GetName() string { } type DeleteArtifactResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` unknownFields protoimpl.UnknownFields - - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteArtifactResponse) Reset() { *x = DeleteArtifactResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteArtifactResponse) String() string { @@ -698,7 +674,7 @@ func (*DeleteArtifactResponse) ProtoMessage() {} func (x *DeleteArtifactResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -729,173 +705,105 @@ func (x *DeleteArtifactResponse) GetArtifactId() int64 { var File_artifact_proto protoreflect.FileDescriptor -var file_artifact_proto_rawDesc = []byte{ - 0x0a, 0x0e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x12, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a, - 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x22, 0xf5, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, - 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, - 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, - 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, - 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, - 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, - 0x6f, 0x6b, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x75, 0x70, 0x6c, - 0x6f, 0x61, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, - 0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xe8, - 0x01, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, - 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, - 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, - 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, - 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x4b, 0x0a, 0x18, 0x46, 0x69, 0x6e, - 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, - 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, - 0x66, 0x61, 0x63, 0x74, 0x49, 0x64, 0x22, 0x84, 0x02, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41, - 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, - 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, - 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x69, 0x6c, - 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x69, 0x6c, - 0x74, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x7c, 0x0a, - 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, - 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, - 0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x26, - 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, - 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, - 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, - 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, - 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, - 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x64, - 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, - 0x73, 0x69, 0x7a, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, - 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, - 0xa6, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, - 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, - 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, - 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x53, - 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e, - 0x65, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69, - 0x67, 0x6e, 0x65, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xa0, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, - 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, - 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, - 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, - 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x49, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_artifact_proto_rawDesc = "" + + "\n" + + "\x0eartifact.proto\x12\x1dgithub.actions.results.api.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\xb0\x02\n" + + "\x15CreateArtifactRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x129\n" + + "\n" + + "expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12\x18\n" + + "\aversion\x18\x05 \x01(\x05R\aversion\x129\n" + + "\tmime_type\x18\x06 \x01(\v2\x1c.google.protobuf.StringValueR\bmimeType\"T\n" + + "\x16CreateArtifactResponse\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12*\n" + + "\x11signed_upload_url\x18\x02 \x01(\tR\x0fsignedUploadUrl\"\xe8\x01\n" + + "\x17FinalizeArtifactRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\x120\n" + + "\x04hash\x18\x05 \x01(\v2\x1c.google.protobuf.StringValueR\x04hash\"K\n" + + "\x18FinalizeArtifactResponse\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12\x1f\n" + + "\vartifact_id\x18\x02 \x01(\x03R\n" + + "artifactId\"\x84\x02\n" + + "\x14ListArtifactsRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12=\n" + + "\vname_filter\x18\x03 \x01(\v2\x1c.google.protobuf.StringValueR\n" + + "nameFilter\x128\n" + + "\tid_filter\x18\x04 \x01(\v2\x1b.google.protobuf.Int64ValueR\bidFilter\"|\n" + + "\x15ListArtifactsResponse\x12c\n" + + "\tartifacts\x18\x01 \x03(\v2E.github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifactR\tartifacts\"\xa1\x02\n" + + "&ListArtifactsResponse_MonolithArtifact\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x1f\n" + + "\vdatabase_id\x18\x03 \x01(\x03R\n" + + "databaseId\x12\x12\n" + + "\x04name\x18\x04 \x01(\tR\x04name\x12\x12\n" + + "\x04size\x18\x05 \x01(\x03R\x04size\x129\n" + + "\n" + + "created_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\xa6\x01\n" + + "\x1bGetSignedArtifactURLRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\"=\n" + + "\x1cGetSignedArtifactURLResponse\x12\x1d\n" + + "\n" + + "signed_url\x18\x01 \x01(\tR\tsignedUrl\"\xa0\x01\n" + + "\x15DeleteArtifactRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\"I\n" + + "\x16DeleteArtifactResponse\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12\x1f\n" + + "\vartifact_id\x18\x02 \x01(\x03R\n" + + "artifactIdB)Z'code.gitea.io/gitea/routers/api/actionsb\x06proto3" var ( file_artifact_proto_rawDescOnce sync.Once - file_artifact_proto_rawDescData = file_artifact_proto_rawDesc + file_artifact_proto_rawDescData []byte ) func file_artifact_proto_rawDescGZIP() []byte { file_artifact_proto_rawDescOnce.Do(func() { - file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(file_artifact_proto_rawDescData) + file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_artifact_proto_rawDesc), len(file_artifact_proto_rawDesc))) }) return file_artifact_proto_rawDescData } -var ( - file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11) - file_artifact_proto_goTypes = []interface{}{ - (*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest - (*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse - (*FinalizeArtifactRequest)(nil), // 2: github.actions.results.api.v1.FinalizeArtifactRequest - (*FinalizeArtifactResponse)(nil), // 3: github.actions.results.api.v1.FinalizeArtifactResponse - (*ListArtifactsRequest)(nil), // 4: github.actions.results.api.v1.ListArtifactsRequest - (*ListArtifactsResponse)(nil), // 5: github.actions.results.api.v1.ListArtifactsResponse - (*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact - (*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest - (*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse - (*DeleteArtifactRequest)(nil), // 9: github.actions.results.api.v1.DeleteArtifactRequest - (*DeleteArtifactResponse)(nil), // 10: github.actions.results.api.v1.DeleteArtifactResponse - (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp - (*wrapperspb.StringValue)(nil), // 12: google.protobuf.StringValue - (*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value - } -) - +var file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_artifact_proto_goTypes = []any{ + (*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest + (*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse + (*FinalizeArtifactRequest)(nil), // 2: github.actions.results.api.v1.FinalizeArtifactRequest + (*FinalizeArtifactResponse)(nil), // 3: github.actions.results.api.v1.FinalizeArtifactResponse + (*ListArtifactsRequest)(nil), // 4: github.actions.results.api.v1.ListArtifactsRequest + (*ListArtifactsResponse)(nil), // 5: github.actions.results.api.v1.ListArtifactsResponse + (*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact + (*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest + (*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse + (*DeleteArtifactRequest)(nil), // 9: github.actions.results.api.v1.DeleteArtifactRequest + (*DeleteArtifactResponse)(nil), // 10: github.actions.results.api.v1.DeleteArtifactResponse + (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp + (*wrapperspb.StringValue)(nil), // 12: google.protobuf.StringValue + (*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value +} var file_artifact_proto_depIdxs = []int32{ 11, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp - 12, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue - 12, // 2: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue - 13, // 3: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value - 6, // 4: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact - 11, // 5: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 12, // 1: github.actions.results.api.v1.CreateArtifactRequest.mime_type:type_name -> google.protobuf.StringValue + 12, // 2: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue + 12, // 3: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue + 13, // 4: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value + 6, // 5: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact + 11, // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_artifact_proto_init() } @@ -903,145 +811,11 @@ func file_artifact_proto_init() { if File_artifact_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_artifact_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateArtifactRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateArtifactResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FinalizeArtifactRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FinalizeArtifactResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListArtifactsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListArtifactsResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListArtifactsResponse_MonolithArtifact); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetSignedArtifactURLRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetSignedArtifactURLResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteArtifactRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteArtifactResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_artifact_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_artifact_proto_rawDesc), len(file_artifact_proto_rawDesc)), NumEnums: 0, NumMessages: 11, NumExtensions: 0, @@ -1052,7 +826,6 @@ func file_artifact_proto_init() { MessageInfos: file_artifact_proto_msgTypes, }.Build() File_artifact_proto = out.File - file_artifact_proto_rawDesc = nil file_artifact_proto_goTypes = nil file_artifact_proto_depIdxs = nil } diff --git a/routers/api/actions/artifact.proto b/routers/api/actions/artifact.proto index c68e5d030d..7da8bad564 100644 --- a/routers/api/actions/artifact.proto +++ b/routers/api/actions/artifact.proto @@ -5,12 +5,15 @@ import "google/protobuf/wrappers.proto"; package github.actions.results.api.v1; +option go_package = "code.gitea.io/gitea/routers/api/actions"; + message CreateArtifactRequest { string workflow_run_backend_id = 1; string workflow_job_run_backend_id = 2; string name = 3; google.protobuf.Timestamp expires_at = 4; int32 version = 5; + google.protobuf.StringValue mime_type = 6; } message CreateArtifactResponse { diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 76facd769f..838ddb7f91 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" @@ -282,7 +283,7 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) { artifact.FileCompressedSize != chunksTotalSize { artifact.FileSize = fileRealTotalSize artifact.FileCompressedSize = chunksTotalSize - artifact.ContentEncoding = ctx.Req.Header.Get("Content-Encoding") + artifact.ContentEncodingOrType = ctx.Req.Header.Get("Content-Encoding") if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error update artifact: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error update artifact") @@ -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") @@ -492,11 +500,11 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) { defer fd.Close() // if artifact is compressed, set content-encoding header to gzip - if artifact.ContentEncoding == "gzip" { + if artifact.ContentEncodingOrType == actions.ContentEncodingV3Gzip { ctx.Resp.Header().Set("Content-Encoding", "gzip") } log.Debug("[artifact] downloadArtifact, name: %s, path: %s, storage: %s, size: %d", artifact.ArtifactName, artifact.ArtifactPath, artifact.StoragePath, artifact.FileSize) - ctx.ServeContent(fd, &context.ServeHeaderOptions{ + ctx.ServeContent(fd, context.ServeHeaderOptions{ Filename: artifact.ArtifactName, LastModified: artifact.CreatedUnix.AsLocalTime(), }) diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index 86a51d6ca6..6f84f7a5cf 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 { @@ -285,6 +287,17 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int return nil } +func generateArtifactStoragePath(artifact *actions.ActionArtifact) string { + // if chunk is gzip, use gz as extension + // download-artifact action will use content-encoding header to decide if it should decompress the file + extension := "chunk" + if artifact.ContentEncodingOrType == actions.ContentEncodingV3Gzip { + extension = "chunk.gz" + } + + return fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension) +} + func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact, checksum string) error { sort.Slice(chunks, func(i, j int) bool { return chunks[i].Start < chunks[j].Start @@ -335,15 +348,8 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st mergedReader = io.TeeReader(mergedReader, hashSha256) } - // if chunk is gzip, use gz as extension - // download-artifact action will use content-encoding header to decide if it should decompress the file - extension := "chunk" - if artifact.ContentEncoding == "gzip" { - extension = "chunk.gz" - } - // save merged file - storagePath := fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension) + storagePath := generateArtifactStoragePath(artifact) written, err := st.Save(storagePath, mergedReader, artifact.FileCompressedSize) if err != nil { return fmt.Errorf("save merged file error: %v", err) diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 62605f2702..f1f33424ed 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -89,10 +89,12 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/base64" + "encoding/hex" "encoding/xml" "errors" "fmt" "io" + "mime" "net/http" "net/url" "path" @@ -100,25 +102,25 @@ import ( "strings" "time" - "code.gitea.io/gitea/models/actions" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "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" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/known/timestamppb" + "xorm.io/builder" ) -const ( - ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService" - ArtifactV4ContentEncoding = "application/zip" -) +const ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService" type artifactV4Routes struct { prefix string @@ -219,7 +221,7 @@ func parseChunkFileItemV4(st storage.ObjectStorage, artifactID int64, fpath stri return &item, nil } -func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) { +func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions_model.ActionTask, string, bool) { rawTaskID := ctx.Req.URL.Query().Get("taskID") rawArtifactID := ctx.Req.URL.Query().Get("artifactID") sig := ctx.Req.URL.Query().Get("sig") @@ -246,13 +248,13 @@ func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (* ctx.HTTPError(http.StatusUnauthorized, "Error link expired") return nil, "", false } - task, err := actions.GetTaskByID(ctx, taskID) + task, err := actions_model.GetTaskByID(ctx, taskID) if err != nil { log.Error("Error runner api getting task by ID: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task by ID") return nil, "", false } - if task.Status != actions.StatusRunning { + if task.Status != actions_model.StatusRunning { log.Error("Error runner api getting task: task is not running") ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task: task is not running") return nil, "", false @@ -265,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.ActionArtifact, error) { - var art actions.ActionArtifact - has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ? AND content_encoding = ?", runID, name, name+".zip", ArtifactV4ContentEncoding).Get(&art) +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, "run_attempt_id": runAttemptID, "artifact_name": name}, builder.Like{"content_encoding", "%/%"}).Get(&art) if err != nil { return nil, err } else if !has { @@ -321,26 +323,59 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { if req.ExpiresAt != nil { retentionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24) } + encoding := req.GetMimeType().GetValue() + // Validate media type + if encoding != "" { + encoding, _, _ = mime.ParseMediaType(encoding) + } + fileName := artifactName + if !strings.Contains(encoding, "/") || strings.EqualFold(encoding, actions_model.ContentTypeZip) && !strings.HasSuffix(fileName, ".zip") { + encoding = actions_model.ContentTypeZip + fileName = artifactName + ".zip" + } // create or get artifact with name and path - artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", retentionDays) + artifact, err := actions_model.CreateArtifact(ctx, ctx.ActionTask, artifactName, fileName, retentionDays) if err != nil { log.Error("Error create or get artifact: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error create or get artifact") return } - artifact.ContentEncoding = ArtifactV4ContentEncoding + artifact.ContentEncodingOrType = encoding artifact.FileSize = 0 artifact.FileCompressedSize = 0 - if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + + var respData CreateArtifactResponse + + if setting.Actions.ArtifactStorage.ServeDirect() && setting.Actions.ArtifactStorage.Type == setting.AzureBlobStorageType { + storagePath := generateArtifactStoragePath(artifact) + if artifact.StoragePath != "" { + _ = storage.ActionsArtifacts.Delete(artifact.StoragePath) + } + artifact.StoragePath = storagePath + artifact.Status = actions_model.ArtifactStatusUploadPending + u, err := storage.ActionsArtifacts.ServeDirectURL(artifact.StoragePath, artifact.ArtifactPath, http.MethodPut, nil) + if err != nil { + log.Error("Error ServeDirectURL: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error ServeDirectURL") + return + } + respData = CreateArtifactResponse{ + Ok: true, + SignedUploadUrl: u.String(), + } + } else { + respData = CreateArtifactResponse{ + Ok: true, + SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID), + } + } + + if err := actions_model.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID") return } - respData := CreateArtifactResponse{ - Ok: true, - SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID), - } r.sendProtobufBody(ctx, &respData) } @@ -354,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") @@ -370,7 +405,7 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { } artifact.FileCompressedSize += uploadedLength artifact.FileSize += uploadedLength - if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + if err := actions_model.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID") return @@ -441,16 +476,34 @@ 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") return } - var chunks []*chunkFileItem + if setting.Actions.ArtifactStorage.ServeDirect() && setting.Actions.ArtifactStorage.Type == setting.AzureBlobStorageType { + r.finalizeAzureServeDirect(ctx, &req, artifact) + } else { + r.finalizeDefaultArtifact(ctx, &req, artifact, runID) + } + + // Return on finalize error + if ctx.Written() { + return + } + + respData := FinalizeArtifactResponse{ + Ok: true, + ArtifactId: artifact.ID, + } + r.sendProtobufBody(ctx, &respData) +} + +func (r *artifactV4Routes) finalizeDefaultArtifact(ctx *ArtifactContext, req *FinalizeArtifactRequest, artifact *actions_model.ActionArtifact, runID int64) { blockList, blockListErr := r.readBlockList(runID, artifact.ID) - chunks, err = listOrderedChunksForArtifact(r.fs, runID, artifact.ID, blockList) + chunks, err := listOrderedChunksForArtifact(r.fs, runID, artifact.ID, blockList) if err != nil { log.Error("Error list chunks: %v", errors.Join(blockListErr, err)) ctx.HTTPError(http.StatusInternalServerError, "Error list chunks") @@ -465,21 +518,63 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { return } - checksum := "" - if req.Hash != nil { - checksum = req.Hash.Value - } - if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil { + if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, req.GetHash().GetValue()); err != nil { log.Error("Error merge chunks: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks") return } +} - respData := FinalizeArtifactResponse{ - Ok: true, - ArtifactId: artifact.ID, +func (r *artifactV4Routes) finalizeAzureServeDirect(ctx *ArtifactContext, req *FinalizeArtifactRequest, artifact *actions_model.ActionArtifact) { + checksumValue, hasSha256Checksum := strings.CutPrefix(req.GetHash().GetValue(), "sha256:") + var actualLength int64 + if hasSha256Checksum { + hashSha256 := sha256.New() + obj, err := storage.ActionsArtifacts.Open(artifact.StoragePath) + if err != nil { + log.Error("Error read block: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error read block") + return + } + defer obj.Close() + actualLength, err = io.Copy(hashSha256, obj) + if err != nil { + log.Error("Error read block: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error read block") + return + } + rawChecksum := hashSha256.Sum(nil) + actualChecksum := hex.EncodeToString(rawChecksum) + if checksumValue != actualChecksum { + log.Error("Error merge chunks: checksum mismatch") + ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks: checksum mismatch") + return + } + } else { + fi, err := storage.ActionsArtifacts.Stat(artifact.StoragePath) + if err != nil { + log.Error("Error stat block: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error stat block") + return + } + actualLength = fi.Size() + } + + if req.Size != actualLength { + log.Error("Error merge chunks: length mismatch") + ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks: length mismatch") + return + } + + // Update artifact metadata and status now that the upload is confirmed. + artifact.FileSize = actualLength + artifact.FileCompressedSize = actualLength + artifact.Status = actions_model.ArtifactStatusUploadConfirmed + if err := actions_model.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + log.Error("Error UpdateArtifactByID: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID") + return } - r.sendProtobufBody(ctx, &respData) } func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { @@ -493,9 +588,11 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { return } - artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ - RunID: runID, - Status: int(actions.ArtifactStatusUploadConfirmed), + 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, }) if err != nil { log.Error("Error getting artifacts: %v", err) @@ -507,7 +604,7 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { table := map[string]*ListArtifactsResponse_MonolithArtifact{} for _, artifact := range artifacts { - if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != ArtifactV4ContentEncoding { + if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value { table[artifact.ArtifactName] = nil continue } @@ -547,13 +644,13 @@ 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") return } - if artifact.Status != actions.ArtifactStatusUploadConfirmed { + if artifact.Status != actions_model.ArtifactStatusUploadConfirmed { log.Error("Error artifact not found: %s", artifact.Status.ToString()) ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return @@ -563,9 +660,9 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { if setting.Actions.ArtifactStorage.ServeDirect() { // DO NOT USE the http POST method coming from the getSignedArtifactURL endpoint - u, err := storage.ActionsArtifacts.ServeDirectURL(artifact.StoragePath, artifact.ArtifactPath, http.MethodGet, nil) - if u != nil && err == nil { - respData.SignedUrl = u.String() + u, err := actions.GetArtifactV4ServeDirectURL(artifact, http.MethodGet) + if err == nil { + respData.SignedUrl = u } } if respData.SignedUrl == "" { @@ -581,21 +678,23 @@ 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") return } - if artifact.Status != actions.ArtifactStatusUploadConfirmed { + if artifact.Status != actions_model.ArtifactStatusUploadConfirmed { log.Error("Error artifact not found: %s", artifact.Status.ToString()) ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } - file, _ := r.fs.Open(artifact.StoragePath) - - _, _ = io.Copy(ctx.Resp, file) + err = actions.DownloadArtifactV4ReadStorage(ctx.Base, artifact) + if err != nil { + log.Error("Error serve artifact: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "failed to download artifact") + } } func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { @@ -610,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.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 190dc69744..0c9c2a5f4a 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" @@ -80,9 +79,7 @@ func (s *Service) Register( AgentLabels: labels, Ephemeral: req.Msg.Ephemeral, } - if err := runner.GenerateToken(); err != nil { - return nil, errors.New("can't generate token") - } + runner.GenerateAndFillToken() // create new runner if err := actions_model.CreateRunner(ctx, runner); err != nil { @@ -226,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 { @@ -234,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) } } @@ -264,7 +261,16 @@ func (s *Service) UpdateLog( } ack := task.LogLength - if len(req.Msg.Rows) == 0 || req.Msg.Index > ack || int64(len(req.Msg.Rows))+req.Msg.Index <= ack { + // Trim rows the runner already had acked. + var rows []*runnerv1.LogRow + if req.Msg.Index <= ack && int64(len(req.Msg.Rows))+req.Msg.Index > ack { + rows = req.Msg.Rows[ack-req.Msg.Index:] + } + + // Bail unless we have new rows or a NoMore to finalize. Even with + // NoMore, bail when the runner has outrun the server — archiving a + // log with a gap is worse than asking it to retry. + if len(rows) == 0 && (!req.Msg.NoMore || req.Msg.Index > ack) { res.Msg.AckIndex = ack return res, nil } @@ -273,7 +279,9 @@ func (s *Service) UpdateLog( return nil, status.Errorf(codes.AlreadyExists, "log file has been archived") } - rows := req.Msg.Rows[ack-req.Msg.Index:] + // WriteLogs is called even with no rows: with offset==0 it bootstraps + // an empty DBFS file so TransferLogs below has something to read when + // the runner finalizes a task that produced no log output. ns, err := actions.WriteLogs(ctx, task.LogFilename, task.LogSize, rows) if err != nil { return nil, status.Errorf(codes.Internal, "unable to append logs to dbfs file: %v", err) diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index f250a1a549..52fc287a0e 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -54,7 +54,7 @@ func GetRepositoryKey(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{ ContentType: "application/x-pem-file", Filename: fmt.Sprintf("%s@%s.rsa.pub", ctx.Package.Owner.LowerName, hex.EncodeToString(fingerprint)), }) diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 44bb80018b..9283eb8a5e 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -32,6 +32,7 @@ import ( "code.gitea.io/gitea/routers/api/packages/rpm" "code.gitea.io/gitea/routers/api/packages/rubygems" "code.gitea.io/gitea/routers/api/packages/swift" + "code.gitea.io/gitea/routers/api/packages/terraform" "code.gitea.io/gitea/routers/api/packages/vagrant" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/context" @@ -472,9 +473,10 @@ func CommonRoutes() *web.Router { g.MatchPath("HEAD", "//repodata/", rpm.CheckRepositoryFileExistence) g.MatchPath("GET", "//repodata/", rpm.GetRepositoryFile) g.MatchPath("PUT", "//upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile) + g.MatchPath("POST", "//package///errata", reqPackageAccess(perm.AccessModeWrite), rpm.UploadErrata) // this URL pattern is only used internally in the RPM index, it is generated by us, the filename part is not really used (can be anything) - g.MatchPath("HEAD,GET", "//package///", rpm.DownloadPackageFile) g.MatchPath("HEAD,GET", "//package////", rpm.DownloadPackageFile) + g.MatchPath("HEAD,GET", "//package///", rpm.DownloadPackageFile) g.MatchPath("DELETE", "//package///", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile) }, reqPackageAccess(perm.AccessModeRead)) @@ -514,6 +516,21 @@ func CommonRoutes() *web.Router { r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers) }, reqPackageAccess(perm.AccessModeRead)) }) + // See https://docs.gitlab.com/ci/jobs/fine_grained_permissions/#terraform-state-endpoints + // For endpoint and permission reference + r.Group("/terraform/state/{name}", func() { + r.Get("", terraform.GetTerraformState) + r.Get("/versions/{serial}", terraform.GetTerraformStateBySerial) + r.Group("", func() { + r.Post("", terraform.UploadState) + r.Delete("", terraform.DeleteState) + r.Delete("/versions/{serial}", terraform.DeleteStateBySerial) + }, reqPackageAccess(perm.AccessModeWrite)) + r.Group("/lock", func() { + r.Post("", terraform.LockState) + r.Delete("", terraform.UnlockState) + }, reqPackageAccess(perm.AccessModeWrite)) + }, reqPackageAccess(perm.AccessModeRead)) r.Group("/vagrant", func() { r.Group("/authenticate", func() { r.Get("", vagrant.CheckAuthenticate) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index 5a124f6918..f3b70f39b6 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -35,7 +35,7 @@ func GetRepositoryKey(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{ ContentType: "application/pgp-keys", }) } @@ -232,7 +232,7 @@ func GetPackageOrRepositoryFile(ctx *context.Context) { return } - ctx.ServeContent(bytes.NewReader(data), &context.ServeHeaderOptions{ + ctx.ServeContent(bytes.NewReader(data), context.ServeHeaderOptions{ Filename: filenameOrig, }) return diff --git a/routers/api/packages/composer/api.go b/routers/api/packages/composer/api.go index a3ea2c2f9a..0d95ab3ed9 100644 --- a/routers/api/packages/composer/api.go +++ b/routers/api/packages/composer/api.go @@ -9,7 +9,10 @@ import ( "time" packages_model "code.gitea.io/gitea/models/packages" + access_model "code.gitea.io/gitea/models/perm/access" + "code.gitea.io/gitea/modules/log" composer_module "code.gitea.io/gitea/modules/packages/composer" + "code.gitea.io/gitea/services/context" ) // ServiceIndexResponse contains registry endpoints @@ -91,7 +94,7 @@ type Source struct { Reference string `json:"reference"` } -func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse { +func createPackageMetadataResponse(ctx *context.Context, registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse { versions := make([]*PackageVersionMetadata, 0, len(pds)) for _, pd := range pds { @@ -116,10 +119,15 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac }, } if pd.Repository != nil { - pkg.Source = Source{ - URL: pd.Repository.HTMLURL(), - Type: "git", - Reference: pd.Version.Version, + permission, err := access_model.GetDoerRepoPermission(ctx, pd.Repository, ctx.Doer) + if err != nil { + log.Error("GetDoerRepoPermission[%d]: %v", pd.Repository.ID, err) + } else if permission.HasAnyUnitAccessOrPublicAccess() { + pkg.Source = Source{ + URL: pd.Repository.HTMLURL(), + Type: "git", + Reference: pd.Version.Version, + } } } diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go index 8eb66ca244..b18cdc242c 100644 --- a/routers/api/packages/composer/composer.go +++ b/routers/api/packages/composer/composer.go @@ -146,6 +146,7 @@ func PackageMetadata(ctx *context.Context) { } resp := createPackageMetadataResponse( + ctx, setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/composer", pds, ) diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 0726302d41..3fdd62298e 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -27,6 +27,7 @@ import ( container_module "code.gitea.io/gitea/modules/packages/container" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" auth_service "code.gitea.io/gitea/services/auth" @@ -125,8 +126,15 @@ func APIUnauthorizedError(ctx *context.Context) { // container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed realmURL := httplib.GuessCurrentHostURL(ctx) + "/v2/token" ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`) - // support apple container like: container registry login -u - ctx.Resp.Header().Add("WWW-Authenticate", `Basic realm="Gitea Container Registry"`) + + ownerName := ctx.PathParam("username") + owner, _ := user_model.GetUserByName(ctx, ownerName) + requireSignIn := owner != nil && owner.Visibility != structs.VisibleTypePublic + requireSignIn = requireSignIn || setting.Service.RequireSignInViewStrict + if requireSignIn { + // support apple container like: container registry login -u + ctx.Resp.Header().Add("WWW-Authenticate", `Basic realm="Gitea Container Registry"`) + } apiErrorDefined(ctx, errUnauthorized) } diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go index 82c7952bdb..785efb6dda 100644 --- a/routers/api/packages/debian/debian.go +++ b/routers/api/packages/debian/debian.go @@ -35,7 +35,7 @@ func GetRepositoryKey(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{ ContentType: "application/pgp-keys", Filename: "repository.key", }) @@ -233,7 +233,7 @@ func DownloadPackageFile(ctx *context.Context) { return } - helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{ + helper.ServePackageFile(ctx, s, u, pf, context.ServeHeaderOptions{ ContentType: "application/vnd.debian.binary-package", Filename: pf.Name, LastModified: pf.CreatedUnix.AsLocalTime(), diff --git a/routers/api/packages/helper/helper.go b/routers/api/packages/helper/helper.go index 27d4e6ffdc..01ae5d2b7e 100644 --- a/routers/api/packages/helper/helper.go +++ b/routers/api/packages/helper/helper.go @@ -39,7 +39,7 @@ func ProcessErrorForUser(ctx *context.Context, status int, errObj any) string { // ServePackageFile the content of the package file // If the url is set it will redirect the request, otherwise the content is copied to the response. -func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...*context.ServeHeaderOptions) { +func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...context.ServeHeaderOptions) { if u != nil { ctx.Redirect(u.String()) return @@ -47,11 +47,11 @@ func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf defer s.Close() - var opts *context.ServeHeaderOptions + var opts context.ServeHeaderOptions if len(forceOpts) > 0 { opts = forceOpts[0] } else { - opts = &context.ServeHeaderOptions{ + opts = context.ServeHeaderOptions{ Filename: pf.Name, LastModified: pf.CreatedUnix.AsLocalTime(), } diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index 6c2916908b..446398caf7 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -200,7 +200,7 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool return } - opts := &context.ServeHeaderOptions{ + opts := context.ServeHeaderOptions{ ContentLength: &pb.Size, LastModified: pf.CreatedUnix.AsLocalTime(), } diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index cc2aff8ea0..b65edf2334 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -169,7 +169,7 @@ func UploadPackage(ctx *context.Context) { canWrite := repo.OwnerID == ctx.Doer.ID if !canWrite { - perms, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + perms, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go index 5abbb0c8ae..d4fdb0affa 100644 --- a/routers/api/packages/rpm/rpm.go +++ b/routers/api/packages/rpm/rpm.go @@ -9,7 +9,9 @@ import ( "fmt" "io" "net/http" + "net/url" "strings" + "time" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" @@ -57,7 +59,7 @@ func GetRepositoryKey(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{ ContentType: "application/pgp-keys", Filename: "repository.key", }) @@ -80,7 +82,7 @@ func CheckRepositoryFileExistence(ctx *context.Context) { return } - ctx.SetServeHeaders(&context.ServeHeaderOptions{ + ctx.SetServeHeaders(context.ServeHeaderOptions{ Filename: pf.Name, LastModified: pf.CreatedUnix.AsLocalTime(), }) @@ -219,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) } @@ -316,3 +326,146 @@ func DeletePackageFile(webctx *context.Context) { webctx.Status(http.StatusNoContent) } + +// UploadErrata handles uploading errata information for a package version +func UploadErrata(ctx *context.Context) { + name := ctx.PathParam("name") + version := ctx.PathParam("version") + group := ctx.PathParam("group") + + var updates []*rpm_module.Update + if err := json.NewDecoder(ctx.Req.Body).Decode(&updates); err != nil { + apiError(ctx, http.StatusBadRequest, err) + return + } + + pv, err := packages_model.GetVersionByNameAndVersion(ctx, + ctx.Package.Owner.ID, + packages_model.TypeRpm, + name, + version, + ) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + var vm *rpm_module.VersionMetadata + if pv.MetadataJSON != "" { + if err := json.Unmarshal([]byte(pv.MetadataJSON), &vm); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } else { + vm = &rpm_module.VersionMetadata{} + } + + now := time.Now().Format("2006-01-02 15:04:05") + for _, u := range updates { + if u == nil { + continue + } + + // Sanitize to remove nil elements from JSON payload + var cleanPkgList []*rpm_module.Collection + for _, coll := range u.PkgList { + if coll == nil { + continue + } + var cleanPackages []*rpm_module.UpdatePackage + for _, pkg := range coll.Packages { + if pkg == nil { + continue + } + cleanPackages = append(cleanPackages, pkg) + } + coll.Packages = cleanPackages + cleanPkgList = append(cleanPkgList, coll) + } + u.PkgList = cleanPkgList + + found := false + for i, existing := range vm.Updates { + if existing.ID == u.ID { + // Merge PkgList with deduplication + for _, newColl := range u.PkgList { + if newColl == nil { + continue + } + collFound := false + for j, existingColl := range existing.PkgList { + if existingColl.Short == newColl.Short { + // Merge packages + for _, newPkg := range newColl.Packages { + if newPkg == nil { + continue + } + pkgFound := false + for _, existingPkg := range existingColl.Packages { + if existingPkg.Name == newPkg.Name && + existingPkg.Version == newPkg.Version && + existingPkg.Release == newPkg.Release && + existingPkg.Arch == newPkg.Arch { + pkgFound = true + break + } + } + if !pkgFound { + vm.Updates[i].PkgList[j].Packages = append(vm.Updates[i].PkgList[j].Packages, newPkg) + } + } + collFound = true + break + } + } + if !collFound { + vm.Updates[i].PkgList = append(vm.Updates[i].PkgList, newColl) + } + } + vm.Updates[i].From = u.From + vm.Updates[i].Status = u.Status + vm.Updates[i].Type = u.Type + vm.Updates[i].Version = u.Version + vm.Updates[i].Title = u.Title + vm.Updates[i].Severity = u.Severity + vm.Updates[i].Description = u.Description + vm.Updates[i].References = u.References + vm.Updates[i].Updated = &rpm_module.DateAttr{Date: now} + found = true + break + } + } + if !found { + if u.Issued == nil { + u.Issued = &rpm_module.DateAttr{Date: now} + } + if u.Updated == nil { + u.Updated = &rpm_module.DateAttr{Date: now} + } + vm.Updates = append(vm.Updates, u) + } + } + + vmBytes, err := json.Marshal(vm) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pv.MetadataJSON = string(vmBytes) + if err := packages_model.UpdateVersion(ctx, pv); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + if err := rpm_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, group); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusOK) +} diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index 69764c1df3..fe2e7af6d9 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -79,7 +79,7 @@ func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_mo }) } - ctx.SetServeHeaders(&context.ServeHeaderOptions{ + ctx.SetServeHeaders(context.ServeHeaderOptions{ Filename: filename + ".gz", }) @@ -119,7 +119,7 @@ func ServePackageSpecification(ctx *context.Context) { return } - ctx.SetServeHeaders(&context.ServeHeaderOptions{ + ctx.SetServeHeaders(context.ServeHeaderOptions{ Filename: filename, }) diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go index 66c28c9772..6d70f360f4 100644 --- a/routers/api/packages/swift/swift.go +++ b/routers/api/packages/swift/swift.go @@ -198,6 +198,23 @@ func PackageVersionMetadata(ctx *context.Context) { } metadata := pd.Metadata.(*swift_module.Metadata) + repositoryURLs := make([]string, 0, len(pd.VersionProperties)) + for _, property := range pd.VersionProperties { + if property.Name == swift_module.PropertyRepositoryURL { + repositoryURLs = append(repositoryURLs, property.Value) + } + } + + var author *swift_module.Person + if metadata.Author.Name != "" || metadata.Author.GivenName != "" || metadata.Author.MiddleName != "" || metadata.Author.FamilyName != "" { + author = &swift_module.Person{ + Type: "Person", + Name: metadata.Author.Name, + GivenName: metadata.Author.GivenName, + MiddleName: metadata.Author.MiddleName, + FamilyName: metadata.Author.FamilyName, + } + } setResponseHeaders(ctx.Resp, &headers{}) @@ -220,18 +237,14 @@ func PackageVersionMetadata(ctx *context.Context) { Keywords: metadata.Keywords, CodeRepository: metadata.RepositoryURL, License: metadata.License, + LicenseURL: metadata.LicenseURL, + Author: author, ProgrammingLanguage: swift_module.ProgrammingLanguage{ Type: "ComputerLanguage", Name: "Swift", URL: "https://swift.org", }, - Author: swift_module.Person{ - Type: "Person", - Name: metadata.Author.String(), - GivenName: metadata.Author.GivenName, - MiddleName: metadata.Author.MiddleName, - FamilyName: metadata.Author.FamilyName, - }, + RepositoryURLs: repositoryURLs, }, }) } @@ -281,7 +294,7 @@ func DownloadManifest(ctx *context.Context) { filename = fmt.Sprintf("Package@swift-%s.swift", swiftVersion) } - ctx.ServeContent(strings.NewReader(m.Content), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(m.Content), context.ServeHeaderOptions{ ContentType: "text/x-swift", Filename: filename, LastModified: pv.CreatedUnix.AsLocalTime(), @@ -437,7 +450,7 @@ func DownloadPackageFile(ctx *context.Context) { Digest: pd.Files[0].Blob.HashSHA256, }) - helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{ + helper.ServePackageFile(ctx, s, u, pf, context.ServeHeaderOptions{ Filename: pf.Name, ContentType: "application/zip", LastModified: pf.CreatedUnix.AsLocalTime(), diff --git a/routers/api/packages/terraform/terraform.go b/routers/api/packages/terraform/terraform.go new file mode 100644 index 0000000000..8b731b7dd2 --- /dev/null +++ b/routers/api/packages/terraform/terraform.go @@ -0,0 +1,438 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package terraform + +import ( + "errors" + "fmt" + "io" + "net/http" + "regexp" + "strconv" + "strings" + "unicode" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/globallock" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" + packages_module "code.gitea.io/gitea/modules/packages" + terraform_module "code.gitea.io/gitea/modules/packages/terraform" + "code.gitea.io/gitea/routers/api/packages/helper" + "code.gitea.io/gitea/services/context" + packages_service "code.gitea.io/gitea/services/packages" +) + +var packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`) + +const ( + stateFilename = "tfstate" +) + +func apiError(ctx *context.Context, status int, obj any) { + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.PlainText(status, message) +} + +// GetTerraformState serves the latest version of the state +func GetTerraformState(ctx *context.Context) { + stateName := ctx.PathParam("name") + pv, err := getLatestVersion(ctx, stateName) + if errors.Is(err, packages_model.ErrPackageNotExist) { + apiError(ctx, http.StatusNotFound, nil) + return + } else if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + streamState(ctx, stateName, pv.Version) +} + +// GetTerraformStateBySerial serves a specific version of terraform state. +func GetTerraformStateBySerial(ctx *context.Context) { + streamState(ctx, ctx.PathParam("name"), ctx.PathParam("serial")) +} + +// streamState serves the terraform state file +func streamState(ctx *context.Context, name, serial string) { + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( + ctx, + &packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeTerraformState, + Name: name, + Version: serial, + }, + &packages_service.PackageFileInfo{ + Filename: stateFilename, + }, + ctx.Req.Method, + ) + if err != nil { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { + apiError(ctx, http.StatusNotFound, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + + helper.ServePackageFile(ctx, s, u, pf) +} + +func isValidPackageName(packageName string) bool { + if len(packageName) == 1 && !unicode.IsLetter(rune(packageName[0])) && !unicode.IsNumber(rune(packageName[0])) { + return false + } + return packageNameRegex.MatchString(packageName) && packageName != ".." +} + +// UploadState uploads the specific terraform package. +func UploadState(ctx *context.Context) { + packageName := ctx.PathParam("name") + + if !isValidPackageName(packageName) { + apiError(ctx, http.StatusBadRequest, errors.New("invalid package name")) + return + } + lockKey := getLockKey(ctx) + release, err := globallock.Lock(ctx, lockKey) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer release() + + p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName) + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if p != nil { + // Check lock + lock, err := terraform_module.GetLock(ctx, p.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + // If the state is locked, enforce the lock + if lock.IsLocked() && lock.ID != ctx.FormString("ID") { + ctx.JSON(http.StatusLocked, lock) + return + } + } + + upload, needToClose, err := ctx.UploadStream() + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if needToClose { + defer upload.Close() + } + + buf, err := packages_module.CreateHashedBufferFromReader(upload) + if err != nil { + log.Error("Error creating hashed buffer: %v", err) + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + state, err := terraform_module.ParseState(buf) + if err != nil { + log.Error("Error decoding state: %v", err) + apiError(ctx, http.StatusBadRequest, err) + return + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + _, _, err = packages_service.CreatePackageOrAddFileToExisting( + ctx, + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeTerraformState, + Name: packageName, + Version: strconv.FormatUint(state.Serial, 10), + }, + Creator: ctx.Doer, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: stateFilename, + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + }, + ) + if err != nil { + switch { + case errors.Is(err, packages_model.ErrDuplicatePackageFile): + apiError(ctx, http.StatusConflict, err) + case errors.Is(err, packages_service.ErrQuotaTotalCount), errors.Is(err, packages_service.ErrQuotaTypeSize), errors.Is(err, packages_service.ErrQuotaTotalSize): + apiError(ctx, http.StatusForbidden, err) + default: + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + ctx.Status(http.StatusCreated) +} + +// DeleteStateBySerial deletes the specific serial of a terraform package as long as it's not the latest one. +func DeleteStateBySerial(ctx *context.Context) { + lockKey := getLockKey(ctx) + release, err := globallock.Lock(ctx, lockKey) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer release() + + serial := ctx.PathParam("serial") + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, ctx.PathParam("name"), serial) + if errors.Is(err, packages_model.ErrPackageNotExist) { + apiError(ctx, http.StatusNotFound, err) + return + } else if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pvLatest, err := getLatestVersion(ctx, ctx.PathParam("name")) + if errors.Is(err, packages_model.ErrPackageNotExist) { + apiError(ctx, http.StatusNotFound, err) + return + } else if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if pvLatest.ID == pv.ID { + apiError(ctx, http.StatusForbidden, errors.New("cannot delete the latest version")) + return + } + + err = packages_service.DeletePackageVersionAndReferences(ctx, pv) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + ctx.Status(http.StatusNoContent) +} + +// DeleteState deletes the specific file of a terraform package. +// Fails if the state is locked +func DeleteState(ctx *context.Context) { + packageName := ctx.PathParam("name") + + lockKey := getLockKey(ctx) + release, err := globallock.Lock(ctx, lockKey) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer release() + + p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName) + if err != nil { + if errors.Is(err, packages_model.ErrPackageNotExist) { + apiError(ctx, http.StatusNotFound, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + + lock, err := terraform_module.GetLock(ctx, p.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if lock.IsLocked() { + apiError(ctx, http.StatusLocked, errors.New("terraform state is locked")) + return + } + + pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ + PackageID: p.ID, + IsInternal: optional.None[bool](), + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + err = packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypePackage, p.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + for _, pv := range pvs { + if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + + if err := packages_model.DeletePackageByID(ctx, p.ID); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusOK) +} + +// LockState locks the specific terraform state. +// Internally, it adds a property to the package with the lock information +// Caveat being that it allocates a package if one doesn't exist to attach the property +func LockState(ctx *context.Context) { + packageName := ctx.PathParam("name") + if !isValidPackageName(packageName) { + apiError(ctx, http.StatusBadRequest, errors.New("invalid package name")) + return + } + + var reqLockInfo *terraform_module.LockInfo + reqLockInfo, err := terraform_module.ParseLockInfo(ctx.Req.Body) + if err != nil { + apiError(ctx, http.StatusBadRequest, err) + return + } + + lockKey := getLockKey(ctx) + release, err := globallock.Lock(ctx, lockKey) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer release() + + p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName) + if err != nil { + // If the package doesn't exist, allocate it for the lock. + if errors.Is(err, packages_model.ErrPackageNotExist) { + p = &packages_model.Package{ + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeTerraformState, + Name: packageName, + LowerName: strings.ToLower(packageName), + } + if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } else { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + + currentLock, err := terraform_module.GetLock(ctx, p.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + if currentLock.IsLocked() { + ctx.JSON(http.StatusLocked, currentLock) + return + } + + err = terraform_module.SetLock(ctx, p.ID, reqLockInfo) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusOK) +} + +// UnlockState unlock the specific terraform state. +// Internally, it clears the package property +func UnlockState(ctx *context.Context) { + packageName := ctx.PathParam("name") + if !isValidPackageName(packageName) { + apiError(ctx, http.StatusBadRequest, errors.New("invalid package name")) + return + } + + reqLockInfo, err := terraform_module.ParseLockInfo(ctx.Req.Body) + if err != nil { + apiError(ctx, http.StatusBadRequest, err) + return + } + + lockKey := getLockKey(ctx) + release, err := globallock.Lock(ctx, lockKey) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer release() + + p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName) + if err != nil { + if errors.Is(err, packages_model.ErrPackageNotExist) { + ctx.Status(http.StatusOK) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + + existingLock, err := terraform_module.GetLock(ctx, p.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + // we can bypass messing with the lock since it's empty + if !existingLock.IsLocked() { + ctx.Status(http.StatusOK) + return + } + + // Unlocking ID must be the same as locker one. + if existingLock.ID != reqLockInfo.ID { + apiError(ctx, http.StatusLocked, errors.New("lock ID mismatch")) + return + } + // We can clear the state if lock id matches + err = terraform_module.RemoveLock(ctx, p.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusOK) +} + +func getLatestVersion(ctx *context.Context, packageName string) (*packages_model.PackageVersion, error) { + pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeTerraformState, + Name: packages_model.SearchValue{ExactMatch: true, Value: packageName}, + IsInternal: optional.Some(false), + Sort: packages_model.SortCreatedDesc, + }) + if err != nil { + return nil, err + } + if len(pvs) == 0 { + return nil, packages_model.ErrPackageNotExist + } + return pvs[0], nil +} + +func getLockKey(ctx *context.Context) string { + return fmt.Sprintf("terraform_lock_%d_%s", ctx.Package.Owner.ID, strings.ToLower(ctx.PathParam("name"))) +} diff --git a/routers/api/packages/terraform/terraform_test.go b/routers/api/packages/terraform/terraform_test.go new file mode 100644 index 0000000000..e4705d0ccf --- /dev/null +++ b/routers/api/packages/terraform/terraform_test.go @@ -0,0 +1,36 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package terraform + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidatePackageName(t *testing.T) { + bad := []string{ + "", + ".", + "..", + "-", + "a?b", + "a b", + "a/b", + } + for _, name := range bad { + assert.False(t, isValidPackageName(name), "bad=%q", name) + } + + good := []string{ + "a", + "1", + "a-", + "a_b", + "c.d+", + } + for _, name := range good { + assert.True(t, isValidPackageName(name), "good=%q", name) + } +} diff --git a/routers/api/v1/admin/action.go b/routers/api/v1/admin/action.go index 2fbb8e1a95..4c9c1ed58c 100644 --- a/routers/api/v1/admin/action.go +++ b/routers/api/v1/admin/action.go @@ -29,6 +29,14 @@ func ListWorkflowJobs(ctx *context.APIContext) { // in: query // description: page size of results // type: integer + // - name: sort + // in: query + // description: sort jobs by attribute. Supported values are "id". Default is "id" + // type: string + // - name: order + // in: query + // description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc" + // type: string // responses: // "200": // "$ref": "#/responses/WorkflowJobsList" @@ -36,8 +44,10 @@ func ListWorkflowJobs(ctx *context.APIContext) { // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" - shared.ListJobs(ctx, 0, 0, 0) + shared.ListJobs(ctx, 0, 0, 0, nil) } // ListWorkflowRuns Lists all runs diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index 6390bb7e82..28c21de15f 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -48,7 +48,7 @@ func CreateOrg(ctx *context.APIContext) { visibility := api.VisibleTypePublic if form.Visibility != "" { - visibility = api.VisibilityModes[form.Visibility] + visibility = api.VisibilityModes[string(form.Visibility)] } org := &organization.Organization{ diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go index 93983f6c7e..3d27c87935 100644 --- a/routers/api/v1/admin/runners.go +++ b/routers/api/v1/admin/runners.go @@ -40,7 +40,7 @@ func ListRunners(ctx *context.APIContext) { // required: false // responses: // "200": - // "$ref": "#/definitions/ActionRunnersResponse" + // "$ref": "#/responses/RunnerList" // "400": // "$ref": "#/responses/error" // "404": @@ -63,7 +63,7 @@ func GetRunner(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": @@ -115,7 +115,7 @@ func UpdateRunner(ctx *context.APIContext) { // "$ref": "#/definitions/EditActionRunnerOption" // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index b9dd12f8ff..3fb5856e33 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -123,7 +123,7 @@ func CreateUser(ctx *context.APIContext) { } if form.Visibility != "" { - visibility := api.VisibilityModes[form.Visibility] + visibility := api.VisibilityModes[string(form.Visibility)] overwriteDefault.Visibility = &visibility } @@ -239,7 +239,7 @@ func EditUser(ctx *context.APIContext) { Description: optional.FromPtr(form.Description), IsActive: optional.FromPtr(form.Active), IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin), - Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility), + Visibility: optional.FromMapLookup(api.VisibilityModes, string(form.Visibility)), AllowGitHook: optional.FromPtr(form.AllowGitHook), AllowImportLocal: optional.FromPtr(form.AllowImportLocal), MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation), @@ -464,24 +464,9 @@ func SearchUsers(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) - orderBy := db.SearchOrderByAlphabetically - sortMode := ctx.FormString("sort") - if len(sortMode) > 0 { - sortOrder := ctx.FormString("order") - if len(sortOrder) == 0 { - sortOrder = "asc" - } - if searchModeMap, ok := user_model.AdminUserOrderByMap[sortOrder]; ok { - if order, ok := searchModeMap[sortMode]; ok { - orderBy = order - } else { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode)) - return - } - } else { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder)) - return - } + orderBy, ok := utils.ResolveSortOrder(ctx, user_model.AdminUserOrderByMap, db.SearchOrderByAlphabetically) + if !ok { + return } var visible []api.VisibleType diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 1c7e6b6db3..1e8ce01ce0 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -11,11 +11,9 @@ // // Consumes: // - application/json -// - text/plain // // Produces: // - application/json -// - text/html // // Security: // - BasicAuth : @@ -202,7 +200,7 @@ func repoAssignment() func(ctx *context.APIContext) { if needTwoFactor { ctx.Repo.Permission = access_model.PermissionNoAccess() } else { - ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + ctx.Repo.Permission, err = access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return @@ -396,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 } @@ -436,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 } @@ -635,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"+ @@ -658,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"+ @@ -681,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"+ @@ -707,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 } @@ -867,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, @@ -1235,10 +1232,10 @@ func Routes() *web.Router { m.Group("/branch_protections", func() { m.Get("", repo.ListBranchProtections) m.Post("", bind(api.CreateBranchProtectionOption{}), mustNotBeArchived, repo.CreateBranchProtection) - m.Group("/{name}", func() { + m.Group("/*", func() { m.Get("", repo.GetBranchProtection) m.Patch("", bind(api.EditBranchProtectionOption{}), mustNotBeArchived, repo.EditBranchProtection) - m.Delete("", repo.DeleteBranchProtection) + m.Delete("", mustNotBeArchived, repo.DeleteBranchProtection) }) m.Post("/priority", bind(api.UpdateBranchProtectionPriories{}), mustNotBeArchived, repo.UpdateBranchProtectionPriories) }, reqToken(), reqAdmin()) @@ -1262,6 +1259,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) @@ -1276,7 +1277,7 @@ func Routes() *web.Router { m.Delete("", reqRepoWriter(unit.TypeActions), repo.DeleteArtifact) }) m.Get("/artifacts/{artifact_id}/zip", repo.DownloadArtifact) - }, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true)) + }, reqRepoReader(unit.TypeActions)) m.Group("/keys", func() { m.Combo("").Get(repo.ListDeployKeys). Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey) @@ -1373,6 +1374,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()) @@ -1585,10 +1587,11 @@ func Routes() *web.Router { m.Group("/packages/{username}", func() { m.Group("/{type}/{name}", func() { m.Get("/", packages.ListPackageVersions) + m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) m.Group("/{version}", func() { m.Get("", packages.GetPackage) - m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) + m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackageVersion) m.Get("/files", packages.ListPackageFiles) }) @@ -1616,7 +1619,8 @@ func Routes() *web.Router { Delete(reqToken(), reqOrgOwnership(), org.Delete) m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename) m.Combo("/repos").Get(user.ListOrgRepos). - Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo) + Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo). + Delete(reqToken(), reqOrgOwnership(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), org.DeleteOrgRepos) m.Group("/members", func() { m.Get("", reqToken(), org.ListMembers) m.Combo("/{username}").Get(reqToken(), org.IsMember). @@ -1755,14 +1759,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 909310b4c8..f7623b9105 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 18ed602ddb..d218c19fd4 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -492,7 +492,7 @@ func (Action) ListRunners(ctx *context.APIContext) { // required: false // responses: // "200": - // "$ref": "#/definitions/ActionRunnersResponse" + // "$ref": "#/responses/RunnerList" // "400": // "$ref": "#/responses/error" // "404": @@ -520,7 +520,7 @@ func (Action) GetRunner(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": @@ -582,7 +582,7 @@ func (Action) UpdateRunner(ctx *context.APIContext) { // "$ref": "#/definitions/EditActionRunnerOption" // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": @@ -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/org/org.go b/routers/api/v1/org/org.go index ce2a2e5580..2df871a0aa 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -5,14 +5,20 @@ package org import ( + gocontext "context" "errors" + "fmt" "net/http" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" + 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/graceful" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -23,6 +29,7 @@ import ( "code.gitea.io/gitea/services/convert" feed_service "code.gitea.io/gitea/services/feed" "code.gitea.io/gitea/services/org" + repo_service "code.gitea.io/gitea/services/repository" user_service "code.gitea.io/gitea/services/user" ) @@ -251,7 +258,7 @@ func Create(ctx *context.APIContext) { visibility := api.VisibleTypePublic if form.Visibility != "" { - visibility = api.VisibilityModes[form.Visibility] + visibility = api.VisibilityModes[string(form.Visibility)] } org := &organization.Organization{ @@ -395,7 +402,7 @@ func Edit(ctx *context.APIContext) { Description: optional.FromPtr(form.Description), Website: optional.FromPtr(form.Website), Location: optional.FromPtr(form.Location), - Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()), + Visibility: optional.FromMapLookup(api.VisibilityModes, string(optional.FromPtr(form.Visibility).Value())), RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), } if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil { @@ -497,3 +504,70 @@ func ListOrgActivityFeeds(ctx *context.APIContext) { ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer)) } + +func deleteOrgReposBackground(ctx gocontext.Context, org *organization.Organization, repoIDs []int64, doer *user_model.User) { + defer func() { + if r := recover(); r != nil { + log.Error("panic during org repo deletion: %v, stack: %v", r, log.Stack(2)) + } + }() + + for _, repoID := range repoIDs { + repo, err := repo_model.GetRepositoryByID(ctx, repoID) + if err != nil { + desc := fmt.Sprintf("Failed to get repository ID %d in org %s: %v", repoID, org.Name, err) + _ = system_model.CreateNotice(ctx, system_model.NoticeRepository, desc) + log.Error("GetRepositoryByID failed: %v", desc) + continue + } + if err := repo_service.DeleteRepository(ctx, doer, repo, true); err != nil { + desc := fmt.Sprintf("Failed to delete repository %s (ID: %d) in org %s: %v", repo.Name, repo.ID, org.Name, err) + _ = system_model.CreateNotice(ctx, system_model.NoticeRepository, desc) + log.Error("DeleteRepository failed: %v", desc) + continue + } + log.Info("Successfully deleted repository %s (ID: %d) in org %s", repo.Name, repo.ID, org.Name) + } + log.Info("Completed deletion of repositories in org %s", org.Name) +} + +func DeleteOrgRepos(ctx *context.APIContext) { + // swagger:operation DELETE /orgs/{org}/repos organization orgDeleteRepos + // --- + // summary: Delete all repositories in an organization + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // responses: + // "202": + // "$ref": "#/responses/empty" + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + // Intentionally it only loads repository IDs to avoid loading too much data into memory + // There is no need to do pagination here as the number of repositories is expected to be manageable + repoIDs, err := repo_model.GetOrgRepositoryIDs(ctx, ctx.Org.Organization.ID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + if len(repoIDs) == 0 { + ctx.Status(http.StatusNoContent) + return + } + + // Start deletion (slow) in background with detached context, so it can continue even if the request is canceled + go deleteOrgReposBackground(graceful.GetManager().ShutdownContext(), ctx.Org.Organization, repoIDs, ctx.Doer) + + ctx.Status(http.StatusAccepted) +} diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 3b5711eea3..74ad987540 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -210,7 +210,7 @@ func CreateTeam(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.CreateTeamOption) - teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin) + teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin) team := &organization.Team{ OrgID: ctx.Org.Organization.ID, Name: form.Name, @@ -224,7 +224,7 @@ func CreateTeam(ctx *context.APIContext) { if len(form.UnitsMap) > 0 { attachTeamUnitsMap(team, form.UnitsMap) } else if len(form.Units) > 0 { - unitPerm := perm.ParseAccessMode(form.Permission, perm.AccessModeRead, perm.AccessModeWrite) + unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite) attachTeamUnits(team, unitPerm, form.Units) } else { ctx.APIErrorInternal(errors.New("units permission should not be empty")) @@ -298,7 +298,7 @@ func EditTeam(ctx *context.APIContext) { isAuthChanged := false isIncludeAllChanged := false if !team.IsOwnerTeam() && len(form.Permission) != 0 { - teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin) + teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin) if team.AccessMode != teamPermission { isAuthChanged = true team.AccessMode = teamPermission @@ -314,7 +314,7 @@ func EditTeam(ctx *context.APIContext) { if len(form.UnitsMap) > 0 { attachTeamUnitsMap(team, form.UnitsMap) } else if len(form.Units) > 0 { - unitPerm := perm.ParseAccessMode(form.Permission, perm.AccessModeRead, perm.AccessModeWrite) + unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite) attachTeamUnits(team, unitPerm, form.Units) } } else { @@ -576,7 +576,7 @@ func GetTeamRepos(ctx *context.APIContext) { } repos := make([]*api.Repository, len(teamRepos)) for i, repo := range teamRepos { - permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return @@ -628,7 +628,7 @@ func GetTeamRepo(ctx *context.APIContext) { return } - permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index cee0daccae..c7c66f549c 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -43,7 +43,7 @@ func ListPackages(ctx *context.APIContext) { // in: query // description: package type filter // type: string - // enum: [alpine, cargo, chef, composer, conan, conda, container, cran, debian, generic, go, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, vagrant] + // enum: [alpine, cargo, chef, composer, conan, conda, container, cran, debian, generic, go, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, terraform, vagrant] // - name: q // in: query // description: name filter @@ -118,7 +118,7 @@ func GetPackage(ctx *context.APIContext) { // DeletePackage deletes a package func DeletePackage(ctx *context.APIContext) { - // swagger:operation DELETE /packages/{owner}/{type}/{name}/{version} package deletePackage + // swagger:operation DELETE /packages/{owner}/{type}/{name} package deletePackage // --- // summary: Delete a package // parameters: @@ -137,6 +137,41 @@ func DeletePackage(ctx *context.APIContext) { // description: name of the package // type: string // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + err := packages_service.RemovePackage(ctx, ctx.Doer, ctx.Package.Descriptor.Package) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.Status(http.StatusNoContent) +} + +// DeletePackageVersion deletes a package version +func DeletePackageVersion(ctx *context.APIContext) { + // swagger:operation DELETE /packages/{owner}/{type}/{name}/{version} package deletePackageVersion + // --- + // summary: Delete a package version + // parameters: + // - name: owner + // in: path + // description: owner of the package + // type: string + // required: true + // - name: type + // in: path + // description: type of the package + // type: string + // required: true + // - name: name + // in: path + // description: name of the package + // type: string + // required: true // - name: version // in: path // description: version of the package diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index d704092051..666d4f98ef 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -21,8 +21,8 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" 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" @@ -34,7 +34,7 @@ import ( "code.gitea.io/gitea/services/convert" secret_service "code.gitea.io/gitea/services/secrets" - "github.com/nektos/act/pkg/model" + "gitea.com/gitea/runner/act/model" ) // ListActionsSecrets list an repo's actions secrets @@ -561,7 +561,7 @@ func (Action) ListRunners(ctx *context.APIContext) { // required: false // responses: // "200": - // "$ref": "#/definitions/ActionRunnersResponse" + // "$ref": "#/responses/RunnerList" // "400": // "$ref": "#/responses/error" // "404": @@ -594,7 +594,7 @@ func (Action) GetRunner(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": @@ -666,7 +666,7 @@ func (Action) UpdateRunner(ctx *context.APIContext) { // "$ref": "#/definitions/EditActionRunnerOption" // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": @@ -676,7 +676,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 // --- @@ -707,6 +707,14 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) { // in: query // description: page size of results // type: integer + // - name: sort + // in: query + // description: sort jobs by attribute. Supported values are "id". Default is "id" + // type: string + // - name: order + // in: query + // description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc" + // type: string // responses: // "200": // "$ref": "#/responses/WorkflowJobsList" @@ -714,10 +722,12 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) { // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" 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. @@ -847,6 +857,12 @@ func ListActionTasks(ctx *context.APIContext) { res := new(api.ActionTaskResponse) res.TotalCount = total + taskList := actions_model.TaskList(tasks) + if err := taskList.LoadAttributes(ctx); err != nil { + ctx.APIErrorInternal(err) + return + } + res.Entries = make([]*api.ActionTask, len(tasks)) for i := range tasks { convertedTask, err := convert.ToActionTask(ctx, tasks[i]) @@ -858,7 +874,7 @@ func ListActionTasks(ctx *context.APIContext) { } ctx.SetLinkHeader(total, listOptions.PageSize) - ctx.SetTotalCountHeader(total) // Duplicates api response field but it's better to set it for consistency + ctx.SetTotalCountHeader(total) // Duplicates api response field, but it's better to set it for consistency ctx.JSON(http.StatusOK, &res) } @@ -1154,6 +1170,7 @@ func getCurrentRepoActionRunByID(ctx *context.APIContext) *actions_model.ActionR ctx.APIErrorInternal(err) return nil } + run.Repo = ctx.Repo.Repository return run } @@ -1163,14 +1180,33 @@ 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 } + jobs.SortMatrixGroupsByName() 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 @@ -1192,7 +1228,7 @@ func GetWorkflowRun(ctx *context.APIContext) { // - name: run // in: path // description: id of the run - // type: string + // type: integer // required: true // responses: // "200": @@ -1207,7 +1243,56 @@ func GetWorkflowRun(ctx *context.APIContext) { return } - convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run) + convertedRun, err := convert.ToActionWorkflowRun(ctx, 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, run, attempt) if err != nil { ctx.APIErrorInternal(err) return @@ -1247,6 +1332,8 @@ func RerunWorkflowRun(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/error" // "422": // "$ref": "#/responses/validationError" @@ -1255,12 +1342,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, run, nil) if err != nil { ctx.APIErrorInternal(err) return @@ -1298,6 +1385,8 @@ func RerunFailedWorkflowRun(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/error" // "422": // "$ref": "#/responses/validationError" @@ -1306,7 +1395,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 +1440,8 @@ func RerunWorkflowJob(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/error" // "422": // "$ref": "#/responses/validationError" @@ -1367,12 +1458,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 +1491,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) } @@ -1424,6 +1537,14 @@ func ListWorkflowRunJobs(ctx *context.APIContext) { // in: query // description: page size of results // type: integer + // - name: sort + // in: query + // description: sort jobs by attribute. Supported values are "id". Default is "id" + // type: string + // - name: order + // in: query + // description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc" + // type: string // responses: // "200": // "$ref": "#/responses/WorkflowJobsList" @@ -1431,6 +1552,8 @@ func ListWorkflowRunJobs(ctx *context.APIContext) { // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run") @@ -1440,9 +1563,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. @@ -1708,7 +1897,7 @@ func GetArtifact(ctx *context.APIContext) { return } - if actions.IsArtifactV4(art) { + if actions_service.IsArtifactV4(art) { convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, art) if err != nil { ctx.APIErrorInternal(err) @@ -1757,8 +1946,8 @@ func DeleteArtifact(ctx *context.APIContext) { return } - if actions.IsArtifactV4(art) { - if err := actions_model.SetArtifactNeedDelete(ctx, art.RunID, art.ArtifactName); err != nil { + if actions_service.IsArtifactV4(art) { + if err := actions_model.SetArtifactNeedDeleteByID(ctx, art.ID); err != nil { ctx.APIErrorInternal(err) return } @@ -1784,7 +1973,7 @@ func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) str func buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string { // endPoint is a path like "api/v1/repos/owner/repo/actions/artifacts/1/zip/raw" expires := time.Now().Add(60 * time.Minute).Unix() - uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.URLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10) + uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.RawURLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10) return uploadURL } @@ -1829,18 +2018,16 @@ func DownloadArtifact(ctx *context.APIContext) { ctx.APIError(http.StatusNotFound, "Artifact has expired") return } - ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName)) - if actions.IsArtifactV4(art) { - ok, err := actions.DownloadArtifactV4ServeDirectOnly(ctx.Base, art) - if ok { - return - } - if err != nil { - ctx.APIErrorInternal(err) + if actions_service.IsArtifactV4(art) { + // @actions/toolkit asserts that downloaded artifacts of a different runid return 302 + // https://github.com/actions/toolkit/blob/44d43b5490b02998bd09b0c4ff369a4cc67876c2/packages/artifact/src/internal/download/download-artifact.ts#L203-L210 + if actions_service.DownloadArtifactV4ServeDirect(ctx.Base, art) { return } + // @actions/toolkit asserts a 302 for the artifact download, so we have to build a signed URL and redirect to it + // TODO: a perma link to the code for reference redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository, art.ID), art.ID) ctx.Redirect(redirectURL, http.StatusFound) return @@ -1868,7 +2055,7 @@ func DownloadArtifactRaw(ctx *context.APIContext) { sigStr := ctx.Req.URL.Query().Get("sig") expiresStr := ctx.Req.URL.Query().Get("expires") - sigBytes, _ := base64.URLEncoding.DecodeString(sigStr) + sigBytes, _ := base64.RawURLEncoding.DecodeString(sigStr) expires, _ := strconv.ParseInt(expiresStr, 10, 64) expectedSig := buildSignature(buildDownloadRawEndpoint(repo, art.ID), expires, art.ID) @@ -1887,10 +2074,8 @@ func DownloadArtifactRaw(ctx *context.APIContext) { ctx.APIError(http.StatusNotFound, "Artifact has expired") return } - ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName)) - - if actions.IsArtifactV4(art) { - err := actions.DownloadArtifactV4(ctx.Base, art) + if actions_service.IsArtifactV4(art) { + err := actions_service.DownloadArtifactV4(ctx.Base, art) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index c3b3fc1085..248bb8b5b2 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 @@ -563,7 +563,7 @@ func GetBranchProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - bpName := ctx.PathParam("name") + bpName := ctx.PathParam("*") bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { ctx.APIErrorInternal(err) @@ -711,7 +711,19 @@ func CreateBranchProtection(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } - var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64 + var bypassAllowlistUsers []int64 + if form.EnableBypassAllowlist { + bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + } + var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams, bypassAllowlistTeams []int64 if repo.Owner.IsOrganization() { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false) if err != nil { @@ -749,6 +761,17 @@ func CreateBranchProtection(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } + if form.EnableBypassAllowlist { + bypassAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.BypassAllowlistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + } } protectBranch = &git_model.ProtectedBranch{ @@ -762,6 +785,7 @@ func CreateBranchProtection(ctx *context.APIContext) { EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist, ForcePushAllowlistDeployKeys: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistDeployKeys, EnableMergeWhitelist: form.EnableMergeWhitelist, + EnableBypassAllowlist: form.EnableBypassAllowlist, EnableStatusCheck: form.EnableStatusCheck, StatusCheckContexts: form.StatusCheckContexts, EnableApprovalsWhitelist: form.EnableApprovalsWhitelist, @@ -786,6 +810,8 @@ func CreateBranchProtection(ctx *context.APIContext) { MergeTeamIDs: mergeWhitelistTeams, ApprovalsUserIDs: approvalsWhitelistUsers, ApprovalsTeamIDs: approvalsWhitelistTeams, + BypassUserIDs: bypassAllowlistUsers, + BypassTeamIDs: bypassAllowlistTeams, }); err != nil { ctx.APIErrorInternal(err) return @@ -845,7 +871,7 @@ func EditBranchProtection(ctx *context.APIContext) { // "$ref": "#/responses/repoArchivedError" form := web.GetForm(ctx).(*api.EditBranchProtectionOption) repo := ctx.Repo.Repository - bpName := ctx.PathParam("name") + bpName := ctx.PathParam("*") protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { ctx.APIErrorInternal(err) @@ -906,6 +932,10 @@ func EditBranchProtection(ctx *context.APIContext) { protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist } + if form.EnableBypassAllowlist != nil { + protectBranch.EnableBypassAllowlist = *form.EnableBypassAllowlist + } + if form.EnableStatusCheck != nil { protectBranch.EnableStatusCheck = *form.EnableStatusCheck } @@ -958,7 +988,7 @@ func EditBranchProtection(ctx *context.APIContext) { protectBranch.BlockAdminMergeOverride = *form.BlockAdminMergeOverride } - var whitelistUsers, forcePushAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers []int64 + var whitelistUsers, forcePushAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers, bypassAllowlistUsers []int64 if form.PushWhitelistUsernames != nil { whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false) if err != nil { @@ -1011,8 +1041,21 @@ func EditBranchProtection(ctx *context.APIContext) { } else { approvalsWhitelistUsers = protectBranch.ApprovalsWhitelistUserIDs } + if form.BypassAllowlistUsernames != nil { + bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + } else { + bypassAllowlistUsers = protectBranch.BypassAllowlistUserIDs + } - var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64 + var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams, bypassAllowlistTeams []int64 if repo.Owner.IsOrganization() { if form.PushWhitelistTeams != nil { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false) @@ -1066,6 +1109,23 @@ func EditBranchProtection(ctx *context.APIContext) { } else { approvalsWhitelistTeams = protectBranch.ApprovalsWhitelistTeamIDs } + if form.BypassAllowlistTeams != nil { + bypassAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.BypassAllowlistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } + ctx.APIErrorInternal(err) + return + } + } else { + bypassAllowlistTeams = protectBranch.BypassAllowlistTeamIDs + } + } + if !protectBranch.EnableBypassAllowlist { + bypassAllowlistUsers = nil + bypassAllowlistTeams = nil } err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{ @@ -1077,6 +1137,8 @@ func EditBranchProtection(ctx *context.APIContext) { MergeTeamIDs: mergeWhitelistTeams, ApprovalsUserIDs: approvalsWhitelistUsers, ApprovalsTeamIDs: approvalsWhitelistTeams, + BypassUserIDs: bypassAllowlistUsers, + BypassTeamIDs: bypassAllowlistTeams, }) if err != nil { ctx.APIErrorInternal(err) @@ -1168,7 +1230,7 @@ func DeleteBranchProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - bpName := ctx.PathParam("name") + bpName := ctx.PathParam("*") bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index eed9c19fe1..7d9b290c2a 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -181,7 +181,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) { p := perm.AccessModeWrite if form.Permission != nil { - p = perm.ParseAccessMode(*form.Permission, perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeAdmin) + p = perm.ParseAccessMode(string(*form.Permission), perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeAdmin) } if err := repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, collaborator, p); err != nil { @@ -291,7 +291,7 @@ func GetRepoPermissions(ctx *context.APIContext) { return } - permission, err := access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, collaborator) + permission, err := access_model.GetIndividualUserRepoPermission(ctx, ctx.Repo.Repository, collaborator) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 6285138c27..69dceb6db9 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -62,13 +62,20 @@ func CompareDiff(ctx *context.APIContext) { apiCommits := make([]*api.Commit, 0, len(compareInfo.Commits)) userCache := make(map[string]*user_model.User) + for i := 0; i < len(compareInfo.Commits); i++ { - apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareInfo.Commits[i], userCache, + apiCommit, err := convert.ToCommit( + ctx, + compareInfo.HeadRepo, + compareInfo.HeadGitRepo, + compareInfo.Commits[i], + userCache, convert.ToCommitOptions{ Stat: true, Verification: verification, Files: files, - }) + }, + ) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index d0596d778b..9949928622 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -17,9 +17,9 @@ import ( git_model "code.gitea.io/gitea/models/git" "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/lfs" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" api "code.gitea.io/gitea/modules/structs" @@ -151,35 +151,18 @@ func GetRawFileOrLFS(ctx *context.APIContext) { // OK, now the blob is known to have at most 1024 (lfs pointer max size) bytes, // we can simply read this in one go (This saves reading it twice) - dataRc, err := blob.DataAsync() + lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize) if err != nil { ctx.APIErrorInternal(err) return } - buf, err := io.ReadAll(dataRc) - if err != nil { - _ = dataRc.Close() - ctx.APIErrorInternal(err) - return - } - - if err := dataRc.Close(); err != nil { - log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Repo.Repository, err) - } - // Check if the blob represents a pointer - pointer, _ := lfs.ReadPointer(bytes.NewReader(buf)) + pointer, _ := lfs.ReadPointerFromBuffer(lfsPointerBuf) // if it's not a pointer, just serve the data directly if !pointer.IsValid() { - // First handle caching for the blob - if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { - return - } - - // If not cached - serve! - common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)) + _, _ = ctx.Resp.Write(lfsPointerBuf) return } @@ -188,12 +171,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { // If there isn't one, just serve the data directly if errors.Is(err, git_model.ErrLFSObjectNotExist) { - // Handle caching for the blob SHA (not the LFS object OID) - if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { - return - } - - common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)) + _, _ = ctx.Resp.Write(lfsPointerBuf) return } else if err != nil { ctx.APIErrorInternal(err) @@ -214,14 +192,13 @@ func GetRawFileOrLFS(ctx *context.APIContext) { } } - lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer) + lfsDataFile, err := lfs.ReadMetaObject(meta.Pointer) if err != nil { ctx.APIErrorInternal(err) return } - defer lfsDataRc.Close() - - common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc) + defer lfsDataFile.Close() + httplib.ServeUserContentByFile(ctx.Base.Req, ctx.Base.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath}) } func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) { diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 58f66954e1..38a3952879 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -6,7 +6,6 @@ package repo import ( "errors" - "fmt" "net/http" "code.gitea.io/gitea/models/organization" @@ -14,6 +13,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -71,7 +71,7 @@ func ListForks(ctx *context.APIContext) { apiForks := make([]*api.Repository, len(forks)) for i, fork := range forks { - permission, err := access_model.GetUserRepoPermission(ctx, fork, ctx.Doer) + permission, err := access_model.GetDoerRepoPermission(ctx, fork, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return @@ -83,6 +83,35 @@ func ListForks(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiForks) } +func prepareDoerCreateRepoInOrg(ctx *context.APIContext, orgName string) *organization.Organization { + org, err := organization.GetOrgByName(ctx, orgName) + if errors.Is(err, util.ErrNotExist) { + ctx.APIErrorNotFound() + return nil + } else if err != nil { + ctx.APIErrorInternal(err) + return nil + } + + if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) { + ctx.APIErrorNotFound() + return nil + } + + if !ctx.Doer.IsAdmin { + canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID) + if err != nil { + ctx.APIErrorInternal(err) + return nil + } + if !canCreate { + ctx.APIError(http.StatusForbidden, "User is not allowed to create repositories in this organization.") + return nil + } + } + return org +} + // CreateFork create a fork of a repo func CreateFork(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/forks repository createFork @@ -118,41 +147,18 @@ func CreateFork(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.CreateForkOption) - repo := ctx.Repo.Repository - var forker *user_model.User // user/org that will own the fork - if form.Organization == nil { - forker = ctx.Doer - } else { - org, err := organization.GetOrgByName(ctx, *form.Organization) - if err != nil { - if organization.IsErrOrgNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) - } else { - ctx.APIErrorInternal(err) - } + forkOwner := ctx.Doer // user/org that will own the fork + if form.Organization != nil { + org := prepareDoerCreateRepoInOrg(ctx, *form.Organization) + if ctx.Written() { return } - if !ctx.Doer.IsAdmin { - isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID) - if err != nil { - ctx.APIErrorInternal(err) - return - } else if !isMember { - ctx.APIError(http.StatusForbidden, fmt.Sprintf("User is no Member of Organisation '%s'", org.Name)) - return - } - } - forker = org.AsUser() + forkOwner = org.AsUser() } - var name string - if form.Name == nil { - name = repo.Name - } else { - name = *form.Name - } - - fork, err := repo_service.ForkRepository(ctx, ctx.Doer, forker, repo_service.ForkRepoOptions{ + repo := ctx.Repo.Repository + name := optional.FromPtr(form.Name).ValueOrDefault(repo.Name) + fork, err := repo_service.ForkRepository(ctx, ctx.Doer, forkOwner, repo_service.ForkRepoOptions{ BaseRepo: repo, Name: name, Description: repo.Description, diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go index f8d61ccf00..6b2c7627d0 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 db205380e4..39ca7fb77e 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 { @@ -690,11 +690,11 @@ func CreateIssue(ctx *context.APIContext) { form.Labels = make([]int64, 0) } - if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil { - if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { - ctx.APIError(http.StatusBadRequest, err) - } else if errors.Is(err, user_model.ErrBlockedUser) { + if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, form.Projects); err != nil { + if errors.Is(err, user_model.ErrBlockedUser) { ctx.APIError(http.StatusForbidden, err) + } else if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusBadRequest, err) } else { ctx.APIErrorInternal(err) } @@ -726,6 +726,9 @@ func EditIssue(ctx *context.APIContext) { // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index} issue issueEditIssue // --- // summary: Edit an issue. If using deadline only the date will be taken into account, and time of day ignored. + // description: | + // Pass `content_version` to enable optimistic locking on body edits. + // If the version doesn't match the current value, the request fails with 409 Conflict. // consumes: // - application/json // produces: @@ -772,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 { @@ -785,6 +788,15 @@ func EditIssue(ctx *context.APIContext) { return } + // Fail fast: if content_version is provided and already stale, reject + // before any mutations. The DB-level check in ChangeContent still + // handles concurrent requests. + // TODO: wrap all mutations in a transaction to fully prevent partial writes. + if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion { + ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged) + return + } + if len(form.Title) > 0 { err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title) if err != nil { @@ -793,10 +805,14 @@ func EditIssue(ctx *context.APIContext) { } } if form.Body != nil { - err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion) + contentVersion := issue.ContentVersion + if form.ContentVersion != nil { + contentVersion = *form.ContentVersion + } + err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion) if err != nil { if errors.Is(err, issues_model.ErrIssueAlreadyChanged) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusConflict, err) return } @@ -897,6 +913,18 @@ func EditIssue(ctx *context.APIContext) { } } + // Update projects if provided + if canWrite && form.Projects != nil { + if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, *form.Projects); err != nil { + if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusBadRequest, err) + } else { + ctx.APIErrorInternal(err) + } + return + } + } + // Refetch from database to assign some automatic values issue, err = issues_model.GetIssueByID(ctx, issue.ID) if err != nil { @@ -1004,7 +1032,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 bfe9c92f1c..b6db388a22 100644 --- a/routers/api/v1/repo/issue_attachment.go +++ b/routers/api/v1/repo/issue_attachment.go @@ -186,7 +186,7 @@ func CreateIssueAttachment(ctx *context.APIContext) { } uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size) - attachment, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Attachment.AllowedTypes, &repo_model.Attachment{ + attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{ Name: filename, UploaderID: ctx.Doer.ID, RepoID: ctx.Repo.Repository.ID, @@ -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 37b5836e1d..5d79b2ec5a 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 } @@ -219,7 +219,7 @@ func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *issue if err != nil { return false } - perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, user) + perm, err := access_model.GetDoerRepoPermission(ctx, c.RefRepo, user) if err != nil { return false } @@ -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 3227f5ddee..9a1ce00f16 100644 --- a/routers/api/v1/repo/issue_comment_attachment.go +++ b/routers/api/v1/repo/issue_comment_attachment.go @@ -193,7 +193,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { } uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size) - attachment, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Attachment.AllowedTypes, &repo_model.Attachment{ + attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{ Name: filename, UploaderID: ctx.Doer.ID, RepoID: ctx.Repo.Repository.ID, @@ -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_dependency.go b/routers/api/v1/repo/issue_dependency.go index 139779dcec..28cdbb3091 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -102,7 +102,7 @@ func GetIssueDependencies(ctx *context.APIContext) { perm = existPerm } else { var err error - perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer) + perm, err = access_model.GetDoerRepoPermission(ctx, &blocker.Repository, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return @@ -351,7 +351,7 @@ func GetIssueBlocks(ctx *context.APIContext) { perm = existPerm } else { var err error - perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer) + perm, err = access_model.GetDoerRepoPermission(ctx, &depMeta.Repository, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return @@ -537,7 +537,7 @@ func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository) return &ctx.Repo.Permission } - perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return nil diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index d5eee2d469..1ac545f41b 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 b9e5bcf6eb..2f797a162f 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 e535b5e009..2c9efd9111 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 } @@ -175,7 +175,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) { // schema: // "$ref": "#/definitions/EditReactionOption" // responses: - // "200": + // "204": // "$ref": "#/responses/empty" // "403": // "$ref": "#/responses/forbidden" @@ -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 } @@ -248,8 +248,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp ctx.APIErrorInternal(err) return } - // ToDo respond 204 - ctx.Status(http.StatusOK) + ctx.Status(http.StatusNoContent) } } @@ -305,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 } @@ -408,7 +407,7 @@ func DeleteIssueReaction(ctx *context.APIContext) { // schema: // "$ref": "#/definitions/EditReactionOption" // responses: - // "200": + // "204": // "$ref": "#/responses/empty" // "403": // "$ref": "#/responses/forbidden" @@ -429,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 } @@ -464,7 +463,6 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i ctx.APIErrorInternal(err) return } - // ToDo respond 204 - ctx.Status(http.StatusOK) + ctx.Status(http.StatusNoContent) } } diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go index f9fbff091d..8818ab2972 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 9355177fce..7431493a3f 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.") @@ -262,7 +257,7 @@ func handleRemoteAddrError(ctx *context.APIContext, err error) { addrErr := err.(*git.ErrInvalidCloneAddr) switch { case addrErr.IsURLError: - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, "The provided URL is invalid.") case addrErr.IsPermissionDenied: if addrErr.LocalPath { ctx.APIError(http.StatusUnprocessableEntity, "You are not allowed to import local repositories.") diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index 0dc9013ff3..ac2d8bba06 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") } @@ -351,11 +351,7 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro return } - remoteSuffix, err := util.CryptoRandomString(10) - if err != nil { - ctx.APIErrorInternal(err) - return - } + remoteSuffix := util.CryptoRandomString(10) remoteAddress, err := util.SanitizeURL(mirrorOption.RemoteAddress) if err != nil { diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index af2f6fae5d..d20fc702a9 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -22,6 +22,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" @@ -520,7 +521,7 @@ func CreatePullRequest(ctx *context.APIContext) { BaseBranch: compareResult.BaseRef.ShortName(), HeadRepo: compareResult.HeadRepo, BaseRepo: repo, - MergeBase: compareResult.MergeBase, + MergeBase: compareResult.CompareBase, Type: issues_model.PullRequestGitea, } @@ -652,11 +653,20 @@ 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 } + // Fail fast: if content_version is provided and already stale, reject + // before any mutations. The DB-level check in ChangeContent still + // handles concurrent requests. + // TODO: wrap all mutations in a transaction to fully prevent partial writes. + if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion { + ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged) + return + } + if len(form.Title) > 0 { err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title) if err != nil { @@ -665,10 +675,14 @@ func EditPullRequest(ctx *context.APIContext) { } } if form.Body != nil { - err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion) + contentVersion := issue.ContentVersion + if form.ContentVersion != nil { + contentVersion = *form.ContentVersion + } + err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion) if err != nil { if errors.Is(err, issues_model.ErrIssueAlreadyChanged) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusConflict, err) return } @@ -701,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) { @@ -715,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 @@ -730,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) @@ -898,6 +912,8 @@ func MergePullRequest(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" // "405": @@ -949,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) { @@ -964,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) } @@ -978,7 +996,7 @@ func MergePullRequest(ctx *context.APIContext) { return } if strings.Contains(err.Error(), "Wrong commit ID") { - ctx.JSON(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err) return } ctx.APIErrorInternal(err) @@ -1113,7 +1131,7 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git }() // user should have permission to read baseRepo's codes and pulls, NOT headRepo's - permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer) + permBase, err := access_model.GetDoerRepoPermission(ctx, baseRepo, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return nil, nil @@ -1127,7 +1145,7 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git // user should have permission to read headRepo's codes // TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it. - permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer) + permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return nil, nil @@ -1157,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 @@ -1235,15 +1253,17 @@ func UpdatePullRequest(ctx *context.APIContext) { return } - rebase := ctx.FormString("style") == "rebase" + // keep API back-compat: when no style is given, default to "merge" rather than the repo's DefaultUpdateStyle, + // so existing API clients keep getting a merge update. + rebase := repo_model.UpdateStyle(ctx.FormString("style", string(repo_model.UpdateStyleMerge))) == repo_model.UpdateStyleRebase - allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, pr, ctx.Doer) + userUpdateStyles, err := pull_service.CheckUserAllowedToUpdate(ctx, pr, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return } - if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) { + if (rebase && !userUpdateStyles.RebaseAllowed) || (!rebase && !userUpdateStyles.MergeAllowed) { ctx.Status(http.StatusForbidden) return } @@ -1401,7 +1421,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) @@ -1409,12 +1428,17 @@ 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 { compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false) } - if err != nil { + + if gitcmd.StderrHasPrefix(err, "fatal: bad revision") { + ctx.APIError(http.StatusNotFound, "invalid base branch or revision") + return + } else if err != nil { ctx.APIErrorInternal(err) return } @@ -1530,7 +1554,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 { @@ -1547,7 +1571,7 @@ func GetPullRequestFiles(ctx *context.APIContext) { return } - startCommitID := compareInfo.MergeBase + startCommitID := compareInfo.CompareBase endCommitID := headCommitID maxLines := setting.Git.MaxGitDiffLines diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index d4b268c009..a049a61aa9 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 // --- @@ -833,7 +915,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions return } - permDoer, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer) + permDoer, err := access_model.GetDoerRepoPermission(ctx, pr.Issue.Repo, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return @@ -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 c87d22614e..2eade9eab5 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_attachment.go b/routers/api/v1/repo/release_attachment.go index 19075961f3..c0bf89a9d0 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -242,7 +242,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { } // Create a new attachment and save the file - attach, err := attachment_service.UploadAttachmentReleaseSizeLimit(ctx, uploaderFile, setting.Repository.Release.AllowedTypes, &repo_model.Attachment{ + attach, err := attachment_service.UploadAttachmentForRelease(ctx, uploaderFile, &repo_model.Attachment{ Name: filename, UploaderID: ctx.Doer.ID, RepoID: ctx.Repo.Repository.ID, diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go index 8991e201d8..bca5871aa7 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 1b3d85346b..12e525464c 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -21,6 +21,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" @@ -37,6 +38,8 @@ import ( "code.gitea.io/gitea/services/convert" feed_service "code.gitea.io/gitea/services/feed" "code.gitea.io/gitea/services/issue" + "code.gitea.io/gitea/services/migrations" + mirror_service "code.gitea.io/gitea/services/mirror" repo_service "code.gitea.io/gitea/services/repository" ) @@ -184,24 +187,11 @@ func Search(ctx *context.APIContext) { opts.IsPrivate = optional.Some(ctx.FormBool("is_private")) } - sortMode := ctx.FormString("sort") - if len(sortMode) > 0 { - sortOrder := ctx.FormString("order") - if len(sortOrder) == 0 { - sortOrder = "asc" - } - if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok { - if orderBy, ok := searchModeMap[sortMode]; ok { - opts.OrderBy = orderBy - } else { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode)) - return - } - } else { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder)) - return - } + orderBy, ok := utils.ResolveSortOrder(ctx, repo_model.OrderByMap, "") + if !ok { + return } + opts.OrderBy = orderBy repos, count, err := repo_model.SearchRepository(ctx, opts) if err != nil { @@ -221,7 +211,7 @@ func Search(ctx *context.APIContext) { }) return } - permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.JSON(http.StatusInternalServerError, api.SearchError{ OK: false, @@ -262,7 +252,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre DefaultBranch: opt.DefaultBranch, TrustModel: repo_model.ToTrustModel(opt.TrustModel), IsTemplate: opt.Template, - ObjectFormatName: opt.ObjectFormatName, + ObjectFormatName: string(opt.ObjectFormatName), }) if err != nil { if repo_model.IsErrRepoAlreadyExist(err) { @@ -498,31 +488,11 @@ func CreateOrgRepo(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" opt := web.GetForm(ctx).(*api.CreateRepoOption) - org, err := organization.GetOrgByName(ctx, ctx.PathParam("org")) - if err != nil { - if organization.IsErrOrgNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) - } else { - ctx.APIErrorInternal(err) - } + orgName := ctx.PathParam("org") + org := prepareDoerCreateRepoInOrg(ctx, orgName) + if ctx.Written() { return } - - if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) { - ctx.APIErrorNotFound("HasOrgOrUserVisible", nil) - return - } - - if !ctx.Doer.IsAdmin { - canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID) - if err != nil { - ctx.APIErrorInternal(err) - return - } else if !canCreate { - ctx.APIError(http.StatusForbidden, "Given user is not allowed to create repository in organization.") - return - } - } CreateUserRepo(ctx, org.AsUser(), *opt) } @@ -588,7 +558,7 @@ func GetByID(ctx *context.APIContext) { return } - permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return @@ -648,7 +618,11 @@ func Edit(ctx *context.APIContext) { } } - if opts.MirrorInterval != nil || opts.EnablePrune != nil { + if opts.MirrorInterval != nil || + opts.EnablePrune != nil || + opts.MirrorUsername != nil || + opts.MirrorPassword != nil || + opts.MirrorToken != nil { if err := updateMirror(ctx, opts); err != nil { return } @@ -778,7 +752,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 @@ -840,7 +814,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 @@ -911,10 +885,20 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { optional.AssignPtrValue(changed, &config.AllowFastForwardOnly, opts.AllowFastForwardOnly) optional.AssignPtrValue(changed, &config.AllowManualMerge, opts.AllowManualMerge) optional.AssignPtrValue(changed, &config.AutodetectManualMerge, opts.AutodetectManualMerge) + optional.AssignPtrValue(changed, &config.AllowMergeUpdate, opts.AllowMergeUpdate) optional.AssignPtrValue(changed, &config.AllowRebaseUpdate, opts.AllowRebaseUpdate) optional.AssignPtrValue(changed, &config.DefaultDeleteBranchAfterMerge, opts.DefaultDeleteBranchAfterMerge) optional.AssignPtrValue(changed, &config.DefaultAllowMaintainerEdit, opts.DefaultAllowMaintainerEdit) optional.AssignPtrString(changed, &config.DefaultMergeStyle, opts.DefaultMergeStyle) + optional.AssignPtrString(changed, &config.DefaultUpdateStyle, opts.DefaultUpdateStyle) + // only validate update-style fields when the caller is actually changing one of them, + // so unrelated PATCH calls don't reject historical configs. + if opts.AllowMergeUpdate != nil || opts.AllowRebaseUpdate != nil || opts.DefaultUpdateStyle != nil { + if err := config.ValidateUpdateSettings(); err != nil { + ctx.APIError(http.StatusUnprocessableEntity, err) + return err + } + } if *changed || mustInsertPullRequestUnit { units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, @@ -1079,6 +1063,57 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error { log.Trace("Repository %s Mirror[%d] Set EnablePrune: %t", repo.FullName(), mirror.ID, mirror.EnablePrune) } + authUpdateRequested := opts.MirrorPassword != nil || opts.MirrorToken != nil || opts.MirrorUsername != nil + if authUpdateRequested { + remoteURL, err := gitrepo.GitRemoteGetURL(ctx, repo, mirror.GetRemoteName()) + if err != nil { + ctx.APIErrorInternal(err) + return err + } + + authUsername := "" + if opts.MirrorUsername != nil { + authUsername = *opts.MirrorUsername + } else if remoteURL.User != nil { + authUsername = remoteURL.User.Username() + } + + authPassword := "" + authToken := "" + if opts.MirrorPassword != nil { + authPassword = *opts.MirrorPassword + } + if opts.MirrorToken != nil { + authToken = *opts.MirrorToken + } + + if opts.MirrorPassword == nil && opts.MirrorToken == nil && remoteURL.User != nil && (authUsername == "" || authUsername == remoteURL.User.Username()) { + authPassword, _ = remoteURL.User.Password() + } + + if authToken != "" { + authPassword = authToken + } + + composedAddress, err := git.ParseRemoteAddr(repo.OriginalURL, authUsername, authPassword) + if err == nil { + err = migrations.IsMigrateURLAllowed(composedAddress, ctx.Doer) + } + if err != nil { + handleRemoteAddrError(ctx, err) + return err + } + + if err := mirror_service.UpdateAddress(ctx, mirror, composedAddress); err != nil { + ctx.APIErrorInternal(err) + return err + } + + if sanitized, err := util.SanitizeURL(repo.OriginalURL); err == nil { + mirror.RemoteAddress = sanitized + } + } + // finally update the mirror in the DB if err := repo_model.UpdateMirror(ctx, mirror); err != nil { log.Error("Failed to Set Mirror Interval: %s", err) diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index 9e77637282..28bc508879 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -107,15 +107,18 @@ func GetAnnotatedTag(ctx *context.APIContext) { return } - if tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha); err != nil { + tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha) + if err != nil { ctx.APIError(http.StatusBadRequest, err) - } else { - commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name) - if err != nil { - ctx.APIError(http.StatusBadRequest, err) - } - ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit)) + return } + + commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name) + if err != nil { + ctx.APIError(http.StatusBadRequest, err) + return + } + ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit)) } // GetTag get the tag of a repository diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go index 739a9e3892..cb0f026933 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 715e76c355..5aae9d6418 100644 --- a/routers/api/v1/shared/action.go +++ b/routers/api/v1/shared/action.go @@ -11,7 +11,9 @@ import ( "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/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,17 +29,26 @@ 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") } listOptions := utils.GetListOptions(ctx) + orderBy, ok := utils.ResolveSortOrder(ctx, actions_model.JobOrderByMap, actions_model.JobOrderByMap["asc"]["id"]) + if !ok { + return + } opts := actions_model.FindRunJobOptions{ OwnerID: ownerID, RepoID: repoID, RunID: runID, ListOptions: listOptions, + OrderBy: orderBy, + } + if runID > 0 { + opts.RunAttemptID = runAttemptID } for _, status := range ctx.FormStrings("status") { values, err := convertToInternal(status) @@ -57,6 +68,12 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) { res := new(api.ActionWorkflowJobsResponse) res.TotalCount = total + jobList := actions_model.ActionJobList(jobs) + if err := jobList.LoadAttributes(ctx, true); err != nil { + ctx.APIErrorInternal(err) + return + } + res.Entries = make([]*api.ActionWorkflowJob, len(jobs)) isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID @@ -65,11 +82,11 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) { if isRepoLevel { repository = ctx.Repo.Repository } else { - repository, err = repo_model.GetRepositoryByID(ctx, jobs[i].RepoID) - if err != nil { - ctx.APIErrorInternal(err) + if jobs[i].Run == nil || jobs[i].Run.Repo == nil { + ctx.APIErrorInternal(fmt.Errorf("job %d is missing its run or repository", jobs[i].ID)) return } + repository = jobs[i].Run.Repo } convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, repository, nil, jobs[i]) @@ -164,21 +181,28 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) { res := new(api.ActionWorkflowRunsResponse) res.TotalCount = total - res.Entries = make([]*api.ActionWorkflowRun, len(runs)) - isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID - for i := range runs { - var repository *repo_model.Repository - if isRepoLevel { - repository = ctx.Repo.Repository - } else { - repository, err = repo_model.GetRepositoryByID(ctx, runs[i].RepoID) - if err != nil { - ctx.APIErrorInternal(err) - return - } - } + runList := actions_model.RunList(runs) + if err := runList.LoadTriggerUser(ctx); err != nil { + ctx.APIErrorInternal(err) + return + } - convertedRun, err := convert.ToActionWorkflowRun(ctx, repository, runs[i]) + if err := runList.LoadRepos(ctx); err != nil { + ctx.APIErrorInternal(err) + return + } + repos := repo_model.RepositoryList(container.FilterSlice(runs, func(r *actions_model.ActionRun) (*repo_model.Repository, bool) { + return r.Repo, r.Repo != nil + })) + if err := repos.LoadOwners(ctx); err != nil { + ctx.APIErrorInternal(err) + return + } + + res.Entries = make([]*api.ActionWorkflowRun, len(runs)) + for i := range runs { + // TODO: load run attempts in batch + convertedRun, err := convert.ToActionWorkflowRun(ctx, 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 5762c5abf1..dfffe72bf0 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 f66cef61df..1a442d1146 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 573e2e4dd0..82638d2f62 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -429,6 +429,14 @@ func ListWorkflowJobs(ctx *context.APIContext) { // in: query // description: page size of results // type: integer + // - name: sort + // in: query + // description: sort jobs by attribute. Supported values are "id". Default is "id" + // type: string + // - name: order + // in: query + // description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc" + // type: string // produces: // - application/json // responses: @@ -438,6 +446,8 @@ func ListWorkflowJobs(ctx *context.APIContext) { // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" - 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 6f1053e7ac..474680adec 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 9c7bd57bc0..8f39319112 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 9ec4d2c938..39ded31bf4 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/repo.go b/routers/api/v1/user/repo.go index e24a7543a1..a664888dbf 100644 --- a/routers/api/v1/user/repo.go +++ b/routers/api/v1/user/repo.go @@ -37,7 +37,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) { apiRepos := make([]*api.Repository, 0, len(repos)) for i := range repos { - permission, err := access_model.GetUserRepoPermission(ctx, repos[i], ctx.Doer) + permission, err := access_model.GetDoerRepoPermission(ctx, repos[i], ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return @@ -123,7 +123,7 @@ func ListMyRepos(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } - permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/user/runners.go b/routers/api/v1/user/runners.go index 667bdb36fe..6db1d48069 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: @@ -40,7 +40,7 @@ func ListRunners(ctx *context.APIContext) { // required: false // responses: // "200": - // "$ref": "#/definitions/ActionRunnersResponse" + // "$ref": "#/responses/RunnerList" // "400": // "$ref": "#/responses/error" // "404": @@ -63,7 +63,7 @@ func GetRunner(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": @@ -115,7 +115,7 @@ func UpdateRunner(ctx *context.APIContext) { // "$ref": "#/definitions/EditActionRunnerOption" // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index 4464d53936..50a54b2683 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -31,7 +31,7 @@ func getStarredRepos(ctx *context.APIContext, user *user_model.User, private boo repos := make([]*api.Repository, len(starredRepos)) for i, starred := range starredRepos { - permission, err := access_model.GetUserRepoPermission(ctx, starred, user) + permission, err := access_model.GetIndividualUserRepoPermission(ctx, starred, user) if err != nil { return nil, err } diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index 751b0ae3bc..9c11d5ca35 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -29,7 +29,7 @@ func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private boo repos := make([]*api.Repository, len(watchedRepos)) for i, watched := range watchedRepos { - permission, err := access_model.GetUserRepoPermission(ctx, watched, user) + permission, err := access_model.GetIndividualUserRepoPermission(ctx, watched, user) if err != nil { return nil, 0, err } diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index c4f21eac8b..8dc19b63a8 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 { @@ -215,6 +215,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI w := &webhook.Webhook{ OwnerID: ownerID, RepoID: repoID, + Name: strings.TrimSpace(form.Name), URL: form.Config["url"], ContentType: webhook.ToHookContentType(form.Config["content_type"]), Secret: form.Config["secret"], @@ -293,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 { @@ -392,6 +393,10 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh w.IsActive = *form.Active } + if form.Name != nil { + w.Name = strings.TrimSpace(*form.Name) + } + if err := webhook.UpdateWebhook(ctx, w); err != nil { ctx.APIErrorInternal(err) return false diff --git a/routers/api/v1/utils/sort.go b/routers/api/v1/utils/sort.go new file mode 100644 index 0000000000..4e4cd10915 --- /dev/null +++ b/routers/api/v1/utils/sort.go @@ -0,0 +1,38 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "fmt" + "net/http" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/services/context" +) + +// ResolveSortOrder reads "sort" and "order" query params and returns the matching +// SearchOrderBy from orderByMap. When "sort" is absent, returns defaultOrder. +// On invalid input it writes a 422 response and returns ok=false; callers should +// then return immediately. +func ResolveSortOrder(ctx *context.APIContext, orderByMap map[string]map[string]db.SearchOrderBy, defaultOrder db.SearchOrderBy) (db.SearchOrderBy, bool) { + sortMode := ctx.FormString("sort") + if sortMode == "" { + return defaultOrder, true + } + sortOrder := ctx.FormString("order") + if sortOrder == "" { + sortOrder = "asc" + } + orderMap, ok := orderByMap[sortOrder] + if !ok { + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: %q", sortOrder)) + return "", false + } + orderBy, ok := orderMap[sortMode] + if !ok { + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: %q", sortMode)) + return "", false + } + return orderBy, true +} diff --git a/routers/api/v1/utils/sort_test.go b/routers/api/v1/utils/sort_test.go new file mode 100644 index 0000000000..b0dc81e50f --- /dev/null +++ b/routers/api/v1/utils/sort_test.go @@ -0,0 +1,44 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/services/contexttest" + + "github.com/stretchr/testify/assert" +) + +func TestResolveSortOrder(t *testing.T) { + m := map[string]map[string]db.SearchOrderBy{ + "asc": {"id": "id ASC"}, + "desc": {"id": "id DESC"}, + } + defaultOrder := db.SearchOrderBy("default") + + cases := []struct { + path string + wantOK bool + wantOrder db.SearchOrderBy + wantStatus int + }{ + {"GET /", true, defaultOrder, 0}, + {"GET /?sort=id", true, "id ASC", 0}, + {"GET /?sort=id&order=desc", true, "id DESC", 0}, + {"GET /?sort=bogus", false, "", http.StatusUnprocessableEntity}, + {"GET /?sort=id&order=bogus", false, "", http.StatusUnprocessableEntity}, + } + for _, tc := range cases { + t.Run(tc.path, func(t *testing.T) { + ctx, _ := contexttest.MockAPIContext(t, tc.path) + got, ok := ResolveSortOrder(ctx, m, defaultOrder) + assert.Equal(t, tc.wantOK, ok) + assert.Equal(t, tc.wantOrder, got) + assert.Equal(t, tc.wantStatus, ctx.Resp.WrittenStatus()) + }) + } +} diff --git a/routers/common/actions.go b/routers/common/actions.go index 39d2111f5a..2b83e5d842 100644 --- a/routers/common/actions.go +++ b/routers/common/actions.go @@ -10,6 +10,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -30,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") } @@ -38,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) } @@ -57,12 +59,11 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository if p := strings.Index(workflowName, "."); p > 0 { workflowName = workflowName[0:p] } - ctx.ServeContent(reader, &context.ServeHeaderOptions{ + ctx.ServeContent(reader, context.ServeHeaderOptions{ Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, curJob.Name, task.ID), ContentLength: &task.LogSize, - ContentType: "text/plain", - ContentTypeCharset: "utf-8", - Disposition: "attachment", + ContentType: "text/plain; charset=utf-8", + ContentDisposition: httplib.ContentDispositionAttachment, }) return nil } diff --git a/routers/common/blockexpensive.go b/routers/common/blockexpensive.go index fec364351c..0407264b1e 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 db5c0db7dd..6ee4af60e8 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/compare.go b/routers/common/compare.go index 7e917c4df8..8b1e1715d2 100644 --- a/routers/common/compare.go +++ b/routers/common/compare.go @@ -129,7 +129,7 @@ func GetHeadOwnerAndRepo(ctx context.Context, baseRepo *repo_model.Repository, c if compareReq.HeadOwner == baseRepo.Owner.Name { headOwner = baseRepo.Owner } else { - headOwner, err = user_model.GetUserOrOrgByName(ctx, compareReq.HeadOwner) + headOwner, err = user_model.GetUserByName(ctx, compareReq.HeadOwner) if err != nil { return nil, nil, err } diff --git a/routers/common/errpage.go b/routers/common/errpage.go index 07760bcd18..9baf7915e1 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/markup.go b/routers/common/markup.go index 56b588332f..05f48c7902 100644 --- a/routers/common/markup.go +++ b/routers/common/markup.go @@ -26,6 +26,8 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur // filePath is the path of the file to render if the end user is trying to preview a repo file (mode == "file") // filePath will be used as RenderContext.RelativePath + // TODO: MARKUP-RENDER-CONTEXT: this logic is unnecessarily complicated. + // Ideally: the "file path" should not appear in the "url path context", but it needs a lot of refactoring to achieve that // for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md" // and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc" @@ -69,7 +71,7 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur case "gfm": // legacy mode rctx = renderhelper.NewRenderContextRepoFile(ctx, repoModel, renderhelper.RepoFileOptions{ DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName, - CurrentRefPath: refPath, CurrentTreePath: treePath, + CurrentRefSubURL: refPath, CurrentTreePath: treePath, }) rctx = rctx.WithMarkupType(markdown.MarkupName) case "comment": @@ -85,7 +87,7 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur case "file": rctx = renderhelper.NewRenderContextRepoFile(ctx, repoModel, renderhelper.RepoFileOptions{ DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName, - CurrentRefPath: refPath, CurrentTreePath: treePath, + CurrentRefSubURL: refPath, CurrentTreePath: treePath, }) rctx = rctx.WithMarkupType("").WithRelativePath(filePath) // render the repo file content by its extension default: diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 9daffb04f1..d1802c6e35 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/routing" @@ -27,22 +28,40 @@ 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)) } - if setting.IsRouteLogEnabled() { - handlers = append(handlers, routing.NewLoggerHandler()) - } + handlers = append(handlers, routing.NewRequestInfoHandler()) if setting.IsAccessLogEnabled() { handlers = append(handlers, context.AccessLogger()) } + if !setting.IsProd { + handlers = append(handlers, public.ViteDevMiddleware) + } + 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 96f23b64fe..fbde419223 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/common/redirect.go b/routers/common/redirect.go index d64f74ec82..e9f14a6eb1 100644 --- a/routers/common/redirect.go +++ b/routers/common/redirect.go @@ -16,11 +16,12 @@ func FetchRedirectDelegate(resp http.ResponseWriter, req *http.Request) { // 2. when use "window.reload()", the hash is not respected, the newly loaded page won't scroll to the hash target. // The typical page is "issue comment" page. The backend responds "/owner/repo/issues/1#comment-2", // then frontend needs this delegate to redirect to the new location with hash correctly. - redirect := req.PostFormValue("redirect") - if !httplib.IsCurrentGiteaSiteURL(req.Context(), redirect) { - resp.WriteHeader(http.StatusBadRequest) + redirect := req.FormValue("redirect") + if req.Method != http.MethodPost || !httplib.IsCurrentGiteaSiteURL(req.Context(), redirect) { + http.Error(resp, "Bad Request", http.StatusBadRequest) return } - resp.Header().Add("Location", redirect) + // no OpenRedirect, the "redirect" is validated by "IsCurrentGiteaSiteURL" above + resp.Header().Set("Location", redirect) resp.WriteHeader(http.StatusSeeOther) } diff --git a/routers/common/redirect_test.go b/routers/common/redirect_test.go new file mode 100644 index 0000000000..c4e6d2e6c7 --- /dev/null +++ b/routers/common/redirect_test.go @@ -0,0 +1,48 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestFetchRedirectDelegate(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://gitea/")() + + cases := []struct { + method string + input string + status int + }{ + {method: "POST", input: "/foo?k=v", status: http.StatusSeeOther}, + {method: "GET", input: "/foo?k=v", status: http.StatusBadRequest}, + {method: "POST", input: `\/foo?k=v`, status: http.StatusBadRequest}, + {method: "POST", input: `\\/foo?k=v`, status: http.StatusBadRequest}, + {method: "POST", input: "https://gitea/xxx", status: http.StatusSeeOther}, + {method: "POST", input: "https://other/xxx", status: http.StatusBadRequest}, + } + for _, c := range cases { + t.Run(c.method+" "+c.input, func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest(c.method, "/?redirect="+url.QueryEscape(c.input), nil) + FetchRedirectDelegate(resp, req) + assert.Equal(t, c.status, resp.Code) + if c.status == http.StatusSeeOther { + assert.Equal(t, c.input, resp.Header().Get("Location")) + } else { + assert.Empty(t, resp.Header().Get("Location")) + assert.Equal(t, "Bad Request", strings.TrimSpace(resp.Body.String())) + } + }) + } +} diff --git a/routers/common/serve.go b/routers/common/serve.go index 4bb1a48b0d..9232d90c94 100644 --- a/routers/common/serve.go +++ b/routers/common/serve.go @@ -4,7 +4,6 @@ package common import ( - "io" "path" "time" @@ -12,7 +11,6 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/context" @@ -24,29 +22,24 @@ func ServeBlob(ctx *context.Base, repo *repo_model.Repository, filePath string, return nil } + if err := repo.LoadOwner(ctx); err != nil { + return err + } + dataRc, err := blob.DataAsync() if err != nil { return err } - defer func() { - if err = dataRc.Close(); err != nil { - log.Error("ServeBlob: Close: %v", err) - } - }() + defer dataRc.Close() - _ = repo.LoadOwner(ctx) - httplib.ServeContentByReader(ctx.Req, ctx.Resp, blob.Size(), dataRc, &httplib.ServeHeaderOptions{ + if lastModified == nil { + lastModified = new(time.Time) + } + httplib.ServeUserContentByReader(ctx.Req, ctx.Resp, blob.Size(), dataRc, httplib.ServeHeaderOptions{ Filename: path.Base(filePath), - CacheIsPublic: !repo.IsPrivate && repo.Owner != nil && repo.Owner.Visibility == structs.VisibleTypePublic, + CacheIsPublic: !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic, CacheDuration: setting.StaticCacheTime, + LastModified: *lastModified, }) return nil } - -func ServeContentByReader(ctx *context.Base, filePath string, size int64, reader io.Reader) { - httplib.ServeContentByReader(ctx.Req, ctx.Resp, size, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)}) -} - -func ServeContentByReadSeeker(ctx *context.Base, filePath string, modTime *time.Time, reader io.ReadSeeker) { - httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, modTime, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)}) -} diff --git a/routers/init.go b/routers/init.go index 2ed7a57e5c..e04b711c4d 100644 --- a/routers/init.go +++ b/routers/init.go @@ -47,6 +47,7 @@ import ( repo_migrations "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" "code.gitea.io/gitea/services/oauth2_provider" + packages_spec "code.gitea.io/gitea/services/packages/pkgspec" pull_service "code.gitea.io/gitea/services/pull" release_service "code.gitea.io/gitea/services/release" repo_service "code.gitea.io/gitea/services/repository" @@ -133,12 +134,6 @@ func InitWebInstalled(ctx context.Context) { external.RegisterRenderers() markup.Init(markup_service.FormalRenderHelperFuncs()) - if setting.EnableSQLite3 { - log.Info("SQLite3 support is enabled") - } else if setting.Database.Type.IsSQLite3() { - log.Fatal("SQLite3 support is disabled, but it is used for database setting. Please get or build a Gitea release with SQLite3 support.") - } - mustInitCtx(ctx, common.InitDBEngine) log.Info("ORM engine initialization successful!") mustInit(system.Init) @@ -149,6 +144,7 @@ func InitWebInstalled(ctx context.Context) { mustInitCtx(ctx, models.Init) mustInitCtx(ctx, authmodel.Init) mustInitCtx(ctx, repo_service.Init) + mustInit(packages_spec.InitManager) // Booting long running goroutines. mustInit(indexer_service.Init) diff --git a/routers/install/install.go b/routers/install/install.go index 81fcdfa384..3b21af6b03 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -26,7 +26,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/user" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/common" @@ -77,7 +76,7 @@ func Install(ctx *context.Context) { form.DbSchema = setting.Database.Schema form.SSLMode = setting.Database.SSLMode - curDBType := setting.Database.Type.String() + curDBType := string(setting.Database.Type) if !slices.Contains(setting.SupportedDatabaseTypes, curDBType) { curDBType = "mysql" } @@ -87,15 +86,7 @@ func Install(ctx *context.Context) { form.AppName = setting.AppName form.RepoRootPath = setting.RepoRootPath form.LFSRootPath = setting.LFS.Storage.Path - - // Note(unknown): it's hard for Windows users change a running user, - // so just use current one if config says default. - if setting.IsWindows && setting.RunUser == "git" { - form.RunUser = user.CurrentUsername() - } else { - form.RunUser = setting.RunUser - } - + form.RunUser = setting.RunUser form.Domain = setting.Domain form.SSHPort = setting.SSH.Port form.HTTPPort = setting.HTTPPort @@ -132,7 +123,7 @@ func Install(ctx *context.Context) { func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { var err error - if (setting.Database.Type == "sqlite3") && + if (setting.Database.Type == setting.DatabaseTypeSQLite3) && len(setting.Database.Path) == 0 { ctx.Data["Err_DbPath"] = true ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_db_path"), tplInstall, form) @@ -144,13 +135,8 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { defer db.UnsetDefaultEngine() if err = db.InitEngine(ctx); err != nil { - if strings.Contains(err.Error(), `Unknown database type: sqlite3`) { - ctx.Data["Err_DbType"] = true - ctx.RenderWithErrDeprecated(ctx.Tr("install.sqlite3_not_available", "https://docs.gitea.com/installation/install-from-binary"), tplInstall, form) - } else { - ctx.Data["Err_DbSetting"] = true - ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) - } + ctx.Data["Err_DbSetting"] = true + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) return false } @@ -272,13 +258,6 @@ func SubmitInstall(ctx *context.Context) { return } - currentUser, match := setting.IsRunUserMatchCurrentUser(form.RunUser) - if !match { - ctx.Data["Err_RunUser"] = true - ctx.RenderWithErrDeprecated(ctx.Tr("install.run_user_not_match", form.RunUser, currentUser), tplInstall, &form) - return - } - // Check logic loophole between disable self-registration and no admin account. if form.DisableRegistration && len(form.AdminName) == 0 { ctx.Data["Err_Services"] = true @@ -344,7 +323,7 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath) cfg.Section("").Key("RUN_MODE").SetValue("prod") - cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type.String()) + cfg.Section("database").Key("DB_TYPE").SetValue(string(setting.Database.Type)) cfg.Section("database").Key("HOST").SetValue(setting.Database.Host) cfg.Section("database").Key("NAME").SetValue(setting.Database.Name) cfg.Section("database").Key("USER").SetValue(setting.Database.User) @@ -371,12 +350,11 @@ func SubmitInstall(ctx *context.Context) { if form.LFSRootPath != "" { cfg.Section("server").Key("LFS_START_SERVER").SetValue("true") cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath) - var lfsJwtSecret string - if _, lfsJwtSecret, err = generate.NewJwtSecretWithBase64(); err != nil { - ctx.RenderWithErrDeprecated(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form) - return + + if !cfg.Section("server").HasKey("LFS_JWT_SECRET_URI") { + _, lfsJwtSecret := generate.NewJwtSecretWithBase64() + cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(lfsJwtSecret) } - cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(lfsJwtSecret) } else { cfg.Section("server").Key("LFS_START_SERVER").SetValue("false") } @@ -437,11 +415,7 @@ func SubmitInstall(ctx *context.Context) { // FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET" // see the "loadOAuth2From" in "setting/oauth2.go" if !cfg.Section("oauth2").HasKey("JWT_SECRET") && !cfg.Section("oauth2").HasKey("JWT_SECRET_URI") { - _, jwtSecretBase64, err := generate.NewJwtSecretWithBase64() - if err != nil { - ctx.RenderWithErrDeprecated(ctx.Tr("install.secret_key_failed", err), tplInstall, &form) - return - } + _, jwtSecretBase64 := generate.NewJwtSecretWithBase64() cfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64) } diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index b595a95b23..3d070a18ab 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -192,7 +192,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { }) return } - perm, err := access_model.GetUserRepoPermission(ctx, repo, pusher) + perm, err := access_model.GetDoerRepoPermission(ctx, repo, pusher) if err != nil { log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err) ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index 2dbf072f3a..deaeadb5ef 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -371,8 +371,8 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r return } - // If we're an admin for the repository we can ignore status checks, reviews and override protected files - if ctx.userPerm.IsAdmin() { + // If we can bypass branch protection we can ignore status checks, reviews and protected files + if git_model.CanBypassBranchProtection(ctx, protectBranch, ctx.user, ctx.userPerm.IsAdmin()) { return } @@ -525,7 +525,7 @@ func (ctx *preReceiveContext) loadPusherAndPermission() bool { return false } ctx.user = user - userPerm, err := access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, user) + userPerm, err := access_model.GetDoerRepoPermission(ctx, ctx.Repo.Repository, user) if err != nil { log.Error("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err) ctx.JSON(http.StatusInternalServerError, private.Response{ diff --git a/routers/private/serv.go b/routers/private/serv.go index b752556c23..7c6eb907fa 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -338,7 +338,7 @@ func ServCommand(ctx *context.PrivateContext) { mode = perm.AccessModeRead } - perm, err := access_model.GetUserRepoPermission(ctx, repo, user) + perm, err := access_model.GetDoerRepoPermission(ctx, repo, user) if err != nil { log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err) ctx.JSON(http.StatusInternalServerError, private.Response{ diff --git a/routers/utils/utils.go b/routers/utils/utils.go index 3035073d5c..47ca13b2aa 100644 --- a/routers/utils/utils.go +++ b/routers/utils/utils.go @@ -5,10 +5,11 @@ package utils import ( "html" - "strings" + "html/template" ) -// SanitizeFlashErrorString will sanitize a flash error string -func SanitizeFlashErrorString(x string) string { - return strings.ReplaceAll(html.EscapeString(x), "\n", "
") +// EscapeFlashErrorString will escape the flash error string +// Maybe do more sanitization in the future, e.g.: hide sensitive information, etc. +func EscapeFlashErrorString(x string) template.HTML { + return template.HTML(html.EscapeString(x)) } diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go index cc7c888a75..5dfc544777 100644 --- a/routers/utils/utils_test.go +++ b/routers/utils/utils_test.go @@ -4,16 +4,17 @@ package utils import ( + "html/template" "testing" "github.com/stretchr/testify/assert" ) -func TestSanitizeFlashErrorString(t *testing.T) { +func TestEscapeFlashErrorString(t *testing.T) { tests := []struct { name string arg string - want string + want template.HTML }{ { name: "no error", @@ -28,13 +29,13 @@ func TestSanitizeFlashErrorString(t *testing.T) { { name: "line break error", arg: "some error:\n\nawesome!", - want: "some error:

awesome!", + want: "some error:\n\nawesome!", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := SanitizeFlashErrorString(tt.arg) + got := EscapeFlashErrorString(tt.arg) assert.Equal(t, tt.want, got) }) } diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go index ecdd462f9e..929faa1968 100644 --- a/routers/web/admin/admin_test.go +++ b/routers/web/admin/admin_test.go @@ -16,64 +16,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestShadowPassword(t *testing.T) { - kases := []struct { - Provider string - CfgItem string - Result string - }{ - { - Provider: "redis", - CfgItem: "network=tcp,addr=:6379,password=gitea,db=0,pool_size=100,idle_timeout=180", - Result: "network=tcp,addr=:6379,password=******,db=0,pool_size=100,idle_timeout=180", - }, - { - Provider: "mysql", - CfgItem: "root:@tcp(localhost:3306)/gitea?charset=utf8", - Result: "root:******@tcp(localhost:3306)/gitea?charset=utf8", - }, - { - Provider: "mysql", - CfgItem: "/gitea?charset=utf8", - Result: "/gitea?charset=utf8", - }, - { - Provider: "mysql", - CfgItem: "user:mypassword@/dbname", - Result: "user:******@/dbname", - }, - { - Provider: "postgres", - CfgItem: "user=pqgotest dbname=pqgotest sslmode=verify-full", - Result: "user=pqgotest dbname=pqgotest sslmode=verify-full", - }, - { - Provider: "postgres", - CfgItem: "user=pqgotest password= dbname=pqgotest sslmode=verify-full", - Result: "user=pqgotest password=****** dbname=pqgotest sslmode=verify-full", - }, - { - Provider: "postgres", - CfgItem: "postgres://user:pass@hostname/dbname", - Result: "postgres://user:******@hostname/dbname", - }, - { - Provider: "couchbase", - CfgItem: "http://dev-couchbase.example.com:8091/", - Result: "http://dev-couchbase.example.com:8091/", - }, - { - Provider: "couchbase", - CfgItem: "http://user:the_password@dev-couchbase.example.com:8091/", - Result: "http://user:******@dev-couchbase.example.com:8091/", - }, - } - - for _, k := range kases { - assert.Equal(t, k.Result, shadowPassword(k.Provider, k.CfgItem)) - } -} - func TestSelfCheckPost(t *testing.T) { defer test.MockVariableValue(&setting.PublicURLDetection)() defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")() diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index e29aca127c..c718eb34e0 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -205,6 +205,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source { SSHPublicKeyClaimName: form.Oauth2SSHPublicKeyClaimName, FullNameClaimName: form.Oauth2FullNameClaimName, + ExternalIDClaim: form.OpenIDConnectExternalIDClaim, } } @@ -439,7 +440,7 @@ func EditAuthSourcePost(ctx *context.Context) { log.Trace("Authentication changed by admin(%s): %d", ctx.Doer.Name, source.ID) ctx.Flash.Success(ctx.Tr("admin.auths.update_success")) - ctx.Redirect(setting.AppSubURL + "/-/admin/auths/" + strconv.FormatInt(form.ID, 10)) + ctx.Redirect(setting.AppSubURL + "/-/admin/auths/" + strconv.FormatInt(source.ID, 10)) } // DeleteAuthSource response for deleting an auth source diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index a449796ec1..03a15b6713 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -7,8 +7,6 @@ package admin import ( "errors" "net/http" - "net/url" - "strings" system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/cache" @@ -59,63 +57,6 @@ func TestCache(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/-/admin/config") } -func shadowPasswordKV(cfgItem, splitter string) string { - fields := strings.Split(cfgItem, splitter) - for i := range fields { - if strings.HasPrefix(fields[i], "password=") { - fields[i] = "password=******" - break - } - } - return strings.Join(fields, splitter) -} - -func shadowURL(provider, cfgItem string) string { - u, err := url.Parse(cfgItem) - if err != nil { - log.Error("Shadowing Password for %v failed: %v", provider, err) - return cfgItem - } - if u.User != nil { - atIdx := strings.Index(cfgItem, "@") - if atIdx > 0 { - colonIdx := strings.LastIndex(cfgItem[:atIdx], ":") - if colonIdx > 0 { - return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] - } - } - } - return cfgItem -} - -func shadowPassword(provider, cfgItem string) string { - switch provider { - case "redis": - return shadowPasswordKV(cfgItem, ",") - case "mysql": - // root:@tcp(localhost:3306)/macaron?charset=utf8 - atIdx := strings.Index(cfgItem, "@") - if atIdx > 0 { - colonIdx := strings.Index(cfgItem[:atIdx], ":") - if colonIdx > 0 { - return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] - } - } - return cfgItem - case "postgres": - // user=jiahuachen dbname=macaron port=5432 sslmode=disable - if !strings.HasPrefix(cfgItem, "postgres://") { - return shadowPasswordKV(cfgItem, " ") - } - fallthrough - case "couchbase": - return shadowURL(provider, cfgItem) - // postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full - // Notice: use shadowURL - } - return cfgItem -} - // Config show admin config page func Config(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.config_summary") @@ -123,9 +64,7 @@ func Config(ctx *context.Context) { ctx.Data["PageIsAdminConfigSummary"] = true ctx.Data["CustomConf"] = setting.CustomConf - ctx.Data["AppUrl"] = setting.AppURL ctx.Data["AppBuiltWith"] = setting.AppBuiltWith - ctx.Data["Domain"] = setting.Domain ctx.Data["RunUser"] = setting.RunUser ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode) ctx.Data["GitVersion"] = git.DefaultFeatures().VersionInfo() @@ -152,8 +91,6 @@ func Config(ctx *context.Context) { ctx.Data["CacheAdapter"] = setting.CacheService.Adapter ctx.Data["CacheInterval"] = setting.CacheService.Interval - - ctx.Data["CacheConn"] = shadowPassword(setting.CacheService.Adapter, setting.CacheService.Conn) ctx.Data["CacheItemTTL"] = setting.CacheService.TTL sessionCfg := setting.SessionConfig @@ -171,7 +108,7 @@ func Config(ctx *context.Context) { sessionCfg.Secure = realSession.Secure sessionCfg.Domain = realSession.Domain } - sessionCfg.ProviderConfig = shadowPassword(sessionCfg.Provider, sessionCfg.ProviderConfig) + sessionCfg.ProviderConfig = "" ctx.Data["SessionConfig"] = sessionCfg ctx.Data["Git"] = setting.Git diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go index 5395529d66..205ab2f8ea 100644 --- a/routers/web/admin/diagnosis.go +++ b/routers/web/admin/diagnosis.go @@ -18,10 +18,10 @@ import ( func MonitorDiagnosis(ctx *context.Context) { seconds := min(max(ctx.FormInt64("seconds"), 1), 300) - httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{ - ContentType: "application/zip", - Disposition: "attachment", - Filename: fmt.Sprintf("gitea-diagnosis-%s.zip", time.Now().Format("20060102-150405")), + httplib.ServeSetHeaders(ctx.Resp, httplib.ServeHeaderOptions{ + ContentType: "application/zip", + Filename: fmt.Sprintf("gitea-diagnosis-%s.zip", time.Now().Format("20060102-150405")), + ContentDisposition: httplib.ContentDispositionAttachment, }) zipWriter := zip.NewWriter(ctx.Resp) diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index a0f983914d..f71a55a219 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -93,7 +93,7 @@ func DeletePackageVersion(ctx *context.Context) { return } - ctx.Flash.Success(ctx.Tr("packages.settings.delete.success")) + ctx.Flash.Success(ctx.Tr("packages.settings.delete.version.success")) ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages?page=" + url.QueryEscape(ctx.FormString("page")) + "&q=" + url.QueryEscape(ctx.FormString("q")) + "&type=" + url.QueryEscape(ctx.FormString("type"))) } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 1219690200..0503bd02f8 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -64,7 +64,7 @@ func prepareCommonAuthPageData(ctx *context.Context, opt CommonAuthOptions) { ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey - ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL + ctx.Data["McaptchaURL"] = strings.TrimSuffix(setting.Service.McaptchaURL, "/") ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey if setting.Service.CaptchaType == setting.ImageCaptcha { ctx.Data["Captcha"] = context.GetImageCaptcha() @@ -218,18 +218,50 @@ func performAutoLogin(ctx *context.Context) bool { return false } -func prepareSignInPageData(ctx *context.Context) { +func performAutoLoginOAuth2(ctx *context.Context, data *preparedSignInData) bool { + // If only 1 OAuth provider is present and other login methods are disabled, redirect to the OAuth provider. + onlySingleOAuth2 := len(data.oauth2Providers) == 1 && + !setting.Service.EnablePasswordSignInForm && + !setting.Service.EnableOpenIDSignIn && + !setting.Service.EnablePasskeyAuth && + !data.enableSSPI + + if !onlySingleOAuth2 { + return false + } + + skipToOAuthURL := setting.AppSubURL + "/user/oauth2/" + url.PathEscape(data.oauth2Providers[0].DisplayName()) + if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" { + skipToOAuthURL += "?redirect_to=" + url.QueryEscape(redirectTo) + } + ctx.Redirect(skipToOAuthURL) + return true +} + +type preparedSignInData struct { + oauth2Providers []oauth2.Provider + enableSSPI bool +} + +func prepareSignInPageData(ctx *context.Context) (ret preparedSignInData) { + var err error + ret.enableSSPI = auth.IsSSPIEnabled(ctx) + ret.oauth2Providers, err = oauth2.GetOAuth2Providers(ctx, optional.Some(true)) + if err != nil { + log.Error("Failed to get OAuth2 providers: %v", err) + } ctx.Data["Title"] = ctx.Tr("sign_in") - ctx.Data["OAuth2Providers"], _ = oauth2.GetOAuth2Providers(ctx, optional.Some(true)) + ctx.Data["OAuth2Providers"] = ret.oauth2Providers ctx.Data["Title"] = ctx.Tr("sign_in") ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login" ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsLogin"] = true - ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx) + ctx.Data["EnableSSPI"] = ret.enableSSPI prepareCommonAuthPageData(ctx, CommonAuthOptions{ EnableCaptcha: setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin, }) + return ret } // SignIn render sign in page @@ -241,7 +273,10 @@ func SignIn(ctx *context.Context) { redirectAfterAuth(ctx) return } - prepareSignInPageData(ctx) + data := prepareSignInPageData(ctx) + if performAutoLoginOAuth2(ctx, &data) { + return + } ctx.HTML(http.StatusOK, tplSignIn) } @@ -279,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) } @@ -458,12 +484,17 @@ func SignOut(ctx *context.Context) { } func buildSignOutRedirectURL(ctx *context.Context) string { - // TODO: can also support REVERSE_PROXY_AUTHENTICATION logout URL in the future if ctx.Doer != nil && ctx.Doer.LoginType == auth.OAuth2 { if s := buildOIDCEndSessionURL(ctx, ctx.Doer); s != "" { return s } } + + // The assumption is: if reverse proxy auth is enabled, then the users should only sign-in via reverse proxy auth. + // TODO: in the future, if we need to distinguish different sign-in methods, we need to save the sign-in method in session and check here + if setting.Service.EnableReverseProxyAuth && setting.ReverseProxyLogoutRedirect != "" { + return setting.ReverseProxyLogoutRedirect + } return setting.AppSubURL + "/" } @@ -471,6 +502,7 @@ func prepareSignUpPageData(ctx *context.Context) bool { ctx.Data["Title"] = ctx.Tr("sign_up") ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up" ctx.Data["PageIsSignUp"] = true + ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx) hasUsers, err := user_model.HasUsers(ctx) if err != nil { @@ -597,16 +629,15 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, case setting.OAuth2AccountLinkingAuto: var user *user_model.User user = &user_model.User{Name: u.Name} - hasUser, err := user_model.GetUser(ctx, user) + hasUser, err := user_model.GetIndividualUser(ctx, user) if !hasUser || err != nil { user = &user_model.User{Email: u.Email} - hasUser, err = user_model.GetUser(ctx, user) + hasUser, err = user_model.GetIndividualUser(ctx, user) if !hasUser || err != nil { ctx.ServerError("UserLinkAccount", err) return false } } - // TODO: probably we should respect 'remember' user's choice... oauth2LinkAccount(ctx, user, possibleLinkAccountData, true) return false // user is already created here, all redirects are handled diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go index d1f808181a..a06e209e4f 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,19 +84,50 @@ 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)) assert.Contains(t, ctx.Flash.ErrorMsg, "auth.oauth.signin.error.general") }) + t.Run("RedirectSingleProvider", func(t *testing.T) { + enablePassword := &setting.Service.EnablePasswordSignInForm + enableOpenID := &setting.Service.EnableOpenIDSignIn + enablePasskey := &setting.Service.EnablePasskeyAuth + defer test.MockVariableValue(enablePassword, false)() + defer test.MockVariableValue(enableOpenID, false)() + defer test.MockVariableValue(enablePasskey, false)() + + testSignIn := func(t *testing.T, link string, expectedCode int, expectedRedirect string) { + ctx, resp := contexttest.MockContext(t, link) + SignIn(ctx) + assert.Equal(t, expectedCode, resp.Code) + if expectedCode == http.StatusSeeOther { + assert.Equal(t, expectedRedirect, test.RedirectURL(resp)) + } + } + 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, "") + *enablePassword, *enableOpenID, *enablePasskey = false, true, false + testSignIn(t, "/user/login", http.StatusOK, "") + *enablePassword, *enableOpenID, *enablePasskey = false, false, true + testSignIn(t, "/user/login", http.StatusOK, "") + + *enablePassword, *enableOpenID, *enablePasskey = false, false, false + addOAuth2Source(t, "dummy-auth-source-2", oauth2.Source{}) + testSignIn(t, "/user/login", http.StatusOK, "") + }) + t.Run("OIDCLogout", func(t *testing.T) { var mockServer *httptest.Server mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index 02e1b7acd2..ad44c5c671 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) } @@ -263,6 +253,11 @@ func LinkAccountPostRegister(ctx *context.Context) { return } + oauth2SignInSync(ctx, linkAccountData.AuthSourceID, u, linkAccountData.GothUser) + if ctx.Written() { + return + } + authSource, err := auth.GetSourceByID(ctx, linkAccountData.AuthSourceID) if err != nil { ctx.ServerError("GetSourceByID", err) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 3b1744669d..867106d352 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -13,6 +13,7 @@ import ( "net/url" "sort" "strings" + "time" "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" @@ -79,7 +80,7 @@ func SignInOAuthCallback(ctx *context.Context) { } } sort.Strings(errorKeyValues) - ctx.Flash.Error(strings.Join(errorKeyValues, "
"), true) + ctx.Flash.Error(strings.Join(errorKeyValues, "\n"), true) } // first look if the provider is still active @@ -301,21 +302,42 @@ func showLinkingLogin(ctx *context.Context, authSourceID int64, gothUser goth.Us ctx.Redirect(setting.AppSubURL + "/user/link_account") } -func oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) { - if setting.OAuth2Client.UpdateAvatar && len(url) > 0 { - resp, err := http.Get(url) - if err == nil { - defer func() { - _ = resp.Body.Close() - }() - } - // ignore any error - if err == nil && resp.StatusCode == http.StatusOK { - data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1)) - if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize { - _ = user_service.UploadAvatar(ctx, u, data) - } - } +var oauth2AvatarHTTPClient = &http.Client{Timeout: 30 * time.Second} + +func oauth2UpdateAvatarIfNeed(ctx *context.Context, avatarURL string, u *user_model.User) { + if !setting.OAuth2Client.UpdateAvatar || len(avatarURL) == 0 { + return + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, avatarURL, nil) + if err != nil { + log.Warn("invalid avatar URL %q: %v", avatarURL, err) + return + } + // Some hosts (e.g. Wikimedia) reject Go's default User-Agent. + req.Header.Set("User-Agent", "Gitea "+setting.AppVer) + + resp, err := oauth2AvatarHTTPClient.Do(req) + if err != nil { + log.Warn("fetch %q failed: %v", avatarURL, err) + return + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + log.Warn("fetch %q returned status %d", avatarURL, resp.StatusCode) + return + } + data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1)) + if err != nil { + log.Warn("read body from %q failed: %v", avatarURL, err) + return + } + if int64(len(data)) > setting.Avatar.MaxFileSize { + log.Warn("avatar from %q exceeds max size %d", avatarURL, setting.Avatar.MaxFileSize) + return + } + if err := user_service.UploadAvatar(ctx, u, data); err != nil { + log.Warn("UploadAvatar for user %q failed: %v", u.Name, err) } } @@ -482,7 +504,7 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ LoginSource: authSource.ID, } - hasUser, err := user_model.GetUser(ctx, user) + hasUser, err := user_model.GetIndividualUser(ctx, user) if err != nil { return nil, goth.User{}, err } @@ -539,7 +561,15 @@ func buildOIDCEndSessionURL(ctx *context.Context, doer *user_model.User) string // https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout params := endSessionURL.Query() params.Set("client_id", oauth2Cfg.ClientID) - params.Set("post_logout_redirect_uri", httplib.GuessCurrentAppURL(ctx)) + + // AWS Cognito uses "logout_uri" instead of the standard "post_logout_redirect_uri" + redirectURI := httplib.GuessCurrentAppURL(ctx) + if oauth2Cfg.Provider == oauth2.ProviderNameAwsCognito { + params.Set("logout_uri", redirectURI) + } else { + params.Set("post_logout_redirect_uri", redirectURI) + } + endSessionURL.RawQuery = params.Encode() return endSessionURL.String() } diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go index 05931d8f59..b4f36d543c 100644 --- a/routers/web/auth/oauth2_provider.go +++ b/routers/web/auth/oauth2_provider.go @@ -561,6 +561,13 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server }) return } + if grant.ApplicationID != app.ID { + handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{ + ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant, + ErrorDescription: "refresh token belongs to a different client", + }) + return + } // check if token got already used if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) { @@ -630,6 +637,13 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s }) return } + if authorizationCode.RedirectURI != "" && form.RedirectURI != authorizationCode.RedirectURI { + handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{ + ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant, + ErrorDescription: "redirect_uri differs from the original authorization request", + }) + return + } // check if granted for this application if authorizationCode.Grant.ApplicationID != app.ID { handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{ diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index 79ff12bc8d..ec1af61ee9 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -272,7 +272,7 @@ func ConnectOpenIDPost(ctx *context.Context) { // add OpenID for the user userOID := &user_model.UserOpenID{UID: u.ID, URI: oid} - if err = user_model.AddUserOpenID(ctx, userOID); err != nil { + if err := user_model.AddUserOpenID(ctx, userOID); err != nil { if user_model.IsErrOpenIDAlreadyUsed(err) { ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form) return @@ -345,11 +345,7 @@ func RegisterOpenIDPost(ctx *context.Context) { } length := max(setting.MinPasswordLength, 256) - password, err := util.CryptoRandomString(int64(length)) - if err != nil { - ctx.RenderWithErrDeprecated(err.Error(), tplSignUpOID, form) - return - } + password := util.CryptoRandomString(int64(length)) u := &user_model.User{ Name: form.UserName, @@ -363,7 +359,7 @@ func RegisterOpenIDPost(ctx *context.Context) { // add OpenID for the user userOID := &user_model.UserOpenID{UID: u.ID, URI: oid} - if err = user_model.AddUserOpenID(ctx, userOID); err != nil { + if err := user_model.AddUserOpenID(ctx, userOID); err != nil { if user_model.IsErrOpenIDAlreadyUsed(err) { ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form) return diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 8283d3ad9d..af54843016 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -17,7 +17,9 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/badge" + "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" @@ -43,8 +45,8 @@ func List(ctx *context.Context) { func FetchActionTest(ctx *context.Context) { _ = ctx.Req.ParseForm() - ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "
" + - "Form: " + ctx.Req.Form.Encode() + "
" + + ctx.Flash.Info("fetch action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "\n" + + "Form: " + ctx.Req.Form.Encode() + "\n" + "PostForm: " + ctx.Req.PostForm.Encode(), ) time.Sleep(2 * time.Second) @@ -190,15 +192,58 @@ func prepareMockData(ctx *context.Context) { prepareMockDataBadgeActionsSvg(ctx) case "/devtest/relative-time": prepareMockDataRelativeTime(ctx) + case "/devtest/toast-and-message": + prepareMockDataToastAndMessage(ctx) + case "/devtest/unicode-escape": + prepareMockDataUnicodeEscape(ctx) } } +func prepareMockDataToastAndMessage(ctx *context.Context) { + msgWithDetails, _ := ctx.RenderToHTML("base/alert_details", map[string]any{ + "Message": "message with details ", + "Summary": "summary with details", + "Details": "details line 1\n details line 2\n details line 3", + }) + msgWithSummary, _ := ctx.RenderToHTML("base/alert_details", map[string]any{ + "Message": "message with summary ", + "Summary": "summary only", + }) + + ctx.Flash.ErrorMsg = string(msgWithDetails) + ctx.Flash.WarningMsg = string(msgWithSummary) + ctx.Flash.InfoMsg = "a long message with line break\nthe second line " + ctx.Flash.SuccessMsg = "single line message " + ctx.Data["Flash"] = ctx.Flash +} + +func prepareMockDataUnicodeEscape(ctx *context.Context) { + content := "// demo code\n" + content += "if accessLevel != \"user\u202E \u2066// Check if admin (invisible char)\u2069 \u2066\" { }\n" + content += "if O𝐾 { } // ambiguous char\n" + content += "if O𝐾 && accessLevel != \"user\u202E \u2066// ambiguous char + invisible char\u2069 \u2066\" { }\n" + content += "str := `\xef` // broken char\n" + content += "str := `\x00 \x19 \x7f` // control char\n" + + lineNums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} + + highlightLines := code.HighlightSearchResultCode("demo.go", "", lineNums, content) + escapeStatus := &charset.EscapeStatus{} + lineEscapeStatus := make([]*charset.EscapeStatus, len(highlightLines)) + for i, hl := range highlightLines { + lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, ctx.Locale) + escapeStatus = escapeStatus.Or(lineEscapeStatus[i]) + } + ctx.Data["HighlightLines"] = highlightLines + ctx.Data["EscapeStatus"] = escapeStatus + ctx.Data["LineEscapeStatus"] = lineEscapeStatus +} + func TmplCommon(ctx *context.Context) { prepareMockData(ctx) - if ctx.Req.Method == http.MethodPost { - _ = ctx.Req.ParseForm() - ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"
"+ - "Form: "+ctx.Req.Form.Encode()+"
"+ + if ctx.Req.Method == http.MethodPost && ctx.FormBool("mock_response_delay") { + ctx.Flash.Info("form submit: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"\n"+ + "Form: "+ctx.Req.Form.Encode()+"\n"+ "PostForm: "+ctx.Req.PostForm.Encode(), true, ) diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index 00ca095e71..83e9bef9c8 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,27 +66,29 @@ 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 + } resp := &actions.ViewResponse{} + 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", @@ -94,37 +103,129 @@ 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, - Status: "expired", + Name: "artifact-a", + Size: 100 * 1024, + Status: "expired", + ExpiresUnix: alignTime(time.Now().Add(-24*time.Hour).Unix(), 3600), }) resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ - Name: "artifact-b", - Size: 1024 * 1024, - Status: "completed", + Name: "artifact-b", + Size: 1024 * 1024, + Status: "completed", + ExpiresUnix: alignTime(time.Now().Add(24*time.Hour).Unix(), 3600), }) resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ - Name: "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong", - Size: 100 * 1024, - Status: "expired", + Name: "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong", + Size: 100 * 1024, + Status: "expired", + ExpiresUnix: alignTime(time.Now().Add(-24*time.Hour).Unix(), 3600), }) resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ - Name: "artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong", - Size: 1024 * 1024, - Status: "completed", + Name: "artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong", + Size: 1024 * 1024, + Status: "completed", + 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(), @@ -134,13 +235,41 @@ 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: "job 102", + Name: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit", Status: actions_model.StatusFailure.String(), CanRerun: false, Duration: "3h", Needs: []string{"job-100", "job-101"}, }) + 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(), + CanRerun: false, + Duration: "2m", + Needs: []string{"job-100"}, + }) + + // 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: jobID, + Link: jobLink(jobID), + JobID: "job-dup-test-" + strconv.Itoa(i), + Name: "job dup test " + strconv.Itoa(i), + Status: actions_model.StatusSuccess.String(), + CanRerun: false, + Duration: "2m", + Needs: []string{"job-103", "job-101", "job-100"}, + }) + } + } fillViewRunResponseCurrentJob(ctx, resp) ctx.JSON(http.StatusOK, resp) @@ -152,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/feed/branch.go b/routers/web/feed/branch.go index eb7f6dc5bc..5818f509fe 100644 --- a/routers/web/feed/branch.go +++ b/routers/web/feed/branch.go @@ -4,7 +4,6 @@ package feed import ( - "strings" "time" "code.gitea.io/gitea/models/repo" @@ -39,14 +38,14 @@ func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType stri for _, commit := range commits { feed.Items = append(feed.Items, &feeds.Item{ Id: commit.ID.String(), - Title: strings.TrimSpace(strings.Split(commit.Message(), "\n")[0]), + Title: commit.MessageTitle(), Link: &feeds.Link{Href: repo.HTMLURL() + "/commit/" + commit.ID.String()}, Author: &feeds.Author{ Name: commit.Author.Name, Email: commit.Author.Email, }, - Description: commit.Message(), - Content: commit.Message(), + Description: commit.MessageUTF8(), // TODO: description can be shorten content + Content: commit.MessageUTF8(), Created: commit.Committer.When, }) } diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index a5c379e01a..5d208bb286 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -15,6 +15,7 @@ import ( activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" @@ -237,7 +238,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio } } if len(content) == 0 { - content = templates.SanitizeHTML(desc) + content = markup.Sanitize(desc) } items = append(items, &feeds.Item{ diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go index 026c15c43a..2888f4e61f 100644 --- a/routers/web/feed/file.go +++ b/routers/web/feed/file.go @@ -4,7 +4,6 @@ package feed import ( - "strings" "time" "code.gitea.io/gitea/models/repo" @@ -46,14 +45,14 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string for _, commit := range commits { feed.Items = append(feed.Items, &feeds.Item{ Id: commit.ID.String(), - Title: strings.TrimSpace(strings.Split(commit.Message(), "\n")[0]), + Title: commit.MessageTitle(), Link: &feeds.Link{Href: repo.HTMLURL() + "/commit/" + commit.ID.String()}, Author: &feeds.Author{ Name: commit.Author.Name, Email: commit.Author.Email, }, - Description: commit.Message(), - Content: commit.Message(), + Description: commit.MessageUTF8(), // TODO: description can be shorten content + Content: commit.MessageUTF8(), Created: commit.Committer.When, }) } diff --git a/routers/web/healthcheck/check.go b/routers/web/healthcheck/check.go index de9b2c8ec1..116aab886b 100644 --- a/routers/web/healthcheck/check.go +++ b/routers/web/healthcheck/check.go @@ -111,16 +111,10 @@ func checkDatabase(ctx context.Context, checks checks) status { } if setting.Database.Type.IsSQLite3() && st.Status == pass { - if !setting.EnableSQLite3 { + if _, err := os.Stat(setting.Database.Path); err != nil { st.Status = fail st.Time = getCheckTime() - log.Error("SQLite3 health check failed with error: %v", "this Gitea binary is built without SQLite3 enabled") - } else { - if _, err := os.Stat(setting.Database.Path); err != nil { - st.Status = fail - st.Time = getCheckTime() - log.Error("SQLite3 file exists check failed with error: %v", err) - } + log.Error("SQLite3 file exists check failed with error: %v", err) } } diff --git a/routers/web/home.go b/routers/web/home.go index 7efa5f344e..d14a7bab13 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 a50d9130ac..0b939ee435 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/misc/swagger.go b/routers/web/misc/swagger.go index 1ca347551c..4abd4f042d 100644 --- a/routers/web/misc/swagger.go +++ b/routers/web/misc/swagger.go @@ -6,15 +6,9 @@ package misc import ( "net/http" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" ) -// tplSwagger swagger page template -const tplSwagger templates.TplName = "swagger/ui" - -// Swagger render swagger-ui page with v1 json func Swagger(ctx *context.Context) { - ctx.Data["APIJSONVersion"] = "v1" - ctx.HTML(http.StatusOK, tplSwagger) + ctx.HTML(http.StatusOK, "swagger/openapi-viewer") } diff --git a/routers/web/org/block.go b/routers/web/org/block.go index 60f722dd39..e728e4dce5 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/home.go b/routers/web/org/home.go index e18a8de40f..56475c47f0 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -98,8 +98,10 @@ func home(ctx *context.Context, viewRepositories bool) { ctx.ServerError("FindOrgMembers", err) return } - ctx.Data["Members"] = members - ctx.Data["Teams"] = ctx.Org.Teams + + const orgOverviewTeamsLimit = 5 + ctx.Data["OrgOverviewMembers"] = members + ctx.Data["OrgOverviewTeams"] = ctx.Org.Teams[:min(len(ctx.Org.Teams), orgOverviewTeamsLimit)] ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0 @@ -180,7 +182,7 @@ func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.Pr } rctx := renderhelper.NewRenderContextRepoFile(ctx, profileRepo, renderhelper.RepoFileOptions{ - CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileRepo.DefaultBranch)), + CurrentRefSubURL: path.Join("branch", util.PathEscapeSegments(profileRepo.DefaultBranch)), }) ctx.Data["ProfileReadmeContent"], err = markdown.RenderString(rctx, readmeBytes) if err != nil { diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 6523bbf38d..5d7a0a28cd 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -5,6 +5,7 @@ package org import ( + "errors" "net/http" "code.gitea.io/gitea/models/organization" @@ -12,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" org_service "code.gitea.io/gitea/services/org" @@ -76,11 +78,11 @@ func Members(ctx *context.Context) { // MembersAction response for operation to a member of organization func MembersAction(ctx *context.Context) { member, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid")) - if err != nil { - log.Error("GetUserByID: %v", err) - } - if member == nil { - ctx.Redirect(ctx.Org.OrgLink + "/members") + if errors.Is(err, util.ErrNotExist) { + ctx.HTTPError(http.StatusNotFound) + return + } else if err != nil { + ctx.ServerError("GetUserByID", err) return } @@ -105,40 +107,25 @@ func MembersAction(ctx *context.Context) { return } err = org_service.RemoveOrgUser(ctx, org, member) - if organization.IsErrLastOrgOwner(err) { - ctx.Flash.Error(ctx.Tr("form.last_org_owner")) - ctx.JSONRedirect(ctx.Org.OrgLink + "/members") - return - } case "leave": err = org_service.RemoveOrgUser(ctx, org, ctx.Doer) if err == nil { ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName())) - ctx.JSON(http.StatusOK, map[string]any{ - "redirect": "", // keep the user stay on current page, in case they want to do other operations. - }) - } else if organization.IsErrLastOrgOwner(err) { - ctx.Flash.Error(ctx.Tr("form.last_org_owner")) - ctx.JSONRedirect(ctx.Org.OrgLink + "/members") - } else { - log.Error("RemoveOrgUser(%d,%d): %v", org.ID, ctx.Doer.ID, err) + ctx.JSONRedirect(setting.AppSubURL + "/") + return } + } + + if err == nil { + ctx.JSONOK() return } - if err != nil { - log.Error("Action(%s): %v", ctx.PathParam("action"), err) - ctx.JSON(http.StatusOK, map[string]any{ - "ok": false, - "err": err.Error(), - }) + if organization.IsErrLastOrgOwner(err) { + ctx.JSONError(ctx.Tr("form.last_org_owner")) return } - redirect := ctx.Org.OrgLink + "/members" - if ctx.PathParam("action") == "leave" { - redirect = setting.AppSubURL + "/" - } - - ctx.JSONRedirect(redirect) + log.Error("Action(%s): %v", ctx.PathParam("action"), err) + ctx.JSONError(err.Error()) // FIXME: legacy logic, errors are handled together, it's not right, need to distinguish between different errors } diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 4cdf81c155..ae32be0575 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/org/teams.go b/routers/web/org/teams.go index 1e22a67032..7312478299 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" @@ -54,13 +55,54 @@ func Teams(ctx *context.Context) { ctx.Data["Title"] = org.FullName ctx.Data["PageIsOrgTeams"] = true - for _, t := range ctx.Org.Teams { + keyword := ctx.FormTrim("q") + page := max(ctx.FormInt("page"), 1) + pagingNum := setting.UI.MembersPagingNum + + searchTeams := func() (teams []*org_model.Team, count int64, err error) { + if keyword == "" { + // fast path, use existing teams in context if no need to filter from database + count = int64(len(ctx.Org.Teams)) + start := (page - 1) * pagingNum + if start > len(ctx.Org.Teams) { + return nil, count, nil + } + end := min(start+pagingNum, len(ctx.Org.Teams)) + return ctx.Org.Teams[start:end], count, nil + } + + shouldSeeAllOrgTeams, err := context.UserShouldSeeAllOrgTeams(ctx) + if err != nil { + return nil, 0, err + } + opts := &org_model.SearchTeamOptions{ + OrgID: org.ID, + UserID: util.Iif(shouldSeeAllOrgTeams, 0, ctx.Doer.ID), + Keyword: keyword, + IncludeDesc: true, + ListOptions: db.ListOptions{Page: page, PageSize: pagingNum}, + } + return org_model.SearchTeam(ctx, opts) + } + + teams, count, err := searchTeams() + if err != nil { + ctx.ServerError("SearchTeam", err) + return + } + + for _, t := range teams { if err := t.LoadMembers(ctx); err != nil { ctx.ServerError("GetMembers", err) return } } - ctx.Data["Teams"] = ctx.Org.Teams + + ctx.Data["OrgListTeams"] = teams + ctx.Data["Keyword"] = keyword + pager := context.NewPagination(count, setting.UI.MembersPagingNum, page, 5) + pager.AddParamFromRequest(ctx.Req) + ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplTeams) } @@ -213,7 +255,7 @@ func checkIsOrgMemberAndRedirect(ctx *context.Context, defaultRedirect string) { if isOrgMember, err := org_model.IsOrganizationMember(ctx, ctx.Org.Organization.ID, ctx.Doer.ID); err != nil { ctx.ServerError("IsOrganizationMember", err) return - } else if !isOrgMember { + } else if !isOrgMember && !ctx.Doer.IsAdmin { if ctx.Org.Organization.Visibility.IsPrivate() { defaultRedirect = setting.AppSubURL + "/" } else { @@ -567,6 +609,8 @@ func DeleteTeam(ctx *context.Context) { // TeamInvite renders the team invite page func TeamInvite(ctx *context.Context) { invite, org, team, inviter, err := getTeamInviteFromContext(ctx) + // TODO: to quickly debug the UI, can uncomment this (don't worry, it won't pass CI lint) + // invite, org, team, inviter, err = &org_model.TeamInvite{}, &org_model.Organization{}, &org_model.Team{}, ctx.Doer, nil if err != nil { if org_model.IsErrTeamInviteNotFound(err) { ctx.NotFound(err) diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 988d2d0a99..5c454dac24 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -28,7 +28,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" - act_model "github.com/nektos/act/pkg/model" + act_model "gitea.com/gitea/runner/act/model" "go.yaml.in/yaml/v4" ) @@ -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 } @@ -151,6 +151,11 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow workflows = append(workflows, workflow) continue } + if err := actions.ValidateWorkflowContent(content); err != nil { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) + workflows = append(workflows, workflow) + continue + } workflow.Workflow = wf // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. hasJobWithoutNeeds := false @@ -176,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 @@ -187,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 } @@ -306,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 @@ -315,6 +320,10 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) { if !job.Status.IsWaiting() { continue } + if err := actions.ValidateWorkflowContent(job.WorkflowPayload); err != nil { + runErrors[run.ID] = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) + break + } hasOnlineRunner := false for _, runner := range runners { if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) { @@ -346,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/actions_test.go b/routers/web/repo/actions/actions_test.go index 9e83ff0fbb..3925337844 100644 --- a/routers/web/repo/actions/actions_test.go +++ b/routers/web/repo/actions/actions_test.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/models/db" unittest "code.gitea.io/gitea/models/unittest" - act_model "github.com/nektos/act/pkg/model" + act_model "gitea.com/gitea/runner/act/model" "github.com/stretchr/testify/assert" ) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 98d86f0bb3..99f15a4605 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/templates" @@ -33,9 +34,8 @@ 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" + "gitea.com/gitea/runner/act/model" ) func findCurrentJobByPathParam(ctx *context_module.Context, jobs []*actions_model.ActionRunJob) (job *actions_model.ActionRunJob, hasPathParam bool) { @@ -67,15 +67,158 @@ func getCurrentRunByPathParam(ctx *context_module.Context) (run *actions_model.A return run } +// resolveCurrentRunForView resolves GET Actions page URLs and supports both ID-based and legacy index-based forms. +// +// By default, run summary pages (/actions/runs/{run}) use a best-effort ID-first fallback, +// and job pages (/actions/runs/{run}/jobs/{job}) try to confirm an ID-based URL first and prefer the ID-based interpretation when both are valid. +// +// `by_id=1` param explicitly forces the ID-based path, and `by_index=1` explicitly forces the legacy index-based path. +// If both are present, `by_id` takes precedence. +func resolveCurrentRunForView(ctx *context_module.Context) *actions_model.ActionRun { + // `by_id` explicitly requests ID-based resolution, so the request skips the legacy index-based disambiguation logic and resolves the run by ID directly. + // It takes precedence over `by_index` when both query parameters are present. + if ctx.PathParam("run") == "latest" || ctx.FormBool("by_id") { + return getCurrentRunByPathParam(ctx) + } + + runNum := ctx.PathParamInt64("run") + if runNum <= 0 { + ctx.NotFound(nil) + return nil + } + + byIndex := ctx.FormBool("by_index") + + if ctx.PathParam("job") == "" { + // The URL does not contain a {job} path parameter, so it cannot use the + // job-specific rules to disambiguate ID-based URLs from legacy index-based URLs. + // Because of that, this path is handled with a best-effort ID-first fallback by default. + // + // When the same repository contains: + // - a run whose ID matches runNum, and + // - a different run whose repo-scope index also matches runNum + // this path prefers the ID match and may show a different run than the old legacy URL originally intended, + // unless `by_index=1` explicitly forces the legacy index-based interpretation. + + if !byIndex { + runByID, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runNum) + if err == nil { + return runByID + } + if !errors.Is(err, util.ErrNotExist) { + ctx.ServerError("GetRun:"+ctx.PathParam("run"), err) + return nil + } + } + + runByIndex, err := actions_model.GetRunByRepoAndIndex(ctx, ctx.Repo.Repository.ID, runNum) + if err == nil { + ctx.Redirect(fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.RepoLink, runByIndex.ID), http.StatusFound) + return nil + } + if !errors.Is(err, util.ErrNotExist) { + ctx.ServerError("GetRunByRepoAndIndex", err) + return nil + } + ctx.NotFound(nil) + return nil + } + + jobNum := ctx.PathParamInt64("job") + if jobNum < 0 { + ctx.NotFound(nil) + return nil + } + + // A job index should not be larger than MaxJobNumPerRun, so larger values can skip the legacy index-based path and be treated as job IDs directly. + if !byIndex && jobNum >= actions_model.MaxJobNumPerRun { + return getCurrentRunByPathParam(ctx) + } + + var runByID, runByIndex *actions_model.ActionRun + var targetJobByIndex *actions_model.ActionRunJob + + // Each run must have at least one job, so a valid job ID in the same run cannot be smaller than the run ID. + if !byIndex && jobNum >= runNum { + // Probe the repo-scoped job ID first and only accept it when the job exists and belongs to the same runNum. + job, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobNum) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.ServerError("GetRunJobByRepoAndID", err) + return nil + } + if job != nil { + if err := job.LoadRun(ctx); err != nil { + ctx.ServerError("LoadRun", err) + return nil + } + if job.Run.ID == runNum { + runByID = job.Run + } + } + } + + // Try to resolve the request as a legacy run-index/job-index URL. + { + run, err := actions_model.GetRunByRepoAndIndex(ctx, ctx.Repo.Repository.ID, runNum) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.ServerError("GetRunByRepoAndIndex", err) + return nil + } + if run != nil { + jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID) + if err != nil { + ctx.ServerError("GetRunJobsByRunID", err) + return nil + } + if jobNum < int64(len(jobs)) { + runByIndex = run + targetJobByIndex = jobs[jobNum] + } + } + } + + if runByID == nil && runByIndex == nil { + ctx.NotFound(nil) + return nil + } + + if runByID != nil && runByIndex == nil { + return runByID + } + + if runByID == nil && runByIndex != nil { + ctx.Redirect(fmt.Sprintf("%s/actions/runs/%d/jobs/%d", ctx.Repo.RepoLink, runByIndex.ID, targetJobByIndex.ID), http.StatusFound) + return nil + } + + // Reaching this point means both ID-based and legacy index-based interpretations are valid. Prefer the ID-based interpretation by default. + // Use `by_index=1` query parameter to access the legacy index-based interpretation when necessary. + return runByID +} + func View(ctx *context_module.Context) { ctx.Data["PageIsActions"] = true - run := getCurrentRunByPathParam(ctx) + run := resolveCurrentRunForView(ctx) 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) } @@ -118,9 +261,10 @@ type ViewRequest struct { } type ArtifactsViewItem struct { - Name string `json:"name"` - Size int64 `json:"size"` - Status string `json:"status"` + Name string `json:"name"` + Size int64 `json:"size"` + Status string `json:"status"` + ExpiresUnix int64 `json:"expiresUnix"` } type ViewResponse struct { @@ -128,21 +272,30 @@ type ViewResponse struct { State struct { Run struct { - 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 @@ -161,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"` @@ -169,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"` @@ -206,23 +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"), - }) - } - return artifactsViewItems, nil -} - func ViewPost(ctx *context_module.Context) { - run, jobs := getCurrentRunJobsByPathParam(ctx) + run, attempt, jobs := getCurrentRunJobsByPathParam(ctx) if ctx.Written() { return } @@ -232,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 } @@ -243,22 +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 { @@ -267,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(), @@ -285,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(), @@ -309,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) { @@ -325,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 @@ -455,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 } @@ -474,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) { @@ -520,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 @@ -542,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() } @@ -558,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) { @@ -651,28 +814,109 @@ 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 } + jobs.SortMatrixGroupsByName() 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) { @@ -680,9 +924,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{}{}) @@ -693,14 +944,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 { @@ -716,10 +970,11 @@ func ArtifactsDownloadView(ctx *context_module.Context) { } } - ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName)) - - if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) { - err := actions.DownloadArtifactV4(ctx.Base, artifacts[0]) + // A v4 Artifact may only contain a single file + // Multiple files are uploaded as a single file archive + // All other cases fall back to the legacy v1–v3 zip handling below + if len(artifacts) == 1 && actions_service.IsArtifactV4(artifacts[0]) { + err := actions_service.DownloadArtifactV4(ctx.Base, artifacts[0]) if err != nil { ctx.ServerError("DownloadArtifactV4", err) return @@ -729,34 +984,41 @@ func ArtifactsDownloadView(ctx *context_module.Context) { // Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend // Those need to be zipped for download - writer := zip.NewWriter(ctx.Resp) - defer writer.Close() - for _, art := range artifacts { + ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(artifactName+".zip")) + zipWriter := zip.NewWriter(ctx.Resp) + defer zipWriter.Close() + + writeArtifactToZip := func(art *actions_model.ActionArtifact) error { f, err := storage.ActionsArtifacts.Open(art.StoragePath) if err != nil { - ctx.ServerError("ActionsArtifacts.Open", err) - return + return fmt.Errorf("ActionsArtifacts.Open: %w", err) } + defer f.Close() - var r io.ReadCloser - if art.ContentEncoding == "gzip" { + var r io.ReadCloser = f + if art.ContentEncodingOrType == actions_model.ContentEncodingV3Gzip { r, err = gzip.NewReader(f) if err != nil { - ctx.ServerError("gzip.NewReader", err) - return + return fmt.Errorf("gzip.NewReader: %w", err) } - } else { - r = f } defer r.Close() - w, err := writer.Create(art.ArtifactPath) + w, err := zipWriter.Create(art.ArtifactPath) if err != nil { - ctx.ServerError("writer.Create", err) - return + return fmt.Errorf("zipWriter.Create: %w", err) } - if _, err := io.Copy(w, r); err != nil { - ctx.ServerError("io.Copy", err) + _, err = io.Copy(w, r) + if err != nil { + return fmt.Errorf("io.Copy: %w", err) + } + return nil + } + + for _, art := range artifacts { + err := writeArtifactToZip(art) + if err != nil { + ctx.ServerError("writeArtifactToZip", err) return } } @@ -789,8 +1051,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 } @@ -809,7 +1073,7 @@ func EnableWorkflowFile(ctx *context_module.Context) { func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) { workflow := ctx.FormString("workflow") if len(workflow) == 0 { - ctx.ServerError("workflow", nil) + ctx.JSONError("workflow is required") return } diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go index 4cfe879032..420fa6d557 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/attachment.go b/routers/web/repo/attachment.go index 19d533f362..247f6d530b 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -6,33 +6,45 @@ package repo import ( "net/http" + auth_model "code.gitea.io/gitea/models/auth" issues_model "code.gitea.io/gitea/models/issues" 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/modules/httpcache" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" - "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/attachment" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" repo_service "code.gitea.io/gitea/services/repository" ) +func attachmentReadScope(unitType unit.Type) (auth_model.AccessTokenScope, bool) { + switch unitType { + case unit.TypeIssues, unit.TypePullRequests: + return auth_model.AccessTokenScopeReadIssue, true + case unit.TypeReleases: + return auth_model.AccessTokenScopeReadRepository, true + default: + return "", false + } +} + // UploadIssueAttachment response for Issue/PR attachments func UploadIssueAttachment(ctx *context.Context) { - uploadAttachment(ctx, ctx.Repo.Repository.ID, setting.Attachment.AllowedTypes) + uploadAttachment(ctx, ctx.Repo.Repository.ID, attachment.UploadAttachmentForIssue) } // UploadReleaseAttachment response for uploading release attachments func UploadReleaseAttachment(ctx *context.Context) { - uploadAttachment(ctx, ctx.Repo.Repository.ID, setting.Repository.Release.AllowedTypes) + uploadAttachment(ctx, ctx.Repo.Repository.ID, attachment.UploadAttachmentForRelease) } // UploadAttachment response for uploading attachments -func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) { +func uploadAttachment(ctx *context.Context, repoID int64, uploadFunc attachment.UploadAttachmentFunc) { if !setting.Attachment.Enabled { ctx.HTTPError(http.StatusNotFound, "attachment is not enabled") return @@ -46,7 +58,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) { defer file.Close() uploaderFile := attachment.NewLimitedUploaderKnownSize(file, header.Size) - attach, err := attachment.UploadAttachmentReleaseSizeLimit(ctx, uploaderFile, allowedTypes, &repo_model.Attachment{ + attach, err := uploadFunc(ctx, uploaderFile, &repo_model.Attachment{ Name: header.Filename, UploaderID: ctx.Doer.ID, RepoID: repoID, @@ -56,7 +68,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) { ctx.HTTPError(http.StatusBadRequest, err.Error()) return } - ctx.ServerError("UploadAttachmentReleaseSizeLimit", err) + ctx.ServerError("uploadAttachment(uploadFunc)", err) return } @@ -119,7 +131,7 @@ func DeleteAttachment(ctx *context.Context) { }) } -// GetAttachment serve attachments with the given UUID +// ServeAttachment serve attachments with the given UUID func ServeAttachment(ctx *context.Context, uuid string) { attach, err := repo_model.GetAttachmentByUUID(ctx, uuid) if err != nil { @@ -150,16 +162,19 @@ func ServeAttachment(ctx *context.Context, uuid string) { return } } else { // If we have the linked type, we need to check access - var perm access_model.Permission - if ctx.Repo.Repository == nil { - repo, err := repo_model.GetRepositoryByID(ctx, repoID) + var ( + perm access_model.Permission + repo = ctx.Repo.Repository + ) + if repo == nil { + repo, err = repo_model.GetRepositoryByID(ctx, repoID) if err != nil { ctx.ServerError("GetRepositoryByID", err) return } - perm, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + perm, err = access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.ServerError("GetDoerRepoPermission", err) return } } else { @@ -170,6 +185,13 @@ func ServeAttachment(ctx *context.Context, uuid string) { ctx.HTTPError(http.StatusNotFound) return } + + if requiredScope, ok := attachmentReadScope(unitType); ok { + context.CheckTokenScopes(ctx, repo, requiredScope) + if ctx.Written() { + return + } + } } if err := attach.IncreaseDownloadCount(ctx); err != nil { @@ -199,7 +221,7 @@ func ServeAttachment(ctx *context.Context, uuid string) { } defer fr.Close() - common.ServeContentByReadSeeker(ctx.Base, attach.Name, new(attach.CreatedUnix.AsTime()), fr) + httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, fr, httplib.ServeHeaderOptions{Filename: attach.Name}) } // GetAttachment serve attachments diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index 4fb61bee6d..803397f33f 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -231,7 +231,7 @@ func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.Avatar br.PreviousSha = part.PreviousSha br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath)) br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha)) - br.CommitMessage = commit.CommitMessage + br.CommitMessage = commit.MessageUTF8() br.CommitSince = templates.TimeSince(commit.Author.When) } diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 5e5cfec5c2..ce0e0b03ab 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]) } @@ -231,7 +231,7 @@ func CreateBranch(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.push_rejected"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(e.Message), + "Details": utils.EscapeFlashErrorString(e.Message), }) if err != nil { ctx.ServerError("UpdatePullRequest.HTMLString", err) diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 168d959494..6c973696ff 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -9,7 +9,6 @@ import ( "fmt" "html/template" "net/http" - "path" "strings" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -97,8 +96,6 @@ func Commits(ctx *context.Context) { } else { ctx.Data["CommitsTagsMap"] = commitsTagsMap } - ctx.Data["Username"] = ctx.Repo.Owner.Name - ctx.Data["Reponame"] = ctx.Repo.Repository.Name ctx.Data["CommitCount"] = commitsCount pager := context.NewPagination(commitsCount, pageSize, page, 5) @@ -164,9 +161,6 @@ func Graph(ctx *context.Context) { ctx.Data["AllRefs"] = gitRefs - ctx.Data["Username"] = ctx.Repo.Owner.Name - ctx.Data["Reponame"] = ctx.Repo.Repository.Name - divOnly := ctx.FormBool("div-only") queryParams := ctx.Req.URL.Query() queryParams.Del("div-only") @@ -210,8 +204,6 @@ func SearchCommits(ctx *context.Context) { if all { ctx.Data["All"] = true } - ctx.Data["Username"] = ctx.Repo.Owner.Name - ctx.Data["Reponame"] = ctx.Repo.Repository.Name ctx.HTML(http.StatusOK, tplCommits) } @@ -249,8 +241,6 @@ func FileHistory(ctx *context.Context) { return } - ctx.Data["Username"] = ctx.Repo.Owner.Name - ctx.Data["Reponame"] = ctx.Repo.Repository.Name ctx.Data["FileTreePath"] = ctx.Repo.TreePath ctx.Data["CommitCount"] = commitsCount @@ -322,7 +312,7 @@ func Diff(ctx *context.Context) { MaxLines: maxLines, MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters, MaxFiles: maxFiles, - WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)), + WhitespaceBehavior: gitdiff.GetWhitespaceFlag(GetWhitespaceBehavior(ctx)), }, files...) if err != nil { ctx.NotFound(err) @@ -347,8 +337,6 @@ func Diff(ctx *context.Context) { ctx.Data["CommitID"] = commitID ctx.Data["AfterCommitID"] = commitID - ctx.Data["Username"] = userName - ctx.Data["Reponame"] = repoName var parentCommit *git.Commit var parentCommitID string @@ -361,7 +349,7 @@ func Diff(ctx *context.Context) { parentCommitID = parentCommit.ID.String() } setCompareContext(ctx, parentCommit, commit, userName, repoName) - ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID) + ctx.Data["Title"] = commit.MessageTitle() + " · " + base.ShortSha(commitID) ctx.Data["Commit"] = commit ctx.Data["Diff"] = diff ctx.Data["DiffBlobExcerptData"] = diffBlobExcerptData @@ -384,7 +372,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) } @@ -409,8 +397,9 @@ func Diff(ctx *context.Context) { if err == nil { ctx.Data["NoteCommit"] = note.Commit ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) - rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefPath: path.Join("commit", util.PathEscapeSegments(commitID))}) - ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) + rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefSubURL: "commit/" + util.PathEscapeSegments(commitID)}) + htmlMessage := template.HTML(template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) + ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, htmlMessage) if err != nil { ctx.ServerError("PostProcessCommitMessage", err) return @@ -465,7 +454,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/common_recentbranches.go b/routers/web/repo/common_recentbranches.go index c2083dec73..c749f15755 100644 --- a/routers/web/repo/common_recentbranches.go +++ b/routers/web/repo/common_recentbranches.go @@ -33,9 +33,9 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) { opts.BaseRepo = ctx.Repo.Repository.BaseRepo } - baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer) + baseRepoPerm, err := access_model.GetDoerRepoPermission(ctx, opts.BaseRepo, ctx.Doer) if err != nil { - log.Error("GetUserRepoPermission: %v", err) + log.Error("GetDoerRepoPermission: %v", err) return } if !opts.Repo.CanContentChange() || !opts.BaseRepo.CanContentChange() { diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index e034731e5c..46867e80bb 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -13,6 +13,7 @@ import ( "path/filepath" "sort" "strings" + "unicode" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" @@ -189,8 +190,18 @@ func setCsvCompareContext(ctx *context.Context) { } } -// ParseCompareInfo parse compare info between two commit for preparing comparing references -func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { +type comparePageInfoType struct { + compareInfo *git_service.CompareInfo + nothingToCompare bool + allowCreatePull bool +} + +func newComparePageInfo() *comparePageInfoType { + return &comparePageInfoType{} +} + +// parseCompareInfo parse compare info between two commit for preparing comparing references +func (cpi *comparePageInfoType) parseCompareInfo(ctx *context.Context) error { baseRepo := ctx.Repo.Repository fileOnly := ctx.FormBool("file-only") @@ -199,47 +210,29 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { // remove the check when we support compare with carets if compareReq.BaseOriRefSuffix != "" { - ctx.HTTPError(http.StatusBadRequest, "Unsupported comparison syntax: ref with suffix") - return nil + return util.NewInvalidArgumentErrorf("unsupported comparison syntax: ref with suffix") } // 2 get repository and owner for head headOwner, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq) - switch { - case errors.Is(err, util.ErrInvalidArgument): - ctx.HTTPError(http.StatusBadRequest, err.Error()) - return nil - case errors.Is(err, util.ErrNotExist): - ctx.NotFound(nil) - return nil - case err != nil: - ctx.ServerError("GetHeadOwnerAndRepo", err) - return nil + if err != nil { + return err } - isSameRepo := baseRepo.ID == headRepo.ID - // 3 permission check // base repository's code unit read permission check has been done on web.go permBase := ctx.Repo.Permission // If we're not merging from the same repo: + isSameRepo := baseRepo.ID == headRepo.ID if !isSameRepo { // Assert ctx.Doer has permission to read headRepo's codes - permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer) + permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) - return nil + return err } if !permHead.CanRead(unit.TypeCode) { - if log.IsTrace() { - log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", - ctx.Doer, - headRepo, - permHead) - } - ctx.NotFound(nil) - return nil + return util.NewNotExistErrorf("") // permission: no error message for end users } ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode) } @@ -250,24 +243,16 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefName) if baseRef == "" { - ctx.NotFound(nil) - return nil + return util.NewNotExistErrorf("no base ref: %s", baseRefName) } - var headGitRepo *git.Repository - if isSameRepo { - headGitRepo = ctx.Repo.GitRepo - } else { - headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo) - if err != nil { - ctx.ServerError("OpenRepository", err) - return nil - } - defer headGitRepo.Close() + headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, headRepo) + if err != nil { + return err } + headRef := headGitRepo.UnstableGuessRefByShortName(headRefName) if headRef == "" { - ctx.NotFound(nil) - return nil + return util.NewNotExistErrorf("no head ref: %s", headRefName) } ctx.Data["BaseName"] = baseRepo.OwnerName @@ -291,12 +276,9 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { var rootRepo *repo_model.Repository if baseRepo.IsFork { err = baseRepo.GetBaseRepo(ctx) - if err != nil { - if !repo_model.IsErrRepoNotExist(err) { - ctx.ServerError("Unable to find root repo", err) - return nil - } - } else { + if err != nil && !repo_model.IsErrRepoNotExist(err) { + return err + } else if err == nil { rootRepo = baseRepo.BaseRepo } } @@ -313,42 +295,10 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { } } - has := headRepo != nil - // 3. If the base is a forked from "RootRepo" and the owner of - // the "RootRepo" is the :headUser - set headRepo to that - if !has && rootRepo != nil && rootRepo.OwnerID == headOwner.ID { - headRepo = rootRepo - has = true - } - - // 4. If the ctx.Doer has their own fork of the baseRepo and the headUser is the ctx.Doer - // set the headRepo to the ownFork - if !has && ownForkRepo != nil && ownForkRepo.OwnerID == headOwner.ID { - headRepo = ownForkRepo - has = true - } - - // 5. If the headOwner has a fork of the baseRepo - use that - if !has { - headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ID) - has = headRepo != nil - } - - // 6. If the baseRepo is a fork and the headUser has a fork of that use that - if !has && baseRepo.IsFork { - headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ForkID) - has = headRepo != nil - } - - // 7. Otherwise if we're not the same repo and haven't found a repo give up - if !isSameRepo && !has { - ctx.Data["PageIsComparePull"] = false - } - ctx.Data["HeadRepo"] = headRepo ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository - // If we have a rootRepo and it's different from: + // If we have a rootRepo, and it's different from: // 1. the computed base // 2. the computed head // then get the branches of it @@ -361,17 +311,15 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { if !fileOnly { branches, tags, err := getBranchesAndTagsForRepo(ctx, rootRepo) if err != nil { - ctx.ServerError("GetBranchesForRepo", err) - return nil + return err } - ctx.Data["RootRepoBranches"] = branches ctx.Data["RootRepoTags"] = tags } } } - // If we have a ownForkRepo and it's different from: + // If we have a ownForkRepo, and it's different from: // 1. The computed base // 2. The computed head // 3. The rootRepo (if we have one) @@ -386,8 +334,7 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { if !fileOnly { branches, tags, err := getBranchesAndTagsForRepo(ctx, ownForkRepo) if err != nil { - ctx.ServerError("GetBranchesForRepo", err) - return nil + return err } ctx.Data["OwnForkRepoBranches"] = branches ctx.Data["OwnForkRepoTags"] = tags @@ -395,51 +342,63 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { } } - // Treat as pull request if both references are branches - if ctx.Data["PageIsComparePull"] == nil { - ctx.Data["PageIsComparePull"] = baseRef.IsBranch() && headRef.IsBranch() && permBase.CanReadIssuesOrPulls(true) - } - - if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) { - if log.IsTrace() { - log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in baseRepo has Permissions: %-+v", - ctx.Doer, - baseRepo, - permBase) - } - ctx.NotFound(nil) - return nil - } - compareInfo, err := git_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef, headRef, compareReq.DirectComparison(), fileOnly) if err != nil { - ctx.ServerError("GetCompareInfo", err) - return nil - } - if compareReq.DirectComparison() { - ctx.Data["BeforeCommitID"] = compareInfo.BaseCommitID - } else { - ctx.Data["BeforeCommitID"] = compareInfo.MergeBase + return err } - return compareInfo + // Treat as pull request if both references are branches + cpi.allowCreatePull = baseRef.IsBranch() && headRef.IsBranch() && permBase.CanReadIssuesOrPulls(true) + cpi.allowCreatePull = cpi.allowCreatePull && compareInfo.CompareBase != "" + cpi.compareInfo = &compareInfo + return nil } -func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses) (title, content string) { - title = ci.HeadRef.ShortName() +// autoTitleFromBranchName humanizes a branch name into a PR title. +func autoTitleFromBranchName(name string) string { + var buf strings.Builder + var prevIsSpace bool + runes := []rune(name) + for i, r := range runes { + isSpace := unicode.IsSpace(r) + if r == '-' || r == '_' || isSpace { + if !prevIsSpace { + buf.WriteRune(' ') + } + prevIsSpace = true + continue + } + if !prevIsSpace && unicode.IsUpper(r) { + needSpace := i > 0 && unicode.IsLower(runes[i-1]) || i < len(runes)-1 && unicode.IsLower(runes[i+1]) + if needSpace { + buf.WriteRune(' ') + } + } + buf.WriteRune(unicode.ToLower(r)) + prevIsSpace = isSpace + } + out := strings.TrimSpace(buf.String()) + if out == "" { + return out + } + outRunes := []rune(out) + outRunes[0] = unicode.ToUpper(outRunes[0]) + return string(outRunes) +} - if len(commits) > 0 { +func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses, defaultTitleSource string) (title, content string) { + useFirstCommitAsTitle := len(commits) == 1 || (defaultTitleSource == setting.RepoPRTitleSourceFirstCommit && len(commits) > 0) + if useFirstCommitAsTitle { // the "commits" are from "ShowPrettyFormatLogToList", which is ordered from newest to oldest, here take the oldest one c := commits[len(commits)-1] - title = strings.TrimSpace(c.UserCommit.Summary()) + title = c.UserCommit.MessageTitle() + } else { + title = autoTitleFromBranchName(ci.HeadRef.ShortName()) } if len(commits) == 1 { - // FIXME: GIT-COMMIT-MESSAGE-ENCODING: try to convert the encoding for commit message explicitly, ideally it should be done by a git commit struct method c := commits[0] - _, content, _ = strings.Cut(strings.TrimSpace(c.UserCommit.CommitMessage), "\n") - content = strings.TrimSpace(content) - content = string(charset.ToUTF8([]byte(content), charset.ConvertOpts{})) + content = c.MessageBody() } var titleTrailer string @@ -455,16 +414,18 @@ func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*g return title, content } -// PrepareCompareDiff renders compare diff page -func PrepareCompareDiff( - ctx *context.Context, - ci *git_service.CompareInfo, - whitespaceBehavior gitcmd.TrustedCmdArgs, -) (nothingToCompare bool) { +// prepareCompareDiff renders compare diff page. TODO: need to refactor it and other "compare diff" related functions together +func (cpi *comparePageInfoType) prepareCompareDiff(ctx *context.Context, whitespaceBehavior gitcmd.TrustedCmdArgs) { + ci := cpi.compareInfo + if ci.CompareBase == "" { + cpi.nothingToCompare = true + return + } repo := ctx.Repo.Repository headCommitID := ci.HeadCommitID ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link() + ctx.Data["BeforeCommitID"] = ci.CompareBase ctx.Data["AfterCommitID"] = headCommitID // follow GitHub's behavior: autofill the form and expand @@ -474,26 +435,18 @@ func PrepareCompareDiff( ctx.Data["TitleQuery"] = newPrFormTitle ctx.Data["BodyQuery"] = newPrFormBody - if (headCommitID == ci.MergeBase && !ci.DirectComparison()) || - headCommitID == ci.BaseCommitID { - ctx.Data["IsNothingToCompare"] = true - if unit, err := repo.GetUnit(ctx, unit.TypePullRequests); err == nil { - config := unit.PullRequestsConfig() + if headCommitID == ci.CompareBase { + config := repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() + // if auto-detect manual merge, an empty PR will be closed immediately because it is already on base branch + supportEmptyPr := !config.AutodetectManualMerge + acrossRepoPr := !ci.IsSameRef() + ctx.Data["AllowEmptyPr"] = supportEmptyPr && acrossRepoPr - if !config.AutodetectManualMerge { - ctx.Data["AllowEmptyPr"] = !ci.IsSameRef() - return ci.IsSameRef() - } - - ctx.Data["AllowEmptyPr"] = false - } - return true + cpi.nothingToCompare = true + return } - beforeCommitID := ci.MergeBase - if ci.DirectComparison() { - beforeCommitID = ci.BaseCommitID - } + beforeCommitID := ci.CompareBase maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles files := ctx.FormStrings("files") @@ -516,12 +469,12 @@ func PrepareCompareDiff( }, ctx.FormStrings("files")...) if err != nil { ctx.ServerError("GetDiff", err) - return false + return } diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ci.HeadRepo, ci.HeadGitRepo, beforeCommitID, headCommitID) if err != nil { ctx.ServerError("GetDiffShortStat", err) - return false + return } ctx.Data["DiffShortStat"] = diffShortStat ctx.Data["Diff"] = diff @@ -536,7 +489,7 @@ func PrepareCompareDiff( diffTree, err := gitdiff.GetDiffTree(ctx, ci.HeadGitRepo, false, beforeCommitID, headCommitID) if err != nil { ctx.ServerError("GetDiffTree", err) - return false + return } renderedIconPool := fileicon.NewRenderedIconPool() @@ -549,7 +502,7 @@ func PrepareCompareDiff( headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID) if err != nil { ctx.ServerError("GetCommit", err) - return false + return } baseGitRepo := ctx.Repo.GitRepo @@ -557,24 +510,20 @@ func PrepareCompareDiff( beforeCommit, err := baseGitRepo.GetCommit(beforeCommitID) if err != nil { ctx.ServerError("GetCommit", err) - return false + return } commits, err := processGitCommits(ctx, ci.Commits) if err != nil { ctx.ServerError("processGitCommits", err) - return false + return } ctx.Data["Commits"] = commits ctx.Data["CommitCount"] = len(commits) - ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits) - ctx.Data["Username"] = ci.HeadRepo.OwnerName - ctx.Data["Reponame"] = ci.HeadRepo.Name + ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits, setting.Repository.PullRequest.DefaultTitleSource) setCompareContext(ctx, beforeCommit, headCommit, ci.HeadRepo.OwnerName, repo.Name) - - return false } func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repository) (branches, tags []string, err error) { @@ -595,16 +544,22 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor // CompareDiff show different from one commit to another commit func CompareDiff(ctx *context.Context) { - ci := ParseCompareInfo(ctx) - if ctx.Written() { + comparePageInfo := newComparePageInfo() + err := comparePageInfo.parseCompareInfo(ctx) + if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { + ctx.NotFound(nil) + return + } else if err != nil { + ctx.ServerError("ParseCompareInfo", err) return } - + ci := comparePageInfo.compareInfo ctx.Data["PageIsViewCode"] = true ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["CompareInfo"] = ci - nothingToCompare := PrepareCompareDiff(ctx, ci, gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string))) + // TODO: need to refactor "prepare compare" related functions together + comparePageInfo.prepareCompareDiff(ctx, gitdiff.GetWhitespaceFlag(GetWhitespaceBehavior(ctx))) if ctx.Written() { return } @@ -622,16 +577,13 @@ func CompareDiff(ctx *context.Context) { return } - headBranches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ - RepoID: ci.HeadRepo.ID, - ListOptions: db.ListOptionsAll, - IsDeletedBranch: optional.Some(false), - }) + headBranches, headTags, err := getBranchesAndTagsForRepo(ctx, ci.HeadRepo) if err != nil { - ctx.ServerError("GetBranches", err) + ctx.ServerError("GetBranchesAndTagsForRepo", err) return } ctx.Data["HeadBranches"] = headBranches + ctx.Data["HeadTags"] = headTags // For compare repo branches PrepareBranchList(ctx) @@ -639,14 +591,23 @@ func CompareDiff(ctx *context.Context) { return } - headTags, err := repo_model.GetTagNamesByRepoID(ctx, ci.HeadRepo.ID) - if err != nil { - ctx.ServerError("GetTagNamesByRepoID", err) - return + if ci.CompareBase != "" { + comparePageInfo.prepareCreatePullRequestPage(ctx) + if ctx.Written() { + return + } + } else { + ctx.Flash.Error(ctx.Tr("repo.pulls.no_common_history"), true) + ctx.Data["CommitCount"] = 0 } - ctx.Data["HeadTags"] = headTags + ctx.Data["PageIsComparePull"] = comparePageInfo.allowCreatePull + ctx.Data["IsNothingToCompare"] = comparePageInfo.nothingToCompare + ctx.HTML(http.StatusOK, tplCompare) +} - if ctx.Data["PageIsComparePull"] == true { +func (cpi *comparePageInfoType) prepareCreatePullRequestPage(ctx *context.Context) { + ci := cpi.compareInfo + if cpi.allowCreatePull { pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadRef.ShortName(), ci.BaseRef.ShortName(), issues_model.PullRequestFlowGithub) if err != nil { if !issues_model.IsErrPullRequestNotExist(err) { @@ -660,11 +621,10 @@ func CompareDiff(ctx *context.Context) { return } ctx.Data["PullRequest"] = pr - ctx.HTML(http.StatusOK, tplCompareDiff) return } - if !nothingToCompare { + if !cpi.nothingToCompare { // Setup information for new form. pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, true) if ctx.Written() { @@ -676,8 +636,8 @@ func CompareDiff(ctx *context.Context) { } } } - beforeCommitID := ctx.Data["BeforeCommitID"].(string) - afterCommitID := ctx.Data["AfterCommitID"].(string) + beforeCommitID := cpi.compareInfo.CompareBase + afterCommitID := cpi.compareInfo.HeadCommitID ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + ci.CompareSeparator + base.ShortSha(afterCommitID) @@ -686,7 +646,7 @@ func CompareDiff(ctx *context.Context) { if content, ok := ctx.Data["content"].(string); ok && content != "" { // If a template content is set, prepend the "content". In this case that's only // applicable if you have one commit to compare and that commit has a message. - // In that case the commit message will be prepend to the template body. + // In that case the commit message will be prepended to the template body. if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" { // Re-use the same key as that's prioritized over the "content" key. // Add two new lines between the content to ensure there's always at least @@ -708,20 +668,14 @@ 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() - ctx.Data["AllowMaintainerEdit"] = config.DefaultAllowMaintainerEdit - } else { - ctx.Data["AllowMaintainerEdit"] = false - } - - ctx.HTML(http.StatusOK, tplCompare) + prConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() + ctx.Data["AllowMaintainerEdit"] = prConfig.DefaultAllowMaintainerEdit } // attachCommentsToLines attaches comments to their corresponding diff lines @@ -803,7 +757,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/compare_test.go b/routers/web/repo/compare_test.go index 700aba8821..da5b2c84b3 100644 --- a/routers/web/repo/compare_test.go +++ b/routers/web/repo/compare_test.go @@ -6,13 +6,13 @@ package repo import ( "strings" "testing" - "unicode/utf8" asymkey_model "code.gitea.io/gitea/models/asymkey" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" git_service "code.gitea.io/gitea/services/git" "code.gitea.io/gitea/services/gitdiff" @@ -54,38 +54,73 @@ func TestNewPullRequestTitleContent(t *testing.T) { SignCommit: &asymkey_model.SignCommit{ UserCommit: &user_model.UserCommit{ Commit: &git.Commit{ - CommitMessage: msg, + CommitMessage: git.CommitMessage{MessageRaw: msg}, }, }, }, } } - title, content := prepareNewPullRequestTitleContent(ci, nil) - assert.Equal(t, "head-branch", title) + // no commit + title, content := prepareNewPullRequestTitleContent(ci, nil, setting.RepoPRTitleSourceAuto) + assert.Equal(t, "Head branch", title) assert.Empty(t, content) - title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-only")}) - assert.Equal(t, "title-only", title) + title, content = prepareNewPullRequestTitleContent(ci, nil, setting.RepoPRTitleSourceFirstCommit) + assert.Equal(t, "Head branch", title) assert.Empty(t, content) - title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-" + strings.Repeat("a", 255))}) - assert.Equal(t, "title-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", title) - assert.Equal(t, "…aaaaaaaaa\n", content) - - title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title\nbody")}) - assert.Equal(t, "title", title) + // single commit + title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("single-commit-title\nbody")}, setting.RepoPRTitleSourceAuto) + assert.Equal(t, "single-commit-title", title) assert.Equal(t, "body", content) - title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("a\xf0\xf0\xf0\nb\xf0\xf0\xf0")}) - assert.Equal(t, "a?", title) // FIXME: GIT-COMMIT-MESSAGE-ENCODING: "title" doesn't use the same charset converting logic as "content" - assert.Equal(t, "b"+string(utf8.RuneError)+string(utf8.RuneError), content) + title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("single-commit-title\nbody")}, setting.RepoPRTitleSourceFirstCommit) + assert.Equal(t, "single-commit-title", title) + assert.Equal(t, "body", content) - title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{ + // multiple commits + commits := []*git_model.SignCommitWithStatuses{ // ordered from newest to oldest mockCommit("title2\nbody2"), mockCommit("title1\nbody1"), - }) + } + title, content = prepareNewPullRequestTitleContent(ci, commits, setting.RepoPRTitleSourceAuto) + assert.Equal(t, "Head branch", title) + assert.Empty(t, content) + + title, content = prepareNewPullRequestTitleContent(ci, commits, setting.RepoPRTitleSourceFirstCommit) assert.Equal(t, "title1", title) assert.Empty(t, content) + + // title string handling + title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-" + strings.Repeat("a", 255))}, setting.RepoPRTitleSourceFirstCommit) + assert.Equal(t, "title-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", title) + assert.Equal(t, "…aaaaaaaaa\n", content) + + title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title \xf0\nbody \xf0")}, setting.RepoPRTitleSourceFirstCommit) + assert.Equal(t, "title ð", title) + assert.Equal(t, "body ð", content) +} + +func TestAutoTitleFromBranchName(t *testing.T) { + cases := []struct { + branch string + want string + }{ + {"fix/the-bug", "Fix/the bug"}, + {"Already-Capitalized", "Already capitalized"}, + {"ALL-CAPS-BRANCH", "All caps branch"}, + {"FixHTMLBug", "Fix html bug"}, + {"MixedCase-Name", "Mixed case name"}, + {"fooBar-baz", "Foo bar baz"}, + {"foo/BAR", "Foo/bar"}, + {"_leading-underscore", "Leading underscore"}, + {"CamelCase", "Camel case"}, + {"foo--double-dash", "Foo double dash"}, + {"123-fix", "123 fix"}, + } + for _, c := range cases { + assert.Equal(t, c.want, autoTitleFromBranchName(c.branch), "branch: %q", c.branch) + } } diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go index 073d3d7420..7c74987d0d 100644 --- a/routers/web/repo/download.go +++ b/routers/web/repo/download.go @@ -7,45 +7,38 @@ package repo import ( "time" + auth_model "code.gitea.io/gitea/models/auth" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/lfs" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" ) +func checkDownloadTokenScope(ctx *context.Context) bool { + context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read) + return !ctx.Written() +} + // ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Time) error { if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { return nil } - dataRc, err := blob.DataAsync() + lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize) if err != nil { return err } - closed := false - defer func() { - if closed { - return - } - if err = dataRc.Close(); err != nil { - log.Error("ServeBlobOrLFS: Close: %v", err) - } - }() - pointer, _ := lfs.ReadPointer(dataRc) + pointer, _ := lfs.ReadPointerFromBuffer(lfsPointerBuf) if pointer.IsValid() { meta, _ := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid) if meta == nil { - if err = dataRc.Close(); err != nil { - log.Error("ServeBlobOrLFS: Close: %v", err) - } - closed = true return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified) } if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`, meta.UpdatedUnix.AsTimePtr()) { @@ -61,22 +54,14 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim } } - lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer) + lfsDataFile, err := lfs.ReadMetaObject(meta.Pointer) if err != nil { return err } - defer func() { - if err = lfsDataRc.Close(); err != nil { - log.Error("ServeBlobOrLFS: Close: %v", err) - } - }() - common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc) + defer lfsDataFile.Close() + httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath}) return nil } - if err = dataRc.Close(); err != nil { - log.Error("ServeBlobOrLFS: Close: %v", err) - } - closed = true return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified) } @@ -109,6 +94,10 @@ func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) { // SingleDownload download a file by repos path func SingleDownload(ctx *context.Context) { + if !checkDownloadTokenScope(ctx) { + return + } + blob, lastModified := getBlobForEntry(ctx) if blob == nil { return @@ -121,6 +110,10 @@ func SingleDownload(ctx *context.Context) { // SingleDownloadOrLFS download a file by repos path redirecting to LFS if necessary func SingleDownloadOrLFS(ctx *context.Context) { + if !checkDownloadTokenScope(ctx) { + return + } + blob, lastModified := getBlobForEntry(ctx) if blob == nil { return @@ -133,6 +126,10 @@ func SingleDownloadOrLFS(ctx *context.Context) { // DownloadByID download a file by sha1 ID func DownloadByID(ctx *context.Context) { + if !checkDownloadTokenScope(ctx) { + return + } + blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha")) if err != nil { if git.IsErrNotExist(err) { @@ -149,6 +146,10 @@ func DownloadByID(ctx *context.Context) { // DownloadByIDOrLFS download a file by sha1 ID taking account of LFS func DownloadByIDOrLFS(ctx *context.Context) { + if !checkDownloadTokenScope(ctx) { + return + } + blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha")) if err != nil { if git.IsErrNotExist(err) { diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 8fd98980c3..0aa3271683 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -319,7 +319,12 @@ func EditFile(ctx *context.Context) { } } - ctx.Data["CodeEditorConfig"] = getCodeEditorConfig(ctx, ctx.Repo.TreePath) + editorConfig := getCodeEditorConfigByEditorconfig(ctx, ctx.Repo.TreePath) + editorConfig.Autofocus = !isNewFile + if isNewFile { + editorConfig.Filename = "" + } + ctx.Data["CodeEditorConfig"] = editorConfig ctx.HTML(http.StatusOK, tplEditFile) } diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go index 1a01bfd5cb..111e0161f2 100644 --- a/routers/web/repo/editor_apply_patch.go +++ b/routers/web/repo/editor_apply_patch.go @@ -20,7 +20,7 @@ func NewDiffPatch(ctx *context.Context) { } ctx.Data["PageIsPatch"] = true - ctx.Data["CodeEditorConfig"] = CodeEditorConfig{} // not really editing a file, so no need to fill in the config + ctx.Data["CodeEditorConfig"] = CodeEditorConfig{Filename: "diff.patch"} ctx.HTML(http.StatusOK, tplPatchFile) } diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go index 605a35b100..f3cbc09827 100644 --- a/routers/web/repo/editor_cherry_pick.go +++ b/routers/web/repo/editor_cherry_pick.go @@ -6,7 +6,6 @@ package repo import ( "bytes" "net/http" - "strings" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" @@ -33,10 +32,10 @@ func CherryPick(ctx *context.Context) { if ctx.FormString("cherry-pick-type") == "revert" { ctx.Data["CherryPickType"] = "revert" ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha") - ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() + ctx.Data["commit_message"] = "revert " + cherryPickCommit.MessageUTF8() } else { ctx.Data["CherryPickType"] = "cherry-pick" - ctx.Data["commit_summary"], ctx.Data["commit_message"], _ = strings.Cut(cherryPickCommit.Message(), "\n") + ctx.Data["commit_summary"], ctx.Data["commit_message"] = cherryPickCommit.MessageTitle(), cherryPickCommit.MessageBody() } ctx.HTML(http.StatusOK, tplCherryPick) diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go index e1473a34b3..f23b2738e5 100644 --- a/routers/web/repo/editor_error.go +++ b/routers/web/repo/editor_error.go @@ -27,13 +27,13 @@ func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": message, "Summary": summary, - "Details": utils.SanitizeFlashErrorString(details), + "Details": utils.EscapeFlashErrorString(details), }) if err == nil { ctx.JSONError(flashError) } else { - log.Error("RenderToHTML: %v", err) - ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details)) + log.Error("RenderToHTML(%q, %q, %q), error: %v", message, summary, details, err) + ctx.JSONError("Unable to render error details, see server logs") // it should never happen } } diff --git a/routers/web/repo/editor_util.go b/routers/web/repo/editor_util.go index aca732ac70..7155b6e99d 100644 --- a/routers/web/repo/editor_util.go +++ b/routers/web/repo/editor_util.go @@ -65,27 +65,33 @@ func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePa return f(originTreePath, commit) } -// CodeEditorConfig is also used by frontend, defined in "codeeditor.ts" +// CodeEditorConfig is also used by frontend, defined in "codeeditor" module type CodeEditorConfig struct { - PreviewableExtensions []string `json:"previewable_extensions"` - LineWrapExtensions []string `json:"line_wrap_extensions"` - LineWrapOn bool `json:"line_wrap_on"` + Filename string `json:"filename"` // the base name, not full path + Autofocus bool `json:"autofocus"` + PreviewableExtensions []string `json:"previewableExtensions,omitempty"` + LineWrapExtensions []string `json:"lineWrapExtensions,omitempty"` + LineWrap bool `json:"lineWrap"` + Previewable bool `json:"previewable,omitempty"` - IndentStyle string `json:"indent_style"` - IndentSize int `json:"indent_size"` - TabWidth int `json:"tab_width"` - TrimTrailingWhitespace *bool `json:"trim_trailing_whitespace,omitempty"` + // the following can be read from .editorconfig if exists, or use default value + IndentStyle string `json:"indentStyle"` // in most cases, keep it empty by default, detected by the source code + IndentSize int `json:"indentSize"` + TabWidth int `json:"tabWidth"` + TrimTrailingWhitespace *bool `json:"trimTrailingWhitespace,omitempty"` } -func getCodeEditorConfig(ctx *context_service.Context, treePath string) (ret CodeEditorConfig) { +func getCodeEditorConfigByEditorconfig(ctx *context_service.Context, treePath string) CodeEditorConfig { + ret := CodeEditorConfig{Filename: path.Base(treePath)} ret.PreviewableExtensions = markup.PreviewableExtensions() ret.LineWrapExtensions = setting.Repository.Editor.LineWrapExtensions - ret.LineWrapOn = util.SliceContainsString(ret.LineWrapExtensions, path.Ext(treePath), true) + ret.LineWrap = util.SliceContainsString(ret.LineWrapExtensions, path.Ext(treePath), true) + ret.Previewable = util.SliceContainsString(ret.PreviewableExtensions, path.Ext(treePath), true) ec, _, err := ctx.Repo.GetEditorconfig() if err == nil { def, err := ec.GetDefinitionForFilename(treePath) if err == nil { - ret.IndentStyle = def.IndentStyle + ret.IndentStyle = util.IfZero(def.IndentStyle, ret.IndentStyle) ret.IndentSize, _ = strconv.Atoi(def.IndentSize) ret.TabWidth = def.TabWidth ret.TrimTrailingWhitespace = def.TrimTrailingWhitespace diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index fb9445aed0..fbfdcf2d51 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -22,7 +22,6 @@ import ( 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/git" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/gitrepo" @@ -181,34 +180,20 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler { } if repoExist { - // Because of special ref "refs/for" (agit) , need delay write permission check - if git.DefaultFeatures().SupportProcReceive { + // Only the main code repo accepts refs/for pushes, so wiki pushes must keep write checks. + if git.DefaultFeatures().SupportProcReceive && !isWiki { accessMode = perm.AccessModeRead } - taskID, isActionsUser := user_model.GetActionsUserTaskID(ctx.Doer) - if isActionsUser { - p, err := access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID) - if err != nil { - ctx.ServerError("GetActionsUserRepoPermission", err) - return nil - } + p, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) + if err != nil { + ctx.ServerError("GetDoerRepoPermission", err) + return nil + } - if !p.CanAccess(accessMode, unitType) { - ctx.PlainText(http.StatusNotFound, "Repository not found") - return nil - } - } else { - p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserRepoPermission", err) - return nil - } - - if !p.CanAccess(accessMode, unitType) { - ctx.PlainText(http.StatusNotFound, "Repository not found") - return nil - } + if !p.CanAccess(accessMode, unitType) { + ctx.PlainText(http.StatusNotFound, "Repository not found") + return nil } if !isPull && repo.IsMirror { diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 0fe703e150..1e0abd6ed2 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 7f8cc23a3f..ccf9a3749c 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -21,6 +21,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -31,185 +32,159 @@ import ( // NewComment create a comment for issue func NewComment(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.CreateCommentForm) issue := GetActionIssue(ctx) - if ctx.Written() { + if issue == nil { return } - if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) { - if log.IsTrace() { - if ctx.IsSigned { - issueType := "issues" - if issue.IsPull { - issueType = "pulls" - } - 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) - } else { - log.Trace("Permission Denied: Not logged in") - } - } - - ctx.HTTPError(http.StatusForbidden) - return - } - - if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { - ctx.JSONError(ctx.Tr("repo.issues.comment_on_locked")) - return - } - - var attachments []string - if setting.Attachment.Enabled { - attachments = form.Files - } - if ctx.HasError() { ctx.JSONError(ctx.GetErrMsg()) return } - var comment *issues_model.Comment - defer func() { - // Check if issue admin/poster changes the status of issue. - if (ctx.Repo.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. - var pr *issues_model.PullRequest + form := web.GetForm(ctx).(*forms.CreateCommentForm) + issueType := util.Iif(issue.IsPull, "pulls", "issues") - if form.Status == "reopen" && issue.IsPull { - pull := issue.PullRequest - var err error - pr, err = issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow) - if err != nil { - if !issues_model.IsErrPullRequestNotExist(err) { - ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked")) - return - } - } + 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 + } - // Regenerate patch and test conflict. - if pr == nil { - issue.PullRequest.HeadCommitID = "" - pull_service.StartPullRequestCheckImmediately(ctx, issue.PullRequest) - } + if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { + ctx.JSONError(ctx.Tr("repo.issues.comment_on_locked")) + return + } - // check whether the ref of PR in base repo is consistent with the head commit of head branch in the head repo - // get head commit of PR - if pull.Flow == issues_model.PullRequestFlowGithub { - prHeadRef := pull.GetGitHeadRefName() - if err := pull.LoadBaseRepo(ctx); err != nil { - ctx.ServerError("Unable to load base repo", err) - return - } - prHeadCommitID, err := gitrepo.GetFullCommitID(ctx, pull.BaseRepo, prHeadRef) - if err != nil { - ctx.ServerError("Get head commit Id of pr fail", err) - return - } + redirect := fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, issueType, issue.Index) + attachments := util.Iif(setting.Attachment.Enabled, form.Files, nil) - // get head commit of branch in the head repo - if err := pull.LoadHeadRepo(ctx); err != nil { - ctx.ServerError("Unable to load head repo", err) - return - } - if exist, _ := git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.BaseBranch); !exist { - // todo localize - ctx.JSONError("The origin branch is delete, cannot reopen.") - return - } - headBranchRef := git.RefNameFromBranch(pull.HeadBranch) - headBranchCommitID, err := gitrepo.GetFullCommitID(ctx, pull.HeadRepo, headBranchRef.String()) - if err != nil { - ctx.ServerError("Get head commit Id of head branch fail", err) - return - } - - err = pull.LoadIssue(ctx) - if err != nil { - ctx.ServerError("load the issue of pull request error", err) - return - } - - if prHeadCommitID != headBranchCommitID { - // force push to base repo - err := gitrepo.Push(ctx, pull.HeadRepo, pull.BaseRepo, git.PushOptions{ - Branch: pull.HeadBranch + ":" + prHeadRef, - Force: true, - Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo), - }) - if err != nil { - ctx.ServerError("force push error", err) - return - } - } - } - } - - if pr != nil { - ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index)) + // allow empty content if there are attachments + if form.Content != "" || len(attachments) > 0 { + comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments) + if err != nil { + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user")) } else { - if form.Status == "close" && !issue.IsClosed { - if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { - log.Error("CloseIssue: %v", err) - if issues_model.IsErrDependenciesLeft(err) { - if issue.IsPull { - ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked")) - } else { - ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked")) - } - return - } - } else { - if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil { - ctx.ServerError("stopTimerIfAvailable", err) - return - } - log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed) - } - } else if form.Status == "reopen" && issue.IsClosed { - if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { - log.Error("ReopenIssue: %v", err) + ctx.ServerError("CreateIssueComment", err) + } + return + } + // redirect to the comment's hashtag + redirect += "#" + comment.HashTag() + } else if form.Status == "" { + // if no status change (close, reopen), it is a plain comment, and content is required + // "approve/reject" are handled differently in SubmitReview + ctx.JSONError(ctx.Tr("repo.issues.comment_no_content")) + return + } + + // ATTENTION: From now on, do not use ctx.JSONError, don't return on user error, because the comment has been created. + // Always use ctx.Flash.Xxx and then redirect, then the message will be displayed + // TODO: need further refactoring to the code below + + // Check if doer can change the status of issue (close, reopen). + 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. + var branchOtherUnmergedPR *issues_model.PullRequest + var err error + if form.Status == "reopen" && issue.IsPull { + pull := issue.PullRequest + branchOtherUnmergedPR, err = issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow) + if err != nil { + if !issues_model.IsErrPullRequestNotExist(err) { + ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked")) + } + } + + if branchOtherUnmergedPR != nil { + ctx.Flash.Error(ctx.Tr("repo.pulls.open_unmerged_pull_exists", branchOtherUnmergedPR.Index)) + } else { + // Regenerate patch and test conflict. + issue.PullRequest.HeadCommitID = "" + pull_service.StartPullRequestCheckImmediately(ctx, issue.PullRequest) + } + + // check whether the ref of PR in base repo is consistent with the head commit of head branch in the head repo + // get head commit of PR + if branchOtherUnmergedPR != nil && pull.Flow == issues_model.PullRequestFlowGithub { + prHeadRef := pull.GetGitHeadRefName() + if err := pull.LoadBaseRepo(ctx); err != nil { + ctx.ServerError("Unable to load base repo", err) + return + } + prHeadCommitID, err := gitrepo.GetFullCommitID(ctx, pull.BaseRepo, prHeadRef) + if err != nil { + ctx.ServerError("Get head commit Id of pr fail", err) + return + } + + // get head commit of branch in the head repo + if err := pull.LoadHeadRepo(ctx); err != nil { + ctx.ServerError("Unable to load head repo", err) + return + } + if exist, _ := git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.BaseBranch); !exist { + ctx.Flash.Error("The origin branch is delete, cannot reopen.") + return + } + headBranchRef := git.RefNameFromBranch(pull.HeadBranch) + headBranchCommitID, err := gitrepo.GetFullCommitID(ctx, pull.HeadRepo, headBranchRef.String()) + if err != nil { + ctx.ServerError("Get head commit Id of head branch fail", err) + return + } + + err = pull.LoadIssue(ctx) + if err != nil { + ctx.ServerError("load the issue of pull request error", err) + return + } + + if prHeadCommitID != headBranchCommitID { + // force push to base repo + err := gitrepo.Push(ctx, pull.HeadRepo, pull.BaseRepo, git.PushOptions{ + Branch: pull.HeadBranch + ":" + prHeadRef, + Force: true, + Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo), + }) + if err != nil { + ctx.ServerError("force push error", err) + return } } } } - // Redirect to comment hashtag if there is any actual content. - typeName := "issues" - if issue.IsPull { - typeName = "pulls" + if form.Status == "close" && !issue.IsClosed { + if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { + log.Error("CloseIssue: %v", err) + if issues_model.IsErrDependenciesLeft(err) { + if issue.IsPull { + ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked")) + } else { + ctx.Flash.Error(ctx.Tr("repo.issues.dependency.issue_close_blocked")) + } + } + } else { + if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil { + ctx.ServerError("stopTimerIfAvailable", err) + return + } + log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed) + } + } else if form.Status == "reopen" && issue.IsClosed && branchOtherUnmergedPR == nil { + if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { + log.Error("ReopenIssue: %v", err) + ctx.Flash.Error("Unable to reopen.") + } } - if comment != nil { - ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag())) - } else { - ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index)) - } - }() + } // end if: handle close or reopen - // Fix #321: Allow empty comments, as long as we have attachments. - if len(form.Content) == 0 && len(attachments) == 0 { - return - } - - comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments) - if err != nil { - if errors.Is(err, user_model.ErrBlockedUser) { - ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user")) - } else { - ctx.ServerError("CreateIssueComment", err) - } - return - } - - log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID) + ctx.JSONRedirect(redirect) } // UpdateCommentContent change comment of issue's content @@ -230,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 } @@ -314,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() { @@ -349,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 23cedfcb80..01fb139c1a 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_dependency.go b/routers/web/repo/issue_dependency.go index bb2dc5b0fd..b5b252f8a1 100644 --- a/routers/web/repo/issue_dependency.go +++ b/routers/web/repo/issue_dependency.go @@ -55,9 +55,9 @@ func AddDependency(ctx *context.Context) { return } // Can ctx.Doer read issues in the dep repo? - depRepoPerm, err := access_model.GetUserRepoPermission(ctx, dep.Repo, ctx.Doer) + depRepoPerm, err := access_model.GetDoerRepoPermission(ctx, dep.Repo, ctx.Doer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.ServerError("GetDoerRepoPermission", err) return } if !depRepoPerm.CanReadIssuesOrPulls(dep.IsPull) { diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index 83ef515bde..fc891ac8e4 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -38,6 +38,14 @@ func retrieveProjectsForIssueList(ctx *context.Context, repo *repo_model.Reposit ctx.Data["OpenProjects"], ctx.Data["ClosedProjects"] = retrieveProjectsInternal(ctx, repo) } +// parseProjectIDsFromQuery parses the comma-separated `project` (preferred) or `projects` +// query parameter into a slice of int64 IDs. +func parseProjectIDsFromQuery(ctx *context.Context) []int64 { + // FIXME: ISSUE-MULTIPLE-PROJECTS-FILTER: no multiple project filter support yet + // Although here parses the project parameter as a slice, the "search" logic is wrong + return ctx.FormStringInt64s("project") +} + // SearchIssues searches for issues across the repositories that the user has access to func SearchIssues(ctx *context.Context) { before, since, err := context.GetQueryBeforeSince(ctx.Base) @@ -156,10 +164,7 @@ func SearchIssues(ctx *context.Context) { } } - projectID := optional.None[int64]() - if v := ctx.FormInt64("project"); v > 0 { - projectID = optional.Some(v) - } + includedProjectIDs := parseProjectIDsFromQuery(ctx) // this api is also used in UI, // so the default limit is set to fit UI needs @@ -182,7 +187,7 @@ func SearchIssues(ctx *context.Context) { IsClosed: isClosed, IncludedAnyLabelIDs: includedAnyLabels, MilestoneIDs: includedMilestones, - ProjectID: projectID, + ProjectIDs: includedProjectIDs, SortBy: issue_indexer.SortByCreatedDesc, } @@ -298,11 +303,6 @@ func SearchRepoIssuesJSON(ctx *context.Context) { } } - projectID := optional.None[int64]() - if v := ctx.FormInt64("project"); v > 0 { - projectID = optional.Some(v) - } - isPull := optional.None[bool]() switch ctx.FormString("type") { case "pulls": @@ -330,13 +330,20 @@ func SearchRepoIssuesJSON(ctx *context.Context) { Page: ctx.FormInt("page"), PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), }, - Keyword: keyword, - RepoIDs: []int64{ctx.Repo.Repository.ID}, - IsPull: isPull, - IsClosed: isClosed, - ProjectID: projectID, - SortBy: issue_indexer.SortByCreatedDesc, + Keyword: keyword, + RepoIDs: []int64{ctx.Repo.Repository.ID}, + IsPull: isPull, + IsClosed: isClosed, + SortBy: issue_indexer.SortByCreatedDesc, } + + projectIDs := parseProjectIDsFromQuery(ctx) + if len(projectIDs) == 1 && projectIDs[0] == -1 { + searchOpt.NoProjectOnly = true + } else if len(projectIDs) > 0 { + searchOpt.ProjectIDs = projectIDs + } + if since != 0 { searchOpt.UpdatedAfterUnix = optional.Some(since) } @@ -467,7 +474,7 @@ func renderMilestones(ctx *context.Context) { ctx.Data["ClosedMilestones"] = closedMilestones } -func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) { +func prepareIssueFilterAndList(ctx *context.Context, milestoneID int64, projectIDs []int64, isPullOption optional.Option[bool]) { var err error viewType := ctx.FormString("type") sortType := ctx.FormString("sort") @@ -520,7 +527,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6 RepoIDs: []int64{repo.ID}, LabelIDs: preparedLabelFilter.SelectedLabelIDs, MilestoneIDs: mileIDs, - ProjectID: projectID, + ProjectIDs: projectIDs, AssigneeID: assigneeID, MentionedID: mentionedID, PosterID: posterUserID, @@ -529,6 +536,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6 IsPull: isPullOption, IssueIDs: nil, } + if keyword != "" { keywordMatchedIssueIDs, _, err = issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, statsOpts)) if err != nil { @@ -600,7 +608,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6 ReviewRequestedID: reviewRequestedID, ReviewedID: reviewedID, MilestoneIDs: mileIDs, - ProjectID: projectID, + ProjectIDs: projectIDs, IsClosed: isShowClosed, IsPull: isPullOption, LabelIDs: preparedLabelFilter.SelectedLabelIDs, @@ -641,7 +649,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 +708,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 @@ -708,7 +716,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6 ctx.Data["ViewType"] = viewType ctx.Data["SortType"] = sortType ctx.Data["MilestoneID"] = milestoneID - ctx.Data["ProjectID"] = projectID + ctx.Data["ProjectIDs"] = projectIDs ctx.Data["AssigneeID"] = assigneeID ctx.Data["PosterUsername"] = posterUsername ctx.Data["Keyword"] = keyword @@ -749,7 +757,9 @@ func Issues(ctx *context.Context) { ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) } - prepareIssueFilterAndList(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList)) + projectIDs := parseProjectIDsFromQuery(ctx) + + prepareIssueFilterAndList(ctx, ctx.FormInt64("milestone"), projectIDs, optional.Some(isPullList)) if ctx.Written() { return } @@ -759,7 +769,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 1393f62fa5..d442f2804f 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") @@ -121,11 +121,10 @@ func NewIssue(ctx *context.Context) { } pageMetaData.MilestonesData.SelectedMilestoneID = ctx.FormInt64("milestone") - pageMetaData.ProjectsData.SelectedProjectID = ctx.FormInt64("project") - if pageMetaData.ProjectsData.SelectedProjectID > 0 { - if len(ctx.Req.URL.Query().Get("project")) > 0 { - ctx.Data["redirect_after_creation"] = "project" - } + + pageMetaData.SetSelectedProjectIDs(parseProjectIDsFromQuery(ctx)) + if len(pageMetaData.ProjectsData.SelectedProjectIDs) == 1 { + ctx.Data["redirect_after_creation"] = "project" } tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) @@ -146,7 +145,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. @@ -172,7 +171,7 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) templat flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"), "Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)), - "Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")), + "Details": utils.EscapeFlashErrorString(strings.Join(lines, "\n")), }) if err != nil { log.Debug("render flash error: %v", err) @@ -239,8 +238,9 @@ func toSet[ItemType any, KeyType comparable](slice []ItemType, keyFunc func(Item // ValidateRepoMetasForNewIssue check and returns repository's meta information func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueForm, isPull bool) (ret struct { - LabelIDs, AssigneeIDs []int64 - MilestoneID, ProjectID int64 + LabelIDs, AssigneeIDs []int64 + MilestoneID int64 + ProjectIDs []int64 Reviewers []*user_model.User TeamReviewers []*organization.Team @@ -251,7 +251,7 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo return ret } - inputLabelIDs, _ := base.StringsToInt64s(strings.Split(form.LabelIDs, ",")) + inputLabelIDs := ctx.FormStringInt64s("label_ids") candidateLabels := toSet(pageMetaData.LabelsData.AllLabels, func(label *issues_model.Label) int64 { return label.ID }) if len(inputLabelIDs) > 0 && !candidateLabels.Contains(inputLabelIDs...) { ctx.NotFound(nil) @@ -267,13 +267,8 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo } pageMetaData.MilestonesData.SelectedMilestoneID = form.MilestoneID - allProjects := append(slices.Clone(pageMetaData.ProjectsData.OpenProjects), pageMetaData.ProjectsData.ClosedProjects...) - candidateProjects := toSet(allProjects, func(project *project_model.Project) int64 { return project.ID }) - if form.ProjectID > 0 && !candidateProjects.Contains(form.ProjectID) { - ctx.NotFound(nil) - return ret - } - pageMetaData.ProjectsData.SelectedProjectID = form.ProjectID + inputProjectIDs := ctx.FormStringInt64s("project_ids") + pageMetaData.SetSelectedProjectIDs(inputProjectIDs) // prepare assignees candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID }) @@ -318,7 +313,8 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo } } - ret.LabelIDs, ret.AssigneeIDs, ret.MilestoneID, ret.ProjectID = inputLabelIDs, inputAssigneeIDs, form.MilestoneID, form.ProjectID + // Return only the validated IDs. + ret.LabelIDs, ret.AssigneeIDs, ret.MilestoneID, ret.ProjectIDs = inputLabelIDs, inputAssigneeIDs, form.MilestoneID, inputProjectIDs ret.Reviewers, ret.TeamReviewers = reviewers, teamReviewers return ret } @@ -326,33 +322,25 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo // NewIssuePost response for creating new issue func NewIssuePost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateIssueForm) - ctx.Data["Title"] = ctx.Tr("repo.issues.new") - ctx.Data["PageIsIssueList"] = true - ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) - ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes - ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled - upload.AddUploadContext(ctx, "comment") - var ( - repo = ctx.Repo.Repository - attachments []string - ) + repo := ctx.Repo.Repository validateRet := ValidateRepoMetasForNewIssue(ctx, *form, false) if ctx.Written() { return } - labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID + labelIDs, assigneeIDs, milestoneID, projectIDs := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectIDs - if projectID > 0 { - if !ctx.Repo.CanRead(unit.TypeProjects) { + if len(projectIDs) > 0 { + 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 } } + var attachments []string if setting.Attachment.Enabled { attachments = form.Files } @@ -385,7 +373,7 @@ func NewIssuePost(ctx *context.Context) { Ref: form.Ref, } - if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectID); err != nil { + if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectIDs); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.HTTPError(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) } else if errors.Is(err, user_model.ErrBlockedUser) { @@ -397,8 +385,9 @@ func NewIssuePost(ctx *context.Context) { } log.Trace("Issue created: %d/%d", repo.ID, issue.ID) - if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 { - project, err := project_model.GetProjectByID(ctx, projectID) + if ctx.FormString("redirect_after_creation") == "project" && len(projectIDs) > 0 { + // When issue is in multiple projects, redirect to first project from form order. + project, err := project_model.GetProjectByID(ctx, projectIDs[0]) if err == nil { if project.Type == project_model.TypeOrganization { ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID)) diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index 719b485bc5..428171dd0e 100644 --- a/routers/web/repo/issue_page_meta.go +++ b/routers/web/repo/issue_page_meta.go @@ -33,10 +33,18 @@ type issueSidebarAssigneesData struct { CandidateAssignees []*user_model.User } +type issueSidebarProjectCardData struct { + Project *project_model.Project + Columns []*project_model.Column + SelectedColumn *project_model.Column +} + type issueSidebarProjectsData struct { - SelectedProjectID int64 - OpenProjects []*project_model.Project - ClosedProjects []*project_model.Project + SelectedProjectIDs []int64 + ProjectCards []*issueSidebarProjectCardData + + OpenProjects []*project_model.Project + ClosedProjects []*project_model.Project } type IssuePageMetaData struct { @@ -92,12 +100,17 @@ func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository return data } + data.retrieveProjectData(ctx) + if ctx.Written() { + return data + } + // TODO: the issue/pull permissions are quite complex and unclear // A reader could create an issue/PR with setting some meta (eg: assignees from issue template, reviewers, target branch) // 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 } @@ -158,10 +171,80 @@ func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) { ctx.Data["Assignees"] = d.AssigneesData.CandidateAssignees } -func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) { - if d.Issue != nil && d.Issue.Project != nil { - d.ProjectsData.SelectedProjectID = d.Issue.Project.ID +func (d *IssuePageMetaData) retrieveProjectCardsForExistingIssue(ctx *context.Context) { + if err := d.Issue.LoadProjects(ctx); err != nil { + ctx.ServerError("LoadProjects", err) + return } + + // Load column mappings for all projects + projectColumnMap, err := d.Issue.ProjectColumnMap(ctx) + if err != nil { + ctx.ServerError("ProjectColumnMap", err) + return + } + + // Build project cards for each project + d.ProjectsData.ProjectCards = make([]*issueSidebarProjectCardData, 0, len(d.Issue.Projects)) + for _, project := range d.Issue.Projects { + columns, err := project.GetColumns(ctx) + if err != nil { + ctx.ServerError("GetProjectColumns", err) + return + } + + var selectedColumn *project_model.Column + columnID := projectColumnMap[project.ID] + for _, col := range columns { + if col.ID == columnID { + selectedColumn = col + break + } + } + + if selectedColumn == nil { + selectedColumn, err = project.MustDefaultColumn(ctx) + if err != nil { + ctx.ServerError("MustDefaultColumn", err) + return + } + } + d.ProjectsData.ProjectCards = append(d.ProjectsData.ProjectCards, &issueSidebarProjectCardData{ + Project: project, + Columns: columns, + SelectedColumn: selectedColumn, + }) + } + d.ProjectsData.SelectedProjectIDs = make([]int64, 0, len(d.ProjectsData.ProjectCards)) + for _, card := range d.ProjectsData.ProjectCards { + d.ProjectsData.SelectedProjectIDs = append(d.ProjectsData.SelectedProjectIDs, card.Project.ID) + } +} + +func (d *IssuePageMetaData) retrieveProjectData(ctx *context.Context) { + if d.Issue == nil { + return + } + d.retrieveProjectCardsForExistingIssue(ctx) +} + +func (d *IssuePageMetaData) SetSelectedProjectIDs(ids []int64) { + allProjects := map[int64]*project_model.Project{} + for _, p := range d.ProjectsData.OpenProjects { + allProjects[p.ID] = p + } + for _, p := range d.ProjectsData.ClosedProjects { + allProjects[p.ID] = p + } + for _, id := range ids { + if project, ok := allProjects[id]; ok { + d.ProjectsData.ProjectCards = append(d.ProjectsData.ProjectCards, &issueSidebarProjectCardData{Project: project}) + } + } + d.ProjectsData.SelectedProjectIDs = ids +} + +func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) { d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository) } diff --git a/routers/web/repo/issue_suggestions.go b/routers/web/repo/issue_suggestions.go index 9ef3942504..592cc7b1d5 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 b9ed059fde..e93a3107a3 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 2cd8be4533..93f24637da 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -18,7 +18,6 @@ import ( "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" project_model "code.gitea.io/gitea/models/project" - pull_model "code.gitea.io/gitea/models/pull" "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" @@ -29,7 +28,7 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" @@ -57,7 +56,7 @@ func roleDescriptor(ctx *context.Context, repo *repo_model.Repository, poster *u // Guess the role of the poster in the repo by permission perm, hasPermCache := permsCache[poster.ID] if !hasPermCache { - perm, err = access_model.GetUserRepoPermission(ctx, repo, poster) + perm, err = access_model.GetIndividualUserRepoPermission(ctx, repo, poster) if err != nil { return roleDesc, err } @@ -118,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) @@ -145,9 +131,9 @@ func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.Depende perm = existPerm } else { var err error - perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer) + perm, err = access_model.GetDoerRepoPermission(ctx, &blocker.Repository, ctx.Doer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.ServerError("GetDoerRepoPermission", err) return nil, nil } repoPerms[blocker.RepoID] = perm @@ -192,7 +178,7 @@ func filterXRefComments(ctx *context.Context, issue *issues_model.Issue) error { if err != nil { return err } - perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, ctx.Doer) + perm, err := access_model.GetDoerRepoPermission(ctx, c.RefRepo, ctx.Doer) if err != nil { return err } @@ -350,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") @@ -380,6 +366,7 @@ func ViewIssue(ctx *context.Context) { } pageMetaData.LabelsData.SetSelectedLabels(issue.Labels) + prViewInfo := newPullRequestViewInfo() prepareFuncs := []func(*context.Context, *issues_model.Issue){ prepareIssueViewContent, prepareIssueViewCommentsAndSidebarParticipants, @@ -387,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() { @@ -403,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() @@ -427,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) @@ -443,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" @@ -488,26 +485,56 @@ 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) prepareMergeBoxCommitSigning(ctx *context.Context) { + pull := prInfo.issue.PullRequest + data := prInfo.MergeBoxData + + pb := prInfo.ProtectedBranchRule + data.requireSigned = pb != nil && pb.RequireSignedCommits + + wontSignReason := "" if ctx.Doer != nil { sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, ctx.Repo.GitRepo) - ctx.Data["WillSign"] = sign - ctx.Data["SigningKeyMergeDisplay"] = asymkey_model.GetDisplaySigningKey(key) + data.willSign = sign + data.signingKeyMergeDisplay = asymkey_model.GetDisplaySigningKey(key) if err != nil { if asymkey_service.IsErrWontSign(err) { - ctx.Data["WontSignReason"] = err.(*asymkey_service.ErrWontSign).Reason + wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) } else { - ctx.Data["WontSignReason"] = "error" + wontSignReason = "error" log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err) } } - } else { - ctx.Data["WontSignReason"] = "not_signed_in" + } + + if data.willSign { + prInfo.MergeBoxData.infoMergePrompts.AddInfoItem( + svg.RenderHTML("octicon-lock", 16, "tw-text-green"), + ctx.Locale.Tr("repo.signing.will_sign", data.signingKeyMergeDisplay), + ) + } + + if !data.requireSigned { + if wontSignReason != "" { + data.infoMergePrompts.AddInfoItem( + svg.RenderHTML("octicon-unlock"), + ctx.Locale.Tr("repo.signing.wont_sign."+wontSignReason), + ) + } + return + } + + if data.requireSigned && !data.willSign { + data.infoProtectionBlockers.AddErrorItem( + svg.RenderHTML("octicon-x"), + ctx.Locale.Tr("repo.pulls.require_signed_wont_sign"), + ) + if wontSignReason != "" { + data.infoProtectionBlockers.AddInfoItem( + svg.RenderHTML("octicon-unlock"), + ctx.Locale.Tr("repo.signing.wont_sign."+wontSignReason), + ) + } } } @@ -558,14 +585,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,7 +603,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) { @@ -769,7 +793,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 @@ -781,14 +805,14 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue } else if comment.Type == issues_model.CommentTypeAddTimeManual || comment.Type == issues_model.CommentTypeStopTracking || comment.Type == issues_model.CommentTypeDeleteTimeManual { - // drop error since times could be pruned from DB.. + // drop error since times could be pruned from DB _ = comment.LoadTime(ctx) if comment.Content != "" { // Content before v1.21 did store the formatted string instead of seconds, // so "|" is used as delimiter to mark the new format if comment.Content[0] != '|' { // handle old time comments that have formatted text stored - comment.RenderedContent = templates.SanitizeHTML(comment.Content) + comment.RenderedContent = markup.Sanitize(comment.Content) comment.Content = "" } else { // else it's just a duration in seconds to pass on to the frontend @@ -827,27 +851,35 @@ 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") } pull := issue.PullRequest - pull.Issue = issue + data := &pullMergeBoxData{} + prInfo.MergeBoxData = data + canDelete := false - allowMerge := false canWriteToHeadRepo := false pull_service.StartPullRequestCheckOnView(ctx, pull) + if !prInfo.IsPullRequestBroken { + data.ShowUpdatePullInfo = pull.CommitsBehind > 0 && !issue.IsClosed && !pull.IsChecking() && !pull.IsFilesConflicted() && !prInfo.IsPullRequestBroken + prInfo.preparePullUpdateActions(ctx) + if ctx.Written() { + return + } + } + if ctx.IsSigned { if err := pull.LoadHeadRepo(ctx); err != nil { log.Error("LoadHeadRepo: %v", err) } else if pull.HeadRepo != nil { - perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer) + perm, err := access_model.GetDoerRepoPermission(ctx, pull.HeadRepo, ctx.Doer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.ServerError("GetDoerRepoPermission", err) return } if perm.CanWrite(unit.TypeCode) { @@ -867,15 +899,15 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss if err := pull.LoadBaseRepo(ctx); err != nil { log.Error("LoadBaseRepo: %v", err) } - perm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, ctx.Doer) + perm, err := access_model.GetDoerRepoPermission(ctx, pull.BaseRepo, ctx.Doer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.ServerError("GetDoerRepoPermission", err) return } if !canWriteToHeadRepo { // maintainers maybe allowed to push to head repo even if they can't write to it canWriteToHeadRepo = pull.AllowMaintainerEdit && perm.CanWrite(unit.TypeCode) } - allowMerge, err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer) + data.hasPermToMerge, err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer) if err != nil { ctx.ServerError("IsUserAllowedToMerge", err) return @@ -887,111 +919,166 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss } } - ctx.Data["PullMergeBoxReloadingInterval"] = util.Iif(pull != nil && pull.IsChecking(), 2000, 0) - ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo - ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo - ctx.Data["AllowMerge"] = allowMerge + data.ReloadingInterval = util.Iif(pull.IsChecking(), 2000, 0) + data.ShowMergeInstructions = canWriteToHeadRepo + data.ShowPullCommands = pull.HeadRepo != nil && !pull.HasMerged && !issue.IsClosed - prUnit, err := issue.Repo.GetUnit(ctx, unit.TypePullRequests) - if err != nil { - ctx.ServerError("GetUnit", err) - return - } - prConfig := prUnit.PullRequestsConfig() - - ctx.Data["AutodetectManualMerge"] = prConfig.AutodetectManualMerge - - var mergeStyle repo_model.MergeStyle - // Check correct values and select default - if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok || - !prConfig.IsMergeStyleAllowed(ms) { - if prConfig.IsMergeStyleAllowed(prConfig.DefaultMergeStyle) && !ok { - mergeStyle = prConfig.DefaultMergeStyle - } else if prConfig.AllowMerge { - mergeStyle = repo_model.MergeStyleMerge - } else if prConfig.AllowRebase { - mergeStyle = repo_model.MergeStyleRebase - } else if prConfig.AllowRebaseMerge { - mergeStyle = repo_model.MergeStyleRebaseMerge - } else if prConfig.AllowSquash { - mergeStyle = repo_model.MergeStyleSquash - } else if prConfig.AllowFastForwardOnly { - mergeStyle = repo_model.MergeStyleFastForwardOnly - } else if prConfig.AllowManualMerge { - mergeStyle = repo_model.MergeStyleManuallyMerged - } - } - - ctx.Data["MergeStyle"] = mergeStyle - - defaultMergeMessage, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle) - if err != nil { - ctx.ServerError("GetDefaultMergeMessage", err) - return - } - ctx.Data["DefaultMergeMessage"] = defaultMergeMessage - ctx.Data["DefaultMergeBody"] = defaultMergeBody - - defaultSquashMergeMessage, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash) - if err != nil { - ctx.ServerError("GetDefaultSquashMergeMessage", err) - return - } - 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) + prInfo.prepareMergeBoxProtectionChecks(ctx) + if ctx.Written() { return } + prInfo.prepareMergeBoxCommitSigning(ctx) + if ctx.Written() { + return + } + + prInfo.prepareMergeBoxDeleteBranch(ctx, canDelete) + if ctx.Written() { + return + } + + prConfig := issue.Repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() + data.AutodetectManualMerge = prConfig.AutodetectManualMerge + + // 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 = data.enableStatusCheck && !data.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 + + data.canBypassProtection = isRepoAdmin + data.canBypassProtectionAsAdmin = isRepoAdmin + if ctx.IsSigned && prInfo.ProtectedBranchRule != nil { + data.canBypassProtection = git_model.CanBypassBranchProtection(ctx, prInfo.ProtectedBranchRule, ctx.Doer, isRepoAdmin) + data.canBypassProtectionAsAdmin = isRepoAdmin && !prInfo.ProtectedBranchRule.BlockAdminMergeOverride + } + + // CanMergeNow means: if the doer has write permission, whether the PR can be merged now + data.canMergeNow = (!data.hasOverridableBlockers || data.canBypassProtection) && // status checks are satisfied + (!data.requireSigned || data.willSign) // signing requirement is satisfied + + prInfo.prepareMergeBoxFormProps(ctx) + prInfo.prepareMergeBoxInfoItems(ctx) + prInfo.prepareMergeBoxIconColor() + + ctx.Data["PullMergeBoxData"] = prInfo.MergeBoxData +} + +func (prInfo *pullRequestViewInfo) preparePullUpdateActions(ctx *context.Context) { + pull := prInfo.issue.PullRequest + data := prInfo.MergeBoxData + userUpdateStyles, err := pull_service.CheckUserAllowedToUpdate(ctx, pull, ctx.Doer) + if err != nil { + ctx.ServerError("IsUserAllowedToUpdate", err) + return + } + if !userUpdateStyles.MergeAllowed && !userUpdateStyles.RebaseAllowed { + return + } + + issueLink := prInfo.issue.Link() + mergeAction := &pullUpdateAction{ + URL: issueLink + "/update?style=merge", + Text: ctx.Tr("repo.pulls.update_branch"), + } + rebaseAction := &pullUpdateAction{ + URL: issueLink + "/update?style=rebase", + Text: ctx.Tr("repo.pulls.update_branch_rebase"), + } + + if userUpdateStyles.MergeAllowed { + data.UpdateStyleOptions = append(data.UpdateStyleOptions, mergeAction) + } + if userUpdateStyles.RebaseAllowed { + data.UpdateStyleOptions = append(data.UpdateStyleOptions, rebaseAction) + } + + if userUpdateStyles.DefaultUpdateStyle == repo_model.UpdateStyleRebase && userUpdateStyles.RebaseAllowed { + data.UpdatePrimaryAction = rebaseAction + } else if userUpdateStyles.DefaultUpdateStyle == repo_model.UpdateStyleMerge && userUpdateStyles.MergeAllowed { + data.UpdatePrimaryAction = mergeAction + } else { + data.UpdatePrimaryAction = data.UpdateStyleOptions[0] + } + data.UpdatePrimaryAction.Selected = true +} + +func (prInfo *pullRequestViewInfo) prepareMergeBoxProtectionChecks(ctx *context.Context) { + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, prInfo.issue.PullRequest.BaseBranch) + if err != nil { + ctx.ServerError("GetFirstMatchProtectedBranchRule", err) + return + } 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) - 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 + pb.Repo = prInfo.issue.PullRequest.BaseRepo + prInfo.ProtectedBranchRule = pb } - preparePullViewSigning(ctx, issue) + prInfo.prepareMergeBoxStatusCheckData(ctx) if ctx.Written() { return } - preparePullViewDeleteBranch(ctx, issue, canDelete) + prInfo.prepareMergeBoxProtectedRules(ctx) if ctx.Written() { return } +} - stillCanManualMerge := func() bool { - if pull.HasMerged || issue.IsClosed || !ctx.IsSigned { - return false - } - if pull.CanAutoMerge() || pull.IsWorkInProgress(ctx) || pull.IsChecking() { - return false - } - if allowMerge && prConfig.AllowManualMerge { - return true - } - - return false +func (prInfo *pullRequestViewInfo) prepareMergeBoxProtectedRules(ctx *context.Context) { + pb := prInfo.ProtectedBranchRule + if pb == nil { + return } - ctx.Data["StillCanManualMerge"] = stillCanManualMerge() + pull := prInfo.issue.PullRequest + data := prInfo.MergeBoxData - // Check if there is a pending pr merge - ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = pull_model.GetScheduledMergeByPullID(ctx, pull.ID) - if err != nil { - ctx.ServerError("GetScheduledMergeByPullID", err) - return + data.isBlockedByApprovals = !issues_model.HasEnoughApprovals(ctx, pb, pull) + if data.isBlockedByApprovals { + grantedApprovals := issues_model.GetGrantedApprovalsCount(ctx, pb, pull) + blockerInfo := ctx.Locale.Tr("repo.pulls.blocked_by_approvals", grantedApprovals, pb.RequiredApprovals) + if pb.EnableApprovalsWhitelist { + blockerInfo = ctx.Locale.Tr("repo.pulls.blocked_by_approvals_whitelisted", grantedApprovals, pb.RequiredApprovals) + } + data.infoProtectionBlockers.AddErrorItem(svg.RenderHTML("octicon-x"), blockerInfo) + } + + data.isBlockedByRejection = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull) + if data.isBlockedByRejection { + data.infoProtectionBlockers.AddErrorItem(svg.RenderHTML("octicon-x"), ctx.Locale.Tr("repo.pulls.blocked_by_rejection")) + } + + data.isBlockedByOfficialReviewRequests = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull) + if data.isBlockedByOfficialReviewRequests { + data.infoProtectionBlockers.AddErrorItem(svg.RenderHTML("octicon-x"), ctx.Locale.Tr("repo.pulls.blocked_by_official_review_requests")) + } + + data.isBlockedByOutdatedBranch = issues_model.MergeBlockedByOutdatedBranch(pb, pull) + if data.isBlockedByOutdatedBranch { + data.infoProtectionBlockers.AddErrorItem(svg.RenderHTML("octicon-x"), ctx.Locale.Tr("repo.pulls.blocked_by_outdated_branch")) + } + + data.isBlockedByChangedProtectedFiles = len(pull.ChangedProtectedFiles) != 0 + if data.isBlockedByChangedProtectedFiles { + detailItems := escapeStringSliceToHTML(pull.ChangedProtectedFiles) + data.infoProtectionBlockers.AddErrorItem( + svg.RenderHTML("octicon-x"), + ctx.Locale.TrN(len(pull.ChangedProtectedFiles), "repo.pulls.blocked_by_changed_protected_files_1", "repo.pulls.blocked_by_changed_protected_files_n"), + detailItems, + ) } } diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go index dfa3491786..abb2a81d9e 100644 --- a/routers/web/repo/issue_watch.go +++ b/routers/web/repo/issue_watch.go @@ -5,7 +5,6 @@ package repo import ( "net/http" - "strconv" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/log" @@ -24,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" @@ -46,12 +45,7 @@ func IssueWatch(ctx *context.Context) { return } - watch, err := strconv.ParseBool(ctx.Req.PostFormValue("watch")) - if err != nil { - ctx.ServerError("watch is not bool", err) - return - } - + watch := ctx.FormBool("watch") if err := issues_model.CreateOrUpdateIssueWatch(ctx, ctx.Doer.ID, issue.ID, watch); err != nil { ctx.ServerError("CreateOrUpdateIssueWatch", err) return diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go index c7c9da498b..ee525a0db7 100644 --- a/routers/web/repo/middlewares.go +++ b/routers/web/repo/middlewares.go @@ -9,6 +9,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/gitdiff" @@ -80,6 +81,14 @@ func SetWhitespaceBehavior(ctx *context.Context) { } } +func GetWhitespaceBehavior(ctx *context.Context) string { + behavior, ok := ctx.Data["WhitespaceBehavior"].(string) + if !ok { + setting.PanicInDevOrTesting("WhitespaceBehavior is not set in context data or is not a string") + } + return behavior +} + // SetShowOutdatedComments set the show outdated comments option as context variable func SetShowOutdatedComments(ctx *context.Context) { showOutdatedCommentsValue := ctx.FormString("show-outdated") @@ -95,3 +104,11 @@ func SetShowOutdatedComments(ctx *context.Context) { } ctx.Data["ShowOutdatedComments"], _ = strconv.ParseBool(showOutdatedCommentsValue) } + +func GetShowOutdatedComments(ctx *context.Context) bool { + show, ok := ctx.Data["ShowOutdatedComments"].(bool) + if !ok { + setting.PanicInDevOrTesting("ShowOutdatedComments is not set in context data or is not a bool") + } + return show +} diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index b928be2867..759b9910d8 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -238,7 +238,7 @@ func DeleteMilestone(ctx *context.Context) { // MilestoneIssuesAndPulls lists all the issues and pull requests of the milestone func MilestoneIssuesAndPulls(ctx *context.Context) { milestoneID := ctx.PathParamInt64("id") - projectID := ctx.FormInt64("project") + projectIDs := parseProjectIDsFromQuery(ctx) milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) if err != nil { if issues_model.IsErrMilestoneNotExist(err) { @@ -260,13 +260,13 @@ func MilestoneIssuesAndPulls(ctx *context.Context) { ctx.Data["Title"] = milestone.Name ctx.Data["Milestone"] = milestone - prepareIssueFilterAndList(ctx, milestoneID, projectID, optional.None[bool]()) + prepareIssueFilterAndList(ctx, milestoneID, projectIDs, optional.None[bool]()) 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 cfb788a5b2..6dd54e42e2 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 c9bdc5be76..8690e75463 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -17,6 +17,7 @@ import ( 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/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" @@ -45,7 +46,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 } @@ -447,13 +448,12 @@ func UpdateIssueProject(ctx *context.Context) { return } - projectID := ctx.FormInt64("id") + projectIDs := ctx.FormStringInt64s("id") + var failedIssues []int64 for _, issue := range issues { - if issue.Project != nil && issue.Project.ID == projectID { - continue - } - if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil { + if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectIDs); err != nil { if errors.Is(err, util.ErrPermissionDenied) { + failedIssues = append(failedIssues, issue.ID) continue } ctx.ServerError("IssueAssignOrRemoveProject", err) @@ -461,6 +461,58 @@ func UpdateIssueProject(ctx *context.Context) { } } + if len(failedIssues) > 0 { + log.Warn("Failed to assign projects to %d issues due to permission denied: %v", len(failedIssues), failedIssues) + } + + ctx.JSONOK() +} + +// UpdateIssueProjectColumn moves an issue to a different column within its project +func UpdateIssueProjectColumn(ctx *context.Context) { + issue, err := issues_model.GetIssueByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("issue_id")) + if err != nil { + ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err) + return + } + column, err := project_model.GetColumn(ctx, ctx.FormInt64("id")) + if err != nil { + ctx.NotFoundOrServerError("GetColumn", project_model.IsErrProjectColumnNotExist, err) + return + } + + if err := issue.LoadProjects(ctx); err != nil { + ctx.ServerError("LoadProjects", err) + return + } + + issueProjects := issue.Projects + + // it must make sure the requested column is in this issue's projects + var columnProject *project_model.Project + for _, project := range issueProjects { + if column.ProjectID == project.ID { + columnProject = project + break + } + } + if columnProject == nil { + ctx.NotFound(nil) + return + } + + // append to the end of the target column so we don't collide with existing sorting values + newSorting, err := project_model.GetColumnIssueNextSorting(ctx, columnProject.ID, column.ID) + if err != nil { + ctx.ServerError("GetColumnIssueNextSorting", err) + return + } + + if err := project_service.MoveIssuesOnProjectColumn(ctx, ctx.Doer, column, map[int64]int64{newSorting: issue.ID}); err != nil { + ctx.ServerError("MoveIssuesOnProjectColumn", err) + return + } + ctx.JSONOK() } @@ -473,7 +525,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.", }) @@ -520,7 +572,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.", }) @@ -558,7 +610,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.", }) @@ -644,7 +696,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 03dea19db6..4c1a2afb75 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "html" + "html/template" "net/http" "strconv" "strings" @@ -35,6 +36,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" @@ -56,7 +58,6 @@ import ( ) const ( - tplCompareDiff templates.TplName = "repo/diff/compare" tplPullCommits templates.TplName = "repo/pulls/commits" tplPullFiles templates.TplName = "repo/pulls/files" @@ -95,9 +96,9 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository { return nil } - perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.ServerError("GetDoerRepoPermission", err) return nil } @@ -161,14 +162,16 @@ 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 + prInfo.headTarget = pull.HeadBranch } else if pull.HeadRepo == nil { - ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + ":" + pull.HeadBranch + prInfo.headTarget = ctx.Locale.TrString("repo.pull.deleted_branch", pull.HeadBranch) } else { - ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch + prInfo.headTarget = pull.MustHeadUserName(ctx) + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch } + ctx.Data["HeadTarget"] = prInfo.headTarget ctx.Data["BaseTarget"] = pull.BaseBranch headBranchLink := "" if pull.Flow == issues_model.PullRequestFlowGithub { @@ -260,60 +263,247 @@ 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 + + TimelineIconClass string + + ClosedInfoTitle template.HTML + ClosedInfoBody template.HTML + + enableStatusCheck bool + StatusCheckData *pullCommitStatusCheckData + ShowStatusCheck bool + + hasOverridableBlockers bool + canMergeNow bool // PR is mergeable, either no blocker, or doer can bypass the blockers + hasPermToMerge bool // doer has permission to merge + canBypassProtection bool + canBypassProtectionAsAdmin bool + + ShowUpdatePullInfo bool + UpdatePrimaryAction *pullUpdateAction + UpdateStyleOptions []*pullUpdateAction + + MergeFormProps map[string]any + ShowPullCommands bool + ShowMergeInstructions bool + AutodetectManualMerge 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 + signingKeyMergeDisplay string + + infoCommitBlockers pullMergeBoxInfoItemCollection + infoProtectionBlockers pullMergeBoxInfoItemCollection + infoMergePrompts pullMergeBoxInfoItemCollection + + InfoSections []*pullInfoSection } -// 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 +type pullUpdateAction struct { + URL string + Text template.HTML + Selected bool +} - setMergeTarget(ctx, pull) - ctx.Data["HasMerged"] = true +// 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 - baseCommit := GetMergedBaseCommitID(ctx, issue) + IsPullRequestBroken bool + HeadBranchCommitID string + headTarget string // for display purpose only - compareInfo, err := git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo, - git.RefName(baseCommit), git.RefName(pull.GetGitHeadRefName()), false, false) + CompareInfo git_service.CompareInfo + ProtectedBranchRule *git_model.ProtectedBranch + MergeBoxData *pullMergeBoxData + + workInProgressPrefix string +} + +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 + } +} + +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["NumCommits"] = len(prInfo.CompareInfo.Commits) + ctx.Data["NumFiles"] = prInfo.CompareInfo.NumFiles + prInfo.setTemplateDataMergeTarget(ctx) +} + +func (prInfo *pullRequestViewInfo) prepareMergeBoxStatusCheckData(ctx *context.Context) { + headCommitID := prInfo.CompareInfo.HeadCommitID + if headCommitID == "" || prInfo.issue.IsClosed { + return + } + + data := prInfo.MergeBoxData + + var pbRequiredContexts []string + data.enableStatusCheck = prInfo.ProtectedBranchRule != nil && prInfo.ProtectedBranchRule.EnableStatusCheck + if prInfo.ProtectedBranchRule != nil { + pbRequiredContexts = prInfo.ProtectedBranchRule.StatusCheckContexts + } + + statusCheckData := &pullCommitStatusCheckData{} + data.StatusCheckData = statusCheckData + + commitStatuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, prInfo.CompareInfo.HeadCommitID, db.ListOptionsAll) + if err != nil { + log.Error("GetLatestCommitStatus: %v", err) + } + if !ctx.Repo.Permission.CanRead(unit.TypeActions) { + git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) + } + combinedCommitStatus := git_model.CalcCommitStatus(commitStatuses) + statusCheckData.ApproveLink = fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s", ctx.Repo.Repository.Link(), headCommitID) + statusCheckData.PullCommitStatuses = commitStatuses + if combinedCommitStatus != nil { + statusCheckData.pullCommitStatusState = combinedCommitStatus.State + } + + data.ShowStatusCheck = data.enableStatusCheck || len(statusCheckData.PullCommitStatuses) > 0 + + runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses) + if err != nil { + log.Error("GetRunsFromCommitStatuses: %v", err) + } + for _, run := range runs { + if run.NeedApproval { + statusCheckData.RequireApprovalRunCount++ + } + } + if statusCheckData.RequireApprovalRunCount > 0 { + statusCheckData.CanApprove = ctx.Repo.Permission.CanWrite(unit.TypeActions) + } + + var missingRequiredChecks []string + for _, requiredContext := range pbRequiredContexts { + 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 pbRequiredContexts { + 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, pbRequiredContexts) + + if data.enableStatusCheck { + if statusCheckData.RequiredChecksState.IsError() || statusCheckData.RequiredChecksState.IsFailure() { + data.infoProtectionBlockers.AddErrorItem( + svg.RenderHTML("octicon-x"), + ctx.Locale.Tr("repo.pulls.required_status_check_failed"), + ) + } else if !statusCheckData.RequiredChecksState.IsSuccess() { + data.infoProtectionBlockers.AddErrorItem( + svg.RenderHTML("octicon-x"), + ctx.Locale.Tr("repo.pulls.required_status_check_missing"), + ) + } + } +} + +// 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 { @@ -323,14 +513,16 @@ type pullCommitStatusCheckData struct { CanApprove bool // whether the user can approve workflow runs ApproveLink string // link to approve all checks RequiredChecksState commitstatus.CommitStatusState - LatestCommitStatus *git_model.CommitStatus + + pullCommitStatusState commitstatus.CommitStatusState + PullCommitStatuses []*git_model.CommitStatus } func (d *pullCommitStatusCheckData) CommitStatusCheckPrompt(locale translation.Locale) string { if d.RequiredChecksState.IsPending() || len(d.MissingRequiredChecks) > 0 { return locale.TrString("repo.pulls.status_checking") } else if d.RequiredChecksState.IsSuccess() { - if d.LatestCommitStatus != nil && d.LatestCommitStatus.State.IsFailure() { + if d.pullCommitStatusState.IsFailure() { return locale.TrString("repo.pulls.status_checks_failure_optional") } return locale.TrString("repo.pulls.status_checks_success") @@ -344,275 +536,54 @@ 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.CompareBase { ctx.Data["IsNothingToCompare"] = true } + // this one is used by: title edit, sidebar toggle, and merge-box toggle + prInfo.workInProgressPrefix = pull.GetWorkInProgressPrefix(ctx) if pull.IsWorkInProgress(ctx) { - ctx.Data["IsPullWorkInProgress"] = true - ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix(ctx) + ctx.Data["IsPullWorkInProgress"] = prInfo.workInProgressPrefix != "" + ctx.Data["WorkInProgressPrefix"] = prInfo.workInProgressPrefix } - - 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,19 +642,18 @@ 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 } - 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 +661,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 +669,6 @@ func ViewPullCommits(ctx *context.Context) { if ctx.Written() { return } - getBranchData(ctx, issue) ctx.HTML(http.StatusOK, tplPullCommits) } @@ -725,46 +694,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.CompareBase) && (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.CompareBase { + beforeCommitID = prCompareInfo.CompareBase + // 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 +747,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["CompareInfo"] = prCompareInfo ctx.Data["AfterCommitID"] = afterCommitID ctx.Data["BeforeCommitID"] = beforeCommitID @@ -799,7 +765,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { MaxLines: maxLines, MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters, MaxFiles: maxFiles, - WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)), + WhitespaceBehavior: gitdiff.GetWhitespaceFlag(GetWhitespaceBehavior(ctx)), } diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, diffOptions, files...) @@ -838,7 +804,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { "numberOfViewedFiles": numViewedFiles, } - if err = diff.LoadComments(ctx, issue, ctx.Doer, ctx.Data["ShowOutdatedComments"].(bool)); err != nil { + if err = diff.LoadComments(ctx, issue, ctx.Doer, GetShowOutdatedComments(ctx)); err != nil { ctx.ServerError("LoadComments", err) return } @@ -856,17 +822,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 +860,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 +894,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 @@ -958,13 +916,13 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { if pull.HeadRepo != nil { if !pull.HasMerged && ctx.Doer != nil { - perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer) + headPerm, err := access_model.GetDoerRepoPermission(ctx, pull.HeadRepo, ctx.Doer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.ServerError("GetDoerRepoPermission", err) return } - if perm.CanWrite(unit.TypeCode) || issues_model.CanMaintainerWriteToBranch(ctx, perm, pull.HeadBranch, ctx.Doer) { + if issues_model.CanMaintainerWriteToBranch(ctx, headPerm, pull.HeadBranch, ctx.Doer) { ctx.Data["CanEditFile"] = true ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") ctx.Data["HeadRepoLink"] = pull.HeadRepo.Link() @@ -1007,8 +965,6 @@ func UpdatePullRequest(ctx *context.Context) { return } - rebase := ctx.FormString("style") == "rebase" - if err := issue.PullRequest.LoadBaseRepo(ctx); err != nil { ctx.ServerError("LoadBaseRepo", err) return @@ -1018,23 +974,22 @@ func UpdatePullRequest(ctx *context.Context) { return } - allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, issue.PullRequest, ctx.Doer) + userUpdateStyles, err := pull_service.CheckUserAllowedToUpdate(ctx, issue.PullRequest, ctx.Doer) if err != nil { ctx.ServerError("IsUserAllowedToMerge", err) return } - // ToDo: add check if maintainers are allowed to change branch ... (need migration & co) - if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) { - ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed")) - ctx.Redirect(issue.Link()) + rebase := ctx.FormString("style", string(userUpdateStyles.DefaultUpdateStyle)) == string(repo_model.UpdateStyleRebase) + if (rebase && !userUpdateStyles.RebaseAllowed) || (!rebase && !userUpdateStyles.MergeAllowed) { + ctx.JSONError(ctx.Tr("repo.pulls.update_not_allowed")) return } // default merge commit message message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch) - // The update process should not be cancelled by the user + // The update process should not be canceled by the user // so we set the context to be a background context if err = pull_service.Update(graceful.GetManager().ShutdownContext(), issue.PullRequest, ctx.Doer, message, rebase); err != nil { if pull_service.IsErrMergeConflicts(err) { @@ -1042,39 +997,37 @@ func UpdatePullRequest(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.merge_conflict"), "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("UpdatePullRequest.HTMLString", err) return } - ctx.Flash.Error(flashError) - ctx.Redirect(issue.Link()) + ctx.JSONError(flashError) return } else if pull_service.IsErrRebaseConflicts(err) { conflictError := err.(pull_service.ErrRebaseConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), + "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.EscapeFlashErrorString(conflictError.CommitSHA)), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("UpdatePullRequest.HTMLString", err) return } - ctx.Flash.Error(flashError) - ctx.Redirect(issue.Link()) + ctx.JSONError(flashError) return } - ctx.Flash.Error(err.Error()) - ctx.Redirect(issue.Link()) + log.Error("Update pull request failed: %v", err) + ctx.JSONError("Unable to update pull request") return } - time.Sleep(1 * time.Second) + time.Sleep(100 * time.Millisecond) // TODO: it is really questionable whether the Sleep is useful here, need to figure out ctx.Flash.Success(ctx.Tr("repo.pulls.update_branch_success")) - ctx.Redirect(issue.Link()) + ctx.JSONRedirect(issue.Link()) } // MergePullRequest response for merging pull request @@ -1100,7 +1053,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 +1073,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: @@ -1191,7 +1146,7 @@ func MergePullRequest(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.merge_conflict"), "Summary": ctx.Tr("repo.editor.merge_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("MergePullRequest.HTMLString", err) @@ -1202,9 +1157,9 @@ func MergePullRequest(ctx *context.Context) { } else if pull_service.IsErrRebaseConflicts(err) { conflictError := err.(pull_service.ErrRebaseConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), + "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.EscapeFlashErrorString(conflictError.CommitSHA)), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("MergePullRequest.HTMLString", err) @@ -1234,7 +1189,7 @@ func MergePullRequest(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.push_rejected"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), + "Details": utils.EscapeFlashErrorString(pushrejErr.Message), }) if err != nil { ctx.ServerError("MergePullRequest.HTMLString", err) @@ -1349,31 +1304,32 @@ func PullsNewRedirect(ctx *context.Context) { // CompareAndPullRequestPost response for creating pull request func CompareAndPullRequestPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateIssueForm) - ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes") - ctx.Data["PageIsComparePull"] = true - ctx.Data["IsDiffCompare"] = true - 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) - - var ( - repo = ctx.Repo.Repository - attachments []string - ) - - ci := ParseCompareInfo(ctx) - if ctx.Written() { + repo := ctx.Repo.Repository + comparePageInfo := newComparePageInfo() + err := comparePageInfo.parseCompareInfo(ctx) + if errors.Is(err, util.ErrNotExist) { + ctx.JSONErrorNotFound() + return + } else if errors.Is(err, util.ErrInvalidArgument) { + ctx.JSONError(err.Error()) + return + } else if err != nil { + ctx.ServerError("ParseCompareInfo", err) + return + } + ci := comparePageInfo.compareInfo + if ci.CompareBase == "" { + ctx.JSONError(ctx.Tr("repo.pulls.no_common_history")) return } - validateRet := ValidateRepoMetasForNewIssue(ctx, *form, true) if ctx.Written() { return } - labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID + labelIDs, assigneeIDs, milestoneID, projectIDs := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectIDs + var attachments []string if setting.Attachment.Enabled { attachments = form.Files } @@ -1423,7 +1379,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { BaseBranch: ci.BaseRef.ShortName(), HeadRepo: ci.HeadRepo, BaseRepo: repo, - MergeBase: ci.MergeBase, + MergeBase: ci.CompareBase, Type: issues_model.PullRequestGitea, AllowMaintainerEdit: form.AllowMaintainerEdit, } @@ -1438,7 +1394,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { AssigneeIDs: assigneeIDs, Reviewers: validateRet.Reviewers, TeamReviewers: validateRet.TeamReviewers, - ProjectID: projectID, + ProjectIDs: projectIDs, } if err := pull_service.NewPullRequest(ctx, prOpts); err != nil { switch { @@ -1454,7 +1410,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.push_rejected"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), + "Details": utils.EscapeFlashErrorString(pushrejErr.Message), }) if err != nil { ctx.ServerError("CompareAndPullRequest.HTMLString", err) @@ -1550,7 +1506,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_merge_box.go b/routers/web/repo/pull_merge_box.go new file mode 100644 index 0000000000..aae22bbfb2 --- /dev/null +++ b/routers/web/repo/pull_merge_box.go @@ -0,0 +1,196 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "html/template" + + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/svg" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" +) + +type pullMergeBoxInfoItem struct { + ItemClass string + SvgIconHTML template.HTML + InfoHTML template.HTML + ListItems []template.HTML +} + +type pullMergeBoxInfoItemCollection struct { + items []*pullMergeBoxInfoItem +} + +type pullInfoSection struct { + InfoItems []*pullMergeBoxInfoItem +} + +func escapeStringSliceToHTML(s []string) (ret []template.HTML) { + for _, v := range s { + ret = append(ret, template.HTML(template.HTMLEscapeString(v))) + } + return ret +} + +func (c *pullMergeBoxInfoItemCollection) AddInfoItem(svg, info template.HTML, optItems ...[]template.HTML) { + c.items = append(c.items, &pullMergeBoxInfoItem{ + SvgIconHTML: svg, + InfoHTML: info, + ListItems: util.OptionalArg(optItems), + }) +} + +func (c *pullMergeBoxInfoItemCollection) AddErrorItem(svg, info template.HTML, optItems ...[]template.HTML) { + c.items = append(c.items, &pullMergeBoxInfoItem{ + ItemClass: "tw-text-red", + SvgIconHTML: svg, + InfoHTML: info, + ListItems: util.OptionalArg(optItems), + }) +} + +func (prInfo *pullRequestViewInfo) prepareMergeBoxIconColor() { + pull := prInfo.issue.PullRequest + mergeBoxData := prInfo.MergeBoxData + + showAsNormalColor := prInfo.issue.IsClosed || prInfo.workInProgressPrefix != "" || pull.IsEmpty() || pull.IsFilesConflicted() + showAsErrorColor := false + showAsWarningColor := pull.IsChecking() + + if statusCheckData := mergeBoxData.StatusCheckData; statusCheckData != nil { + showAsErrorColor = statusCheckData.pullCommitStatusState.IsError() || statusCheckData.pullCommitStatusState.IsFailure() || + statusCheckData.RequiredChecksState.IsError() || statusCheckData.RequiredChecksState.IsFailure() + + showAsWarningColor = showAsWarningColor || + statusCheckData.pullCommitStatusState.IsWarning() || statusCheckData.pullCommitStatusState.IsPending() || + (mergeBoxData.enableStatusCheck && (statusCheckData.RequiredChecksState.IsWarning() || statusCheckData.RequiredChecksState.IsPending())) + } + + hasBlockers := len(mergeBoxData.infoCommitBlockers.items) > 0 || len(mergeBoxData.infoProtectionBlockers.items) > 0 + + switch { + case pull.HasMerged: + prInfo.MergeBoxData.TimelineIconClass = "tw-text-purple" + case showAsNormalColor: + prInfo.MergeBoxData.TimelineIconClass = "tw-text-text-light" + case showAsErrorColor: + prInfo.MergeBoxData.TimelineIconClass = "tw-text-red" + case showAsWarningColor: + prInfo.MergeBoxData.TimelineIconClass = "tw-text-yellow" + case hasBlockers: + prInfo.MergeBoxData.TimelineIconClass = "tw-text-red" + case pull.IsStatusMergeable(): + prInfo.MergeBoxData.TimelineIconClass = "tw-text-green" + default: + prInfo.MergeBoxData.TimelineIconClass = "tw-text-text-light" + } +} + +func (prInfo *pullRequestViewInfo) prepareMergeBoxInfoItems(ctx *context.Context) { + pull := prInfo.issue.PullRequest + data := prInfo.MergeBoxData + + if pull.HasMerged && data.IsPullBranchDeletable { + data.ClosedInfoTitle = ctx.Locale.Tr("repo.pulls.merged_success") + data.ClosedInfoBody = ctx.Locale.Tr("repo.pulls.merged_info_text", htmlutil.HTMLFormat("%s", prInfo.headTarget)) + return + } else if prInfo.issue.IsClosed { + data.ClosedInfoTitle = ctx.Locale.Tr("repo.pulls.closed") + if prInfo.IsPullRequestBroken { + data.ClosedInfoBody = ctx.Locale.Tr("repo.pulls.cant_reopen_deleted_branch") + } else { + data.ClosedInfoBody = ctx.Locale.Tr("repo.pulls.reopen_to_merge") + } + return + } + + if pull.IsFilesConflicted() { + detailItems := escapeStringSliceToHTML(pull.ConflictedFiles) + if len(detailItems) == 0 { + detailItems = append(detailItems, ctx.Locale.Tr("repo.pulls.files_conflicted_no_listed_files")) + } + if len(detailItems) > 10 { + detailItems = detailItems[:10] + detailItems = append(detailItems, "...") + } + prInfo.MergeBoxData.infoCommitBlockers.AddInfoItem( + svg.RenderHTML("octicon-x"), + ctx.Locale.Tr("repo.pulls.files_conflicted"), + detailItems, + ) + } + + if prInfo.IsPullRequestBroken { + prInfo.MergeBoxData.infoCommitBlockers.AddInfoItem( + svg.RenderHTML("octicon-x"), + ctx.Locale.Tr("repo.pulls.data_broken"), + ) + } + + if pull.IsChecking() { + prInfo.MergeBoxData.infoCommitBlockers.AddInfoItem( + svg.RenderHTML("gitea-running", 16, "rotate-clockwise"), + ctx.Locale.Tr("repo.pulls.is_checking"), + ) + } + + if pull.IsAncestor() { + prInfo.MergeBoxData.infoCommitBlockers.AddInfoItem( + svg.RenderHTML("octicon-alert"), + ctx.Locale.Tr("repo.pulls.is_ancestor"), + ) + } + + if !pull.IsStatusMergeable() { + // it is only a "protection" level blocker, it can be bypassed by admin (e.g.: manually merged) + if pull.IsEmpty() { + prInfo.MergeBoxData.infoProtectionBlockers.AddInfoItem( + svg.RenderHTML("octicon-alert"), + ctx.Locale.Tr("repo.pulls.is_empty"), + ) + } else { + prInfo.MergeBoxData.infoProtectionBlockers.AddErrorItem( + svg.RenderHTML("octicon-x"), + ctx.Locale.Tr("repo.pulls.cannot_auto_merge_desc"), + ) + prInfo.MergeBoxData.infoProtectionBlockers.AddInfoItem( + svg.RenderHTML("octicon-info"), + ctx.Locale.Tr("repo.pulls.cannot_auto_merge_helper"), + ) + } + } + + if !data.hasPermToMerge { + prInfo.MergeBoxData.infoProtectionBlockers.AddInfoItem( + svg.RenderHTML("octicon-info"), + ctx.Locale.Tr("repo.pulls.no_merge_access"), + ) + } + + if data.canMergeNow { + if data.hasOverridableBlockers { + prompt := ctx.Locale.Tr("repo.pulls.required_status_check_bypass_allowlist") + if data.canBypassProtectionAsAdmin { + prompt = ctx.Locale.Tr("repo.pulls.required_status_check_administrator") + } + prInfo.MergeBoxData.infoMergePrompts.AddInfoItem( + svg.RenderHTML("octicon-dot-fill"), + prompt, + ) + } else if pull.IsStatusMergeable() || pull.IsEmpty() { + prInfo.MergeBoxData.infoMergePrompts.AddInfoItem( + svg.RenderHTML("octicon-check"), + ctx.Locale.Tr("repo.pulls.can_auto_merge_desc"), + ) + } + } + + if len(data.infoCommitBlockers.items) > 0 { + data.InfoSections = append(data.InfoSections, &pullInfoSection{data.infoCommitBlockers.items}) + } else { + data.InfoSections = append(data.InfoSections, &pullInfoSection{data.infoProtectionBlockers.items}) + } + data.InfoSections = append(data.InfoSections, &pullInfoSection{data.infoMergePrompts.items}) +} diff --git a/routers/web/repo/pull_merge_form.go b/routers/web/repo/pull_merge_form.go new file mode 100644 index 0000000000..3fa3a91bc9 --- /dev/null +++ b/routers/web/repo/pull_merge_form.go @@ -0,0 +1,184 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "html/template" + + pull_model "code.gitea.io/gitea/models/pull" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/svg" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/context" + pull_service "code.gitea.io/gitea/services/pull" +) + +func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context) { + pull := prInfo.issue.PullRequest + if pull.HasMerged || prInfo.issue.IsClosed { + return + } + if !prInfo.MergeBoxData.hasPermToMerge { + return + } + + prConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() + + // Check correct values and select default + var mergeStyle repo_model.MergeStyle + if prConfig.IsMergeStyleAllowed(prConfig.DefaultMergeStyle) { + mergeStyle = prConfig.DefaultMergeStyle + } else if prConfig.AllowMerge { + mergeStyle = repo_model.MergeStyleMerge + } else if prConfig.AllowRebase { + mergeStyle = repo_model.MergeStyleRebase + } else if prConfig.AllowRebaseMerge { + mergeStyle = repo_model.MergeStyleRebaseMerge + } else if prConfig.AllowSquash { + mergeStyle = repo_model.MergeStyleSquash + } else if prConfig.AllowFastForwardOnly { + mergeStyle = repo_model.MergeStyleFastForwardOnly + } else if prConfig.AllowManualMerge { + mergeStyle = repo_model.MergeStyleManuallyMerged + } + if mergeStyle == "" { + return + } + + // Check if there is a pending pr merge + hasPendingPullRequestMerge, pendingPullRequestMerge, err := pull_model.GetScheduledMergeByPullID(ctx, pull.ID) + if err != nil { + ctx.ServerError("GetScheduledMergeByPullID", err) + return + } + + var hasPendingPullRequestMergeTip template.HTML + if hasPendingPullRequestMerge { + createdPRMergeStr := templates.TimeSince(pendingPullRequestMerge.CreatedUnix) + hasPendingPullRequestMergeTip = ctx.Locale.Tr("repo.pulls.auto_merge_has_pending_schedule", pendingPullRequestMerge.Doer.Name, createdPRMergeStr) + } + + defaultMergeTitle, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle) + if err != nil { + ctx.ServerError("GetDefaultMergeMessage", err) + return + } + defaultSquashMergeTitle, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash) + if err != nil { + ctx.ServerError("GetDefaultSquashMergeMessage", err) + return + } + + var defaultSquashMergeCommitMessages string + if !prInfo.IsPullRequestBroken { + defaultSquashMergeCommitMessages = pull_service.GetSquashMergeCommitMessages(ctx, pull) + } + + allOverridableChecksOk := !prInfo.MergeBoxData.hasOverridableBlockers + mergeFormProps := map[string]any{ + "baseLink": prInfo.issue.Link(), + "textCancel": ctx.Locale.Tr("cancel"), + "textDeleteBranch": ctx.Locale.Tr("repo.branch.delete", prInfo.headTarget), + "textAutoMergeButtonWhenSucceed": ctx.Locale.Tr("repo.pulls.auto_merge_button_when_succeed"), + "textAutoMergeWhenSucceed": ctx.Locale.Tr("repo.pulls.auto_merge_when_succeed"), + "textAutoMergeCancelSchedule": ctx.Locale.Tr("repo.pulls.auto_merge_cancel_schedule"), + "textClearMergeMessage": ctx.Locale.Tr("repo.pulls.clear_merge_message"), + "textClearMergeMessageHint": ctx.Locale.Tr("repo.pulls.clear_merge_message_hint"), + "textMergeCommitId": ctx.Locale.Tr("repo.pulls.merge_commit_id"), + + "canMergeNow": prInfo.MergeBoxData.canMergeNow, + "allOverridableChecksOk": allOverridableChecksOk, + "emptyCommit": pull.IsEmpty(), + "pullHeadCommitID": prInfo.CompareInfo.HeadCommitID, + "isPullBranchDeletable": prInfo.MergeBoxData.IsPullBranchDeletable, + "defaultMergeStyle": mergeStyle, + "defaultDeleteBranchAfterMerge": prConfig.DefaultDeleteBranchAfterMerge, + "mergeMessageFieldPlaceHolder": ctx.Locale.Tr("repo.editor.commit_message_desc"), + "defaultMergeMessage": defaultMergeBody, + + "hasPendingPullRequestMerge": hasPendingPullRequestMerge, + "hasPendingPullRequestMergeTip": hasPendingPullRequestMergeTip, + } + + // if this pr can be merged now, then hide the auto merge + generalHideAutoMerge := prInfo.MergeBoxData.canMergeNow && allOverridableChecksOk + + var mergeStyles []any + if pull.IsStatusMergeable() { + mergeStyles = []any{ + map[string]any{ + "name": "merge", + "allowed": prConfig.AllowMerge, + "textDoMerge": ctx.Locale.Tr("repo.pulls.merge_pull_request"), + "mergeTitleFieldText": defaultMergeTitle, + "mergeMessageFieldText": defaultMergeBody, + "hideAutoMerge": generalHideAutoMerge, + }, + map[string]any{ + "name": "rebase", + "allowed": prConfig.AllowRebase, + "textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_pull_request"), + "hideMergeMessageTexts": true, + "hideAutoMerge": generalHideAutoMerge, + }, + map[string]any{ + "name": "rebase-merge", + "allowed": prConfig.AllowRebaseMerge, + "textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_commit_pull_request"), + "mergeTitleFieldText": defaultMergeTitle, + "mergeMessageFieldText": defaultMergeBody, + "hideAutoMerge": generalHideAutoMerge, + }, + map[string]any{ + "name": "squash", + "allowed": prConfig.AllowSquash, + "textDoMerge": ctx.Locale.Tr("repo.pulls.squash_merge_pull_request"), + "mergeTitleFieldText": defaultSquashMergeTitle, + "mergeMessageFieldText": defaultSquashMergeCommitMessages + defaultSquashMergeBody, + "hideAutoMerge": generalHideAutoMerge, + }, + map[string]any{ + "name": "fast-forward-only", + "allowed": prConfig.AllowFastForwardOnly && pull.CommitsBehind == 0, + "textDoMerge": ctx.Locale.Tr("repo.pulls.fast_forward_only_merge_pull_request"), + "hideMergeMessageTexts": true, + "hideAutoMerge": generalHideAutoMerge, + }, + } + } + + // Manually Merged is not a well-known feature, it is used to mark a non-mergeable PR (already merged, conflicted) as merged + // To test it: + // Enable "Manually Merged" feature in the Repository Settings + // Create a pull request, either: + // - Merge the pull request branch locally and push the merged commit to Gitea + // - Make some conflicts between the base branch and the pull request branch + // Then the Manually Merged form will be shown in the merge form + canUseManualMerge := !pull.IsWorkInProgress(ctx) && !pull.IsChecking() && prConfig.AllowManualMerge + if canUseManualMerge { + mergeStyles = append(mergeStyles, map[string]any{ + "name": "manually-merged", + "allowed": prConfig.AllowManualMerge, + "textDoMerge": ctx.Locale.Tr("repo.pulls.merge_manually"), + "hideMergeMessageTexts": true, + "hideAutoMerge": true, + }) + } + + if len(mergeStyles) > 0 { + mergeFormProps["mergeStyles"] = mergeStyles + prInfo.MergeBoxData.MergeFormProps = mergeFormProps + } else if pull.IsStatusMergeable() { + // no merge style was set in repo setting + prInfo.MergeBoxData.infoCommitBlockers.AddInfoItem( + svg.RenderHTML("octicon-x", 16, "tw-text-red"), + ctx.Locale.Tr("repo.pulls.no_merge_desc"), + ) + prInfo.MergeBoxData.infoCommitBlockers.AddInfoItem( + svg.RenderHTML("octicon-info"), + ctx.Locale.Tr("repo.pulls.no_merge_helper"), + ) + } +} diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index f064058221..bbcf6630b6 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 } @@ -169,7 +169,7 @@ func UpdateResolveConversation(ctx *context.Context) { func renderConversation(ctx *context.Context, comment *issues_model.Comment, origin string) { ctx.Data["PageIsPullFiles"] = origin == "diff" - showOutdatedComments := origin == "timeline" || ctx.Data["ShowOutdatedComments"].(bool) + showOutdatedComments := origin == "timeline" || GetShowOutdatedComments(ctx) comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, showOutdatedComments) if err != nil { ctx.ServerError("FetchCodeCommentsByLine", err) @@ -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 005106a32d..288e107e53 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{ @@ -451,6 +447,7 @@ func NewReleasePost(ctx *context.Context) { return } + form.Target = util.IfZero(form.Target, ctx.Repo.Repository.DefaultBranch) if exist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, form.Target); !exist { ctx.RenderWithErrDeprecated(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form) return @@ -564,6 +561,11 @@ func EditRelease(ctx *context.Context) { } return } + if rel.IsTag { + ctx.NotFound(err) // for a pure tag release, don't allow to edit it as a release + return + } + ctx.Data["ID"] = rel.ID ctx.Data["tag_name"] = rel.TagName ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch) @@ -613,7 +615,7 @@ func EditReleasePost(ctx *context.Context) { return } if rel.IsTag { - ctx.NotFound(err) + ctx.NotFound(err) // for a pure tag release, don't allow to edit it as a release return } ctx.Data["tag_name"] = rel.TagName diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go index 7ba91afb29..d57886c273 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/render.go b/routers/web/repo/render.go index b1299c7047..4a68c96aaa 100644 --- a/routers/web/repo/render.go +++ b/routers/web/repo/render.go @@ -40,9 +40,12 @@ func RenderFile(ctx *context.Context) { defer blobReader.Close() rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), - CurrentTreePath: path.Dir(ctx.Repo.TreePath), - }).WithRelativePath(ctx.Repo.TreePath).WithInStandalonePage(true) + CurrentRefSubURL: ctx.Repo.RefTypeNameSubURL(), + CurrentTreePath: path.Dir(ctx.Repo.TreePath), + }).WithRelativePath(ctx.Repo.TreePath).WithStandalonePage(markup.StandalonePageOptions{ + CurrentWebTheme: ctx.TemplateContext.CurrentWebTheme(), + RenderQueryString: ctx.Req.URL.RawQuery, + }) renderer, rendererInput, err := rctx.DetectMarkupRendererByReader(blobReader) if err != nil { http.Error(ctx.Resp, "Unable to find renderer", http.StatusBadRequest) diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 57937be83e..c7813feae2 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -302,12 +302,12 @@ func CreatePost(ctx *context.Context) { func handleActionError(ctx *context.Context, err error) { switch { case errors.Is(err, user_model.ErrBlockedUser): - ctx.Flash.Error(ctx.Tr("repo.action.blocked_user")) + ctx.JSONError(ctx.Tr("repo.action.blocked_user")) case repo_service.IsRepositoryLimitReached(err): limit := err.(repo_service.LimitReachedError).Limit - ctx.Flash.Error(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit)) + ctx.JSONError(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit)) case errors.Is(err, util.ErrPermissionDenied): - ctx.HTTPError(http.StatusNotFound) + ctx.JSONError(ctx.Tr("error.permission_denied")) default: ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err) } @@ -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/actions.go b/routers/web/repo/setting/actions.go index 2237828d61..27b16b42d9 100644 --- a/routers/web/repo/setting/actions.go +++ b/routers/web/repo/setting/actions.go @@ -6,7 +6,6 @@ package setting import ( "errors" "net/http" - "strings" "code.gitea.io/gitea/models/actions" repo_model "code.gitea.io/gitea/models/repo" @@ -94,15 +93,12 @@ func ActionsUnitPost(ctx *context.Context) { } func AddCollaborativeOwner(ctx *context.Context) { - name := strings.ToLower(ctx.FormString("collaborative_owner")) - - ownerID, err := user_model.GetUserOrOrgIDByName(ctx, name) + collUser, err := user_model.GetUserByName(ctx, ctx.FormString("collaborative_owner")) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.Flash.Error(ctx.Tr("form.user_not_exist")) - ctx.JSONErrorNotFound() + ctx.JSONError(ctx.Tr("form.user_not_exist")) } else { - ctx.ServerError("GetUserOrOrgIDByName", err) + ctx.ServerError("GetUserByName", err) } return } @@ -113,7 +109,7 @@ func AddCollaborativeOwner(ctx *context.Context) { return } actionsCfg := actionsUnit.ActionsConfig() - actionsCfg.AddCollaborativeOwner(ownerID) + actionsCfg.AddCollaborativeOwner(collUser.ID) if err := repo_model.UpdateRepoUnitConfig(ctx, actionsUnit); err != nil { ctx.ServerError("UpdateRepoUnitConfig", err) return diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go index dbfd6e08b6..177d10d165 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/git_hooks.go b/routers/web/repo/setting/git_hooks.go index 1f542a3f9f..a5aa74dafa 100644 --- a/routers/web/repo/setting/git_hooks.go +++ b/routers/web/repo/setting/git_hooks.go @@ -42,7 +42,7 @@ func GitHooksEdit(ctx *context.Context) { return } ctx.Data["Hook"] = hook - ctx.Data["CodeEditorConfig"] = repo.CodeEditorConfig{} // not really editing a repo file, so no editor config + ctx.Data["CodeEditorConfig"] = repo.CodeEditorConfig{Filename: name + ".sh", IndentStyle: "tab", TabWidth: 4} ctx.HTML(http.StatusOK, tplGithookEdit) } diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index 4374e95340..ac09b4fab9 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/glob" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/repo" @@ -82,6 +83,7 @@ func SettingsProtectedBranch(c *context.Context) { c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(rule.WhitelistUserIDs), ",") c.Data["force_push_allowlist_users"] = strings.Join(base.Int64sToStrings(rule.ForcePushAllowlistUserIDs), ",") c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistUserIDs), ",") + c.Data["bypass_allowlist_users"] = strings.Join(base.Int64sToStrings(rule.BypassAllowlistUserIDs), ",") c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistUserIDs), ",") c.Data["status_check_contexts"] = strings.Join(rule.StatusCheckContexts, "\n") contexts, _ := git_model.FindRepoRecentCommitStatusContexts(c, c.Repo.Repository.ID, 7*24*time.Hour) // Find last week status check contexts @@ -97,6 +99,7 @@ func SettingsProtectedBranch(c *context.Context) { c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.WhitelistTeamIDs), ",") c.Data["force_push_allowlist_teams"] = strings.Join(base.Int64sToStrings(rule.ForcePushAllowlistTeamIDs), ",") c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistTeamIDs), ",") + c.Data["bypass_allowlist_teams"] = strings.Join(base.Int64sToStrings(rule.BypassAllowlistTeamIDs), ",") c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistTeamIDs), ",") } @@ -154,7 +157,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) { } } - var whitelistUsers, whitelistTeams, forcePushAllowlistUsers, forcePushAllowlistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64 + var whitelistUsers, whitelistTeams, forcePushAllowlistUsers, forcePushAllowlistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams, bypassAllowlistUsers, bypassAllowlistTeams []int64 protectBranch.RuleName = f.RuleName if f.RequiredApprovals < 0 { ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min")) @@ -214,6 +217,16 @@ func SettingsProtectedBranchPost(ctx *context.Context) { } } + protectBranch.EnableBypassAllowlist = f.EnableBypassAllowlist + if f.EnableBypassAllowlist { + if strings.TrimSpace(f.BypassAllowlistUsers) != "" { + bypassAllowlistUsers, _ = base.StringsToInt64s(strings.Split(f.BypassAllowlistUsers, ",")) + } + if strings.TrimSpace(f.BypassAllowlistTeams) != "" { + bypassAllowlistTeams, _ = base.StringsToInt64s(strings.Split(f.BypassAllowlistTeams, ",")) + } + } + protectBranch.EnableStatusCheck = f.EnableStatusCheck if f.EnableStatusCheck { patterns := strings.Split(strings.ReplaceAll(f.StatusCheckContexts, "\r", "\n"), "\n") @@ -270,6 +283,8 @@ func SettingsProtectedBranchPost(ctx *context.Context) { MergeTeamIDs: mergeWhitelistTeams, ApprovalsUserIDs: approvalsWhitelistUsers, ApprovalsTeamIDs: approvalsWhitelistTeams, + BypassUserIDs: bypassAllowlistUsers, + BypassTeamIDs: bypassAllowlistTeams, }); err != nil { ctx.ServerError("CreateOrUpdateProtectedBranch", err) return @@ -312,10 +327,14 @@ func DeleteProtectedBranchRulePost(ctx *context.Context) { } func UpdateBranchProtectionPriories(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.ProtectBranchPriorityForm) - repo := ctx.Repo.Repository - - if err := git_model.UpdateProtectBranchPriorities(ctx, repo, form.IDs); err != nil { + var form struct { + IDs []int64 `json:"ids"` + } + if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { + ctx.JSONError("invalid argument") + return + } + if err := git_model.UpdateProtectBranchPriorities(ctx, ctx.Repo.Repository, form.IDs); err != nil { ctx.ServerError("UpdateProtectBranchPriorities", err) return } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 5a5137a1a7..a9276f1bc0 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -6,6 +6,7 @@ package setting import ( "errors" + "html/template" "net/http" "strings" "time" @@ -49,6 +50,12 @@ const ( tplDeployKeys templates.TplName = "repo/settings/deploy_keys" ) +type selectOption struct { + Value string + Text template.HTML + Selected bool +} + // SettingsCtxData is a middleware that sets all the general context data for the // settings template. func SettingsCtxData(ctx *context.Context) { @@ -66,6 +73,7 @@ func SettingsCtxData(ctx *context.Context) { ctx.Data["SigningKeyAvailable"] = signing != nil ctx.Data["SigningSettings"] = setting.Repository.Signing ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled + preparePullRequestSettings(ctx) if ctx.Doer.IsAdmin { if setting.Indexer.RepoIndexerEnabled { @@ -96,6 +104,35 @@ func SettingsCtxData(ctx *context.Context) { } } +func preparePullRequestSettings(ctx *context.Context) { + defaultUpdateStyle := repo_model.UpdateStyleMerge + if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) { + prUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit_model.TypePullRequests) + defaultUpdateStyle = util.IfZero(prUnit.PullRequestsConfig().DefaultUpdateStyle, repo_model.UpdateStyleMerge) + } + + updateBranchText := ctx.Tr("repo.pulls.update_branch") + rebaseUpdateText := ctx.Tr("repo.pulls.update_branch_rebase") + defaultUpdateStyleText := updateBranchText + if defaultUpdateStyle == repo_model.UpdateStyleRebase { + defaultUpdateStyleText = rebaseUpdateText + } + + ctx.Data["PullsDefaultUpdateStyleText"] = defaultUpdateStyleText + ctx.Data["PullsDefaultUpdateStyleOptions"] = []selectOption{ + { + Value: string(repo_model.UpdateStyleMerge), + Text: updateBranchText, + Selected: defaultUpdateStyle == repo_model.UpdateStyleMerge, + }, + { + Value: string(repo_model.UpdateStyleRebase), + Text: rebaseUpdateText, + Selected: defaultUpdateStyle == repo_model.UpdateStyleRebase, + }, + } +} + // Settings show a repository's settings page func Settings(ctx *context.Context) { ctx.HTML(http.StatusOK, tplSettingsOptions) @@ -265,8 +302,13 @@ func handleSettingsPostMirror(ctx *context.Context) { handleSettingRemoteAddrError(ctx, err, form) return } - if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() { - form.MirrorPassword, _ = u.User.Password() + if u.User != nil { + if form.MirrorUsername == "" { + form.MirrorUsername = u.User.Username() + } + if form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() { + form.MirrorPassword, _ = u.User.Password() + } } address, err := git.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword) @@ -459,11 +501,7 @@ func handleSettingsPostPushMirrorAdd(ctx *context.Context) { return } - remoteSuffix, err := util.CryptoRandomString(10) - if err != nil { - ctx.ServerError("RandomString", err) - return - } + remoteSuffix := util.CryptoRandomString(10) remoteAddress, err := util.SanitizeURL(form.PushMirrorAddress) if err != nil { @@ -531,7 +569,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 @@ -561,7 +599,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 @@ -615,7 +653,8 @@ func handleSettingsPostAdvanced(ctx *context.Context) { } if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() { - units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, &repo_model.PullRequestsConfig{ + defaultUpdateStyle := util.IfZero(repo_model.UpdateStyle(form.PullsDefaultUpdateStyle), repo_model.UpdateStyleMerge) + prConfig := &repo_model.PullRequestsConfig{ IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace, AllowMerge: form.PullsAllowMerge, AllowRebase: form.PullsAllowRebase, @@ -624,12 +663,20 @@ func handleSettingsPostAdvanced(ctx *context.Context) { AllowFastForwardOnly: form.PullsAllowFastForwardOnly, AllowManualMerge: form.PullsAllowManualMerge, AutodetectManualMerge: form.EnableAutodetectManualMerge, + AllowMergeUpdate: form.PullsAllowMergeUpdate, AllowRebaseUpdate: form.PullsAllowRebaseUpdate, + DefaultUpdateStyle: defaultUpdateStyle, DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge, DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle), DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit, DefaultTargetBranch: strings.TrimSpace(form.DefaultTargetBranch), - })) + } + if err := prConfig.ValidateUpdateSettings(); err != nil { + ctx.Flash.Error(err.Error()) + ctx.Redirect(repo.Link() + "/settings") + return + } + units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, prConfig)) } else if !unit_model.TypePullRequests.UnitGlobalDisabled() { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests) } @@ -728,17 +775,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 @@ -752,14 +799,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 { @@ -767,12 +814,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 } @@ -780,7 +827,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 } @@ -792,25 +839,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) @@ -820,7 +867,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 } } @@ -834,14 +881,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) } @@ -856,12 +903,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 } @@ -890,12 +937,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 } @@ -911,18 +958,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 } @@ -933,12 +980,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 } @@ -971,7 +1018,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/setting/settings_test.go b/routers/web/repo/setting/settings_test.go index 4c65b696c5..154d01fda4 100644 --- a/routers/web/repo/setting/settings_test.go +++ b/routers/web/repo/setting/settings_test.go @@ -13,15 +13,18 @@ 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/gitrepo" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/contexttest" "code.gitea.io/gitea/services/forms" + mirror_service "code.gitea.io/gitea/services/mirror" repo_service "code.gitea.io/gitea/services/repository" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAddReadOnlyDeployKey(t *testing.T) { @@ -386,3 +389,45 @@ func TestDeleteTeam(t *testing.T) { assert.False(t, repo_service.HasRepository(t.Context(), team, re.ID)) } + +func TestHandleSettingsPostMirrorPreservesExistingUsername(t *testing.T) { + defer test.MockVariableValue(&setting.Mirror.Enabled, true)() + + unittest.PrepareTestEnv(t) + + // Use the existing fixture mirror repo (org3/repo5) which has a git repo on disk. + mirrorRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 5}) + mirror := unittest.AssertExistsAndLoadBean(t, &repo_model.Mirror{RepoID: 5}) + + require.NoError(t, mirror_service.UpdateAddress(t.Context(), mirror, "https://existing-user:existing-password@example.com/user2/repo1.git")) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + ctx, _ := contexttest.MockContext(t, mirrorRepo.Link()+"/settings") + contexttest.LoadUser(t, ctx, user.ID) + contexttest.LoadRepo(t, ctx, mirrorRepo.ID) + + web.SetForm(ctx, &forms.RepoSettingForm{ + Interval: "8h", + MirrorAddress: "https://example.com/user2/repo1.git", + MirrorPassword: "updated-password", + }) + + handleSettingsPostMirror(ctx) + + assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) + + updatedMirror := unittest.AssertExistsAndLoadBean(t, &repo_model.Mirror{RepoID: mirrorRepo.ID}) + assert.Equal(t, "https://example.com/user2/repo1.git", updatedMirror.RemoteAddress) + + updatedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: mirrorRepo.ID}) + assert.Equal(t, "https://example.com/user2/repo1.git", updatedRepo.OriginalURL) + + remoteURL, err := gitrepo.GitRemoteGetURL(t.Context(), updatedRepo, updatedMirror.GetRemoteName()) + require.NoError(t, err) + require.NotNil(t, remoteURL.User) + assert.Equal(t, "existing-user", remoteURL.User.Username()) + password, ok := remoteURL.User.Password() + require.True(t, ok) + assert.Equal(t, "updated-password", password) +} diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index f107449749..a7c0b863cc 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -234,6 +234,7 @@ func createWebhook(ctx *context.Context, params webhookParams) { w := &webhook.Webhook{ RepoID: orCtx.RepoID, URL: params.URL, + Name: strings.TrimSpace(params.WebhookForm.Name), HTTPMethod: params.HTTPMethod, ContentType: params.ContentType, Secret: params.WebhookForm.Secret, @@ -288,6 +289,7 @@ func editWebhook(ctx *context.Context, params webhookParams) { } w.URL = params.URL + w.Name = strings.TrimSpace(params.WebhookForm.Name) w.ContentType = params.ContentType w.Secret = params.WebhookForm.Secret w.HookEvent = ParseHookEvent(params.WebhookForm) @@ -448,12 +450,21 @@ func MatrixHooksEditPost(ctx *context.Context) { editWebhook(ctx, matrixHookParams(ctx)) } +func matrixRoomIDEncode(roomID string) string { + // See https://spec.matrix.org/latest/appendices/#room-ids + // Some (unrelated) demo links: https://spec.matrix.org/latest/appendices/#matrixto-navigation + // API spec: https://spec.matrix.org/v1.18/client-server-api/#sending-events-to-a-room + // Some of their examples show links like: "PUT /rooms/!roomid:domain/state/m.example.event" + return strings.NewReplacer("%21", "!", "%3A", ":").Replace(url.PathEscape(roomID)) +} + func matrixHookParams(ctx *context.Context) webhookParams { form := web.GetForm(ctx).(*forms.NewMatrixHookForm) + // TODO: need to migrate to the latest (v3) API: https://spec.matrix.org/v1.18/client-server-api/ return webhookParams{ Type: webhook_module.MATRIX, - URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)), + URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, matrixRoomIDEncode(form.RoomID)), ContentType: webhook.ContentTypeJSON, HTTPMethod: http.MethodPut, WebhookForm: form.WebhookForm, @@ -664,7 +675,7 @@ func TestWebhook(ctx *context.Context) { ID: objectFormat.EmptyObjectID(), Author: ghost.NewGitSig(), Committer: ghost.NewGitSig(), - CommitMessage: "This is a fake commit", + CommitMessage: git.CommitMessage{MessageRaw: "This is a fake commit"}, } } @@ -672,7 +683,7 @@ func TestWebhook(ctx *context.Context) { apiCommit := &api.PayloadCommit{ ID: commit.ID.String(), - Message: commit.Message(), + Message: commit.MessageUTF8(), URL: ctx.Repo.Repository.HTMLURL() + "/commit/" + url.PathEscape(commit.ID.String()), Author: &api.PayloadUser{ Name: commit.Author.Name, diff --git a/routers/web/repo/setting/webhook_test.go b/routers/web/repo/setting/webhook_test.go new file mode 100644 index 0000000000..ca4a21e075 --- /dev/null +++ b/routers/web/repo/setting/webhook_test.go @@ -0,0 +1,15 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWebhookMatrix(t *testing.T) { + assert.Equal(t, "!roomid:domain", matrixRoomIDEncode("!roomid:domain")) + assert.Equal(t, "!room%23id:domain", matrixRoomIDEncode("!room#id:domain")) // maybe it should never really happen in real world +} diff --git a/routers/web/repo/star.go b/routers/web/repo/star.go index 00c06b7d02..c93c877d63 100644 --- a/routers/web/repo/star.go +++ b/routers/web/repo/star.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/services/context" ) -const tplStarUnstar templates.TplName = "repo/star_unstar" +const tplStarUnstar templates.TplName = "repo/header/star" func ActionStar(ctx *context.Context) { err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, ctx.PathParam("action") == "star") @@ -26,6 +26,5 @@ func ActionStar(ctx *context.Context) { ctx.ServerError("GetRepositoryByName", err) return } - ctx.RespHeader().Add("hx-trigger", "refreshUserCards") // see the `hx-trigger="refreshUserCards ..."` comments in tmpl ctx.HTML(http.StatusOK, tplStarUnstar) } diff --git a/routers/web/repo/transfer.go b/routers/web/repo/transfer.go index 5553eee674..a606e0343f 100644 --- a/routers/web/repo/transfer.go +++ b/routers/web/repo/transfer.go @@ -12,7 +12,7 @@ func acceptTransfer(ctx *context.Context) { err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer) if err == nil { ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success")) - ctx.Redirect(ctx.Repo.Repository.Link()) + ctx.JSONRedirect(ctx.Repo.Repository.Link()) return } handleActionError(ctx, err) @@ -22,7 +22,7 @@ func rejectTransfer(ctx *context.Context) { err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer) if err == nil { ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected")) - ctx.Redirect(ctx.Repo.Repository.Link()) + ctx.JSONRedirect(ctx.Repo.Repository.Link()) return } handleActionError(ctx, err) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 7136b87058..2d95d5233e 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -16,10 +16,6 @@ import ( "strings" "time" - _ "image/gif" // for processing gif images - _ "image/jpeg" // for processing jpeg images - _ "image/png" // for processing png images - activities_model "code.gitea.io/gitea/models/activities" admin_model "code.gitea.io/gitea/models/admin" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -46,6 +42,9 @@ import ( _ "golang.org/x/image/bmp" // for processing bmp images _ "golang.org/x/image/webp" // for processing webp images + _ "image/gif" // for processing gif images + _ "image/jpeg" // for processing jpeg images + _ "image/png" // for processing png images ) const ( @@ -140,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) } @@ -159,7 +158,7 @@ func markupRenderToHTML(ctx *context.Context, renderCtx *markup.RenderContext, r go func() { sb := &strings.Builder{} if markup.RendererNeedPostProcess(renderer) { - escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP) // We allow NBSP here this is rendered + escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.EscapeOptionsForView()) } else { escaped = &charset.EscapeStatus{} _, _ = io.Copy(sb, markupRd) @@ -175,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) @@ -195,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 @@ -310,13 +307,15 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri return nil } - { + { // this block is for testing purpose only if timeout != 0 && !setting.IsProd && !setting.IsInTesting { log.Debug("first call to get directory file commit info") clearFilesCommitInfo := func() { log.Warn("clear directory file commit info to force async loading on frontend") for i := range files { - files[i].Commit = nil + if i%2 == 0 { // for testing purpose, only clear half of the files' commit info + files[i].Commit = nil + } } } _ = clearFilesCommitInfo diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 44bc8543b0..f6c97e83bb 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -21,12 +21,11 @@ import ( "code.gitea.io/gitea/modules/git/attribute" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" issue_service "code.gitea.io/gitea/services/issue" - - "github.com/nektos/act/pkg/model" ) func prepareLatestCommitInfo(ctx *context.Context) bool { @@ -60,8 +59,8 @@ func prepareFileViewLfsAttrs(ctx *context.Context) (*attribute.Attributes, bool) func handleFileViewRenderMarkup(ctx *context.Context, prefetchBuf []byte, utf8Reader io.Reader) bool { rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), - CurrentTreePath: path.Dir(ctx.Repo.TreePath), + CurrentRefSubURL: ctx.Repo.RefTypeNameSubURL(), + CurrentTreePath: path.Dir(ctx.Repo.TreePath), }).WithRelativePath(ctx.Repo.TreePath) renderer := rctx.DetectMarkupRenderer(prefetchBuf) @@ -78,14 +77,17 @@ func handleFileViewRenderMarkup(ctx *context.Context, prefetchBuf []byte, utf8Re return false } - ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType - var err error ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, utf8Reader) if err != nil { ctx.ServerError("Render", err) return true } + + opts, ok := markup.GetExternalRendererOptions(renderer) + usingIframe := ok && opts.DisplayInIframe + ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType + ctx.Data["RenderAsMarkup"] = util.Iif(usingIframe, "markup-iframe", "markup-inplace") return true } @@ -119,12 +121,8 @@ func handleFileViewRenderSource(ctx *context.Context, attrs *attribute.Attribute } language := attrs.GetLanguage().Value() - fileContent, lexerName, err := highlight.RenderFullFile(filename, language, buf) + fileContent, lexerName := highlight.RenderFullFile(filename, language, buf) ctx.Data["LexerName"] = lexerName - if err != nil { - log.Error("highlight.RenderFullFile failed, fallback to plain text: %v", err) - fileContent = highlight.RenderPlainText(buf) - } status := &charset.EscapeStatus{} statuses := make([]*charset.EscapeStatus, len(fileContent)) for i, line := range fileContent { @@ -188,8 +186,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { if err != nil { log.Error("actions.GetContentFromEntry: %v", err) } - _, workFlowErr := model.ReadWorkflow(bytes.NewReader(content)) - if workFlowErr != nil { + if workFlowErr := actions.ValidateWorkflowContent(content); workFlowErr != nil { ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) } } else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) { @@ -242,8 +239,6 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { case fInfo.blobOrLfsSize >= setting.UI.MaxDisplayFileSize: ctx.Data["IsFileTooLarge"] = true case handleFileViewRenderMarkup(ctx, buf, contentReader): - // it also sets ctx.Data["FileContent"] and more - ctx.Data["IsMarkup"] = true case handleFileViewRenderSource(ctx, attrs, fInfo, contentReader): // it also sets ctx.Data["FileContent"] and more ctx.Data["IsDisplayingSource"] = true diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index d1a969cf2d..036ddce6b1 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 } @@ -276,7 +276,7 @@ func handleRepoViewSubmodule(ctx *context.Context, commitSubmoduleFile *git.Comm redirectLink := submoduleWebLink.CommitWebLink if isViewHomeOnlyContent(ctx) { ctx.Resp.Header().Set("Content-Type", "text/html; charset=utf-8") - _, _ = ctx.Resp.Write([]byte(htmlutil.HTMLFormat(`%s`, redirectLink, redirectLink))) + _, _ = htmlutil.HTMLPrintf(ctx.Resp, `%s`, redirectLink, redirectLink) } else if !httplib.IsCurrentGiteaSiteURL(ctx, redirectLink) { // don't auto-redirect to external URL, to avoid open redirect or phishing ctx.Data["NotFoundPrompt"] = redirectLink diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index eba3ffc36f..79ca9efc36 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -190,21 +190,21 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), - CurrentTreePath: path.Dir(readmeFullPath), + CurrentRefSubURL: ctx.Repo.RefTypeNameSubURL(), + CurrentTreePath: path.Dir(readmeFullPath), }).WithRelativePath(readmeFullPath) renderer := rctx.DetectMarkupRenderer(buf) if renderer != nil { - ctx.Data["IsMarkup"] = true + ctx.Data["RenderAsMarkup"] = "markup-inplace" ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, rd) if err != nil { log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err) - delete(ctx.Data, "IsMarkup") + delete(ctx.Data, "RenderAsMarkup") } } - if ctx.Data["IsMarkup"] != true { + if ctx.Data["RenderAsMarkup"] == nil { ctx.Data["IsPlainText"] = true content, err := io.ReadAll(rd) if err != nil { diff --git a/routers/web/repo/watch.go b/routers/web/repo/watch.go index 70c548b8ce..616e1ee89c 100644 --- a/routers/web/repo/watch.go +++ b/routers/web/repo/watch.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/services/context" ) -const tplWatchUnwatch templates.TplName = "repo/watch_unwatch" +const tplWatchUnwatch templates.TplName = "repo/header/watch" func ActionWatch(ctx *context.Context) { err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, ctx.PathParam("action") == "watch") @@ -26,6 +26,5 @@ func ActionWatch(ctx *context.Context) { ctx.ServerError("GetRepositoryByName", err) return } - ctx.RespHeader().Add("hx-trigger", "refreshUserCards") // see the `hx-trigger="refreshUserCards ..."` comments in tmpl ctx.HTML(http.StatusOK, tplWatchUnwatch) } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index e5b07633a2..78fb0ca0af 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", @@ -258,8 +258,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { defer markupWr.Close() done := make(chan struct{}) go func() { - // We allow NBSP here this is rendered - escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP) + escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.EscapeOptionsForView()) output = template.HTML(buf.String()) buf.Reset() close(done) @@ -335,9 +334,6 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["Title"] = displayName ctx.Data["title"] = displayName - ctx.Data["Username"] = ctx.Repo.Owner.Name - ctx.Data["Reponame"] = ctx.Repo.Repository.Name - // lookup filename in wiki - get page content, gitTree entry , real filename _, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName) if noEntry { @@ -424,14 +420,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 } @@ -439,7 +435,7 @@ func WikiPost(ctx *context.Context) { return } - if !ctx.Repo.CanWrite(unit.TypeWiki) { + if !ctx.Repo.Permission.CanWrite(unit.TypeWiki) { ctx.NotFound(nil) return } @@ -448,7 +444,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": @@ -458,14 +454,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 } @@ -507,7 +503,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") @@ -545,7 +541,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 3609258440..9d8b2f08eb 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 6968a318c4..4a3c84e32a 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 29f4e9520d..c8842a67e9 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 8a2357623f..beb8832da1 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 2bd0abc4c0..b1ff9db7a2 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 } } } @@ -101,7 +101,7 @@ func FindOwnerProfileReadme(ctx *context.Context, doer *user_model.User, optProf return nil, nil } - perm, err := access_model.GetUserRepoPermission(ctx, profileDbRepo, doer) + perm, err := access_model.GetDoerRepoPermission(ctx, profileDbRepo, doer) if err != nil { log.Error("FindOwnerProfileReadme failed to GetRepositoryByName: %v", err) return nil, nil diff --git a/routers/web/swagger_json.go b/routers/web/swagger_json.go index 52f6beaf59..da41fd9013 100644 --- a/routers/web/swagger_json.go +++ b/routers/web/swagger_json.go @@ -16,3 +16,10 @@ func SwaggerV1Json(ctx *context.Context) { ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe ctx.JSONTemplate("swagger/v1_json") } + +// OpenAPI3Json render OpenAPI 3.0 json (auto-converted from Swagger 2.0) +func OpenAPI3Json(ctx *context.Context) { + ctx.Data["SwaggerAppVer"] = template.HTML(template.JSEscapeString(setting.AppVer)) + ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe + ctx.JSONTemplate("swagger/v1_openapi3_json") +} diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 21ca0fc683..12fb1ee71b 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]) } @@ -655,6 +655,9 @@ func ShowSSHKeys(ctx *context.Context) { // "authorized_keys" file format: "#" followed by comment line per key buf.WriteString("# Gitea isn't a key server. The keys are exported as the user uploaded and might not have been fully verified.\n") for i := range keys { + if keys[i].Type == asymkey_model.KeyTypePrincipal { + continue // SSH principal keys are not for signing or authentication + } buf.WriteString(keys[i].OmitEmail()) buf.WriteString("\n") } diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 3b7ecd062b..8133388c5d 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/package.go b/routers/web/user/package.go index 2dad5be554..d25dc45ba8 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -8,6 +8,7 @@ import ( "errors" "net/http" "net/url" + "time" "code.gitea.io/gitea/models/db" org_model "code.gitea.io/gitea/models/organization" @@ -18,13 +19,13 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" alpine_module "code.gitea.io/gitea/modules/packages/alpine" arch_module "code.gitea.io/gitea/modules/packages/arch" container_module "code.gitea.io/gitea/modules/packages/container" debian_module "code.gitea.io/gitea/modules/packages/debian" rpm_module "code.gitea.io/gitea/modules/packages/rpm" + terraform_module "code.gitea.io/gitea/modules/packages/terraform" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" @@ -35,6 +36,8 @@ import ( "code.gitea.io/gitea/services/forms" packages_service "code.gitea.io/gitea/services/packages" container_service "code.gitea.io/gitea/services/packages/container" + + "github.com/google/uuid" ) const ( @@ -84,9 +87,9 @@ func ListPackages(ctx *context.Context) { continue } - permission, err := access_model.GetUserRepoPermission(ctx, pd.Repository, ctx.Doer) + permission, err := access_model.GetDoerRepoPermission(ctx, pd.Repository, ctx.Doer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.ServerError("GetDoerRepoPermission", err) return } repositoryAccessMap[pd.Repository.ID] = permission.HasAnyUnitAccess() @@ -315,14 +318,19 @@ func ViewPackageVersion(ctx *context.Context) { } ctx.Data["LatestVersions"] = pvs ctx.Data["TotalVersionCount"] = pvsTotal + ctx.Data["PackageVersionViewData"], err = packages_service.GetSpecManager().Get(pd.Package.Type).GetViewPackageVersionData(ctx, pd) + if err != nil { + ctx.ServerError("GetViewPackageVersionData", err) + return + } ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() hasRepositoryAccess := false if pd.Repository != nil { - permission, err := access_model.GetUserRepoPermission(ctx, pd.Repository, ctx.Doer) + permission, err := access_model.GetDoerRepoPermission(ctx, pd.Repository, ctx.Doer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.ServerError("GetDoerRepoPermission", err) return } hasRepositoryAccess = permission.HasAnyUnitAccess() @@ -491,20 +499,52 @@ func packageSettingsPostActionLink(ctx *context.Context, form *forms.PackageSett } func packageSettingsPostActionDelete(ctx *context.Context) { - err := packages_service.RemovePackageVersion(ctx, ctx.Doer, ctx.Package.Descriptor.Version) - if err != nil { - log.Error("Error deleting package: %v", err) - ctx.Flash.Error(ctx.Tr("packages.settings.delete.error")) + pd := ctx.Package.Descriptor + + if ctx.FormString("package_name") != pd.Package.Name { + ctx.Flash.Error(ctx.Tr("packages.settings.delete.invalid_package_name")) + ctx.Redirect(pd.PackageSettingsLink()) + return + } + if err := packages_service.RemovePackage(ctx, ctx.Doer, pd.Package); err != nil { + errTr := util.ErrorAsTranslatable(err) + if errTr == nil { + ctx.ServerError("RemovePackage", err) + return + } + ctx.Flash.Error(errTr.Translate(ctx.Locale)) + ctx.Redirect(pd.PackageSettingsLink()) + return + } + + ctx.Flash.Success(ctx.Tr("packages.settings.delete.success")) + ctx.Redirect(ctx.Package.Owner.HomeLink() + "/-/packages") +} + +// PackageVersionDelete deletes a package version +func PackageVersionDelete(ctx *context.Context) { + pd := ctx.Package.Descriptor + if pd.Version == nil { + ctx.NotFound(nil) + return + } + + if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pd.Version); err != nil { + errTr := util.ErrorAsTranslatable(err) + if errTr == nil { + ctx.ServerError("RemovePackageVersion", err) + return + } + ctx.Flash.Error(errTr.Translate(ctx.Locale)) } else { - ctx.Flash.Success(ctx.Tr("packages.settings.delete.success")) + ctx.Flash.Success(ctx.Tr("packages.settings.delete.version.success")) } - redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages" // redirect to the package if there are still versions available - if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: optional.Some(false)}); has { - redirectURL = ctx.Package.Descriptor.PackageWebLink() + redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages" + if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: pd.Package.ID, IsInternal: optional.Some(false)}); has { + redirectURL = pd.PackageWebLink() } - ctx.Redirect(redirectURL) } @@ -512,7 +552,7 @@ func packageSettingsPostActionDelete(ctx *context.Context) { func DownloadPackageFile(ctx *context.Context) { pf, err := packages_model.GetFileForVersionByID(ctx, ctx.Package.Descriptor.Version.ID, ctx.PathParamInt64("fileid")) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { ctx.NotFound(err) } else { ctx.ServerError("GetFileForVersionByID", err) @@ -526,5 +566,62 @@ func DownloadPackageFile(ctx *context.Context) { return } - packages_helper.ServePackageFile(ctx, s, u, pf) + packages_helper.ServePackageFile(ctx, s, u, pf, httplib.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + ContentDisposition: httplib.ContentDispositionAttachment, + }) +} + +// ActionPackageTerraformLock locks a terraform state +func ActionPackageTerraformLock(ctx *context.Context) { + pd := ctx.Package.Descriptor + if pd.Package.Type != packages_model.TypeTerraformState { + ctx.NotFound(nil) + return + } + + existingLock, err := terraform_module.GetLock(ctx, pd.Package.ID) + if err != nil { + ctx.ServerError("GetLock", err) + return + } + if existingLock.IsLocked() { + ctx.Flash.Error(ctx.Tr("packages.terraform.lock.error.already_locked")) + ctx.Redirect(pd.VersionWebLink()) + return + } + + lockID := uuid.New().String() + lockInfo := &terraform_module.LockInfo{ + ID: lockID, + Operation: "Manual UI Lock", + Who: ctx.Doer.Name, + Created: time.Now(), + } + + if err := terraform_module.SetLock(ctx, pd.Package.ID, lockInfo); err != nil { + ctx.ServerError("SetLock", err) + return + } + + ctx.Flash.Success(ctx.Tr("packages.terraform.lock.success")) + ctx.Redirect(pd.VersionWebLink()) +} + +// ActionPackageTerraformUnlock unlocks a terraform state +func ActionPackageTerraformUnlock(ctx *context.Context) { + pd := ctx.Package.Descriptor + if pd.Package.Type != packages_model.TypeTerraformState { + ctx.NotFound(nil) + return + } + + if err := terraform_module.RemoveLock(ctx, pd.Package.ID); err != nil { + ctx.ServerError("RemoveLock", err) + return + } + + ctx.Flash.Success(ctx.Tr("packages.terraform.unlock.success")) + ctx.Redirect(pd.VersionWebLink()) } diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index faf2f442a2..b1d00520c2 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -251,7 +251,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R log.Error("failed to GetBlobContent: %v", err) } else { rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{ - CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), + CurrentRefSubURL: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), }) if profileContent, err := markdown.RenderString(rctx, bytes); err != nil { log.Error("failed to RenderString: %v", err) diff --git a/routers/web/user/setting/block.go b/routers/web/user/setting/block.go index 3a1625ccf9..1376c0417d 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/user/setting/packages.go b/routers/web/user/setting/packages.go index 62b0240642..66aa241377 100644 --- a/routers/web/user/setting/packages.go +++ b/routers/web/user/setting/packages.go @@ -112,7 +112,7 @@ func RegenerateChefKeyPair(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(priv), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(priv), context.ServeHeaderOptions{ ContentType: "application/x-pem-file", Filename: ctx.Doer.Name + ".priv", }) diff --git a/routers/web/web.go b/routers/web/web.go index 011b724366..c4cf0f48de 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" @@ -45,7 +46,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" - _ "code.gitea.io/gitea/modules/session" // to registers all internal adapters + _ "code.gitea.io/gitea/modules/session" // to register all internal adapters "gitea.com/go-chi/captcha" chi_middleware "github.com/go-chi/chi/v5/middleware" @@ -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,7 +261,8 @@ 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, OPTIONS", "/assets/*", optionsCorsHandler(), public.FileHandlerFunc()) + 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)) routes.Methods("GET, HEAD", "/apple-touch-icon.png", misc.StaticRedirect("/assets/img/apple-touch-icon.png")) @@ -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() { @@ -1077,14 +1079,19 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("/versions", user.ListPackageVersions) m.Group("/{version}", func() { m.Get("", user.ViewPackageVersion) + m.Post("", reqPackageAccess(perm.AccessModeWrite), user.PackageVersionDelete) m.Get("/{version_sub}", user.ViewPackageVersion) - m.Get("/files/{fileid}", user.DownloadPackageFile) - m.Group("/settings", func() { - m.Get("", user.PackageSettings) - m.Post("", web.Bind(forms.PackageSettingForm{}), user.PackageSettingsPost) + m.Group("/terraform", func() { + m.Post("/lock", user.ActionPackageTerraformLock) + m.Post("/unlock", user.ActionPackageTerraformUnlock) }, reqPackageAccess(perm.AccessModeWrite)) + m.Get("/files/{fileid}", user.DownloadPackageFile) }) }) + m.Group("/settings/{type}/{name}", func() { + m.Get("", user.PackageSettings) + m.Post("", web.Bind(forms.PackageSettingForm{}), user.PackageSettingsPost) + }, reqPackageAccess(perm.AccessModeWrite)) }, context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) } @@ -1174,7 +1181,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Combo("/edit").Get(repo_setting.SettingsProtectedBranch). Post(web.Bind(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo_setting.SettingsProtectedBranchPost) m.Post("/{id}/delete", repo_setting.DeleteProtectedBranchRulePost) - m.Post("/priority", web.Bind(forms.ProtectBranchPriorityForm{}), context.RepoMustNotBeArchived(), repo_setting.UpdateBranchProtectionPriories) + m.Post("/priority", context.RepoMustNotBeArchived(), repo_setting.UpdateBranchProtectionPriories) }) m.Group("/tags", func() { @@ -1356,6 +1363,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel) m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone) m.Post("/projects", reqRepoIssuesOrPullsWriter, reqRepoProjectsReader, repo.UpdateIssueProject) + m.Post("/projects/column", reqRepoIssuesOrPullsWriter, reqRepoProjectsWriter, repo.UpdateIssueProjectColumn) m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee) m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus) m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues) @@ -1539,6 +1547,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). @@ -1711,7 +1724,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("/forks", repo.Forks) m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff) - m.Post("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit) + m.Get("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit) }, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{reponame}": repo code @@ -1744,9 +1757,10 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { if setting.API.EnableSwagger { m.Get("/swagger.v1.json", SwaggerV1Json) + m.Get("/openapi3.v1.json", OpenAPI3Json) } - if !setting.IsProd { + if !setting.IsProd || setting.IsInE2eTesting() { m.Group("/devtest", func() { m.Any("", devtest.List) m.Any("/fetch-action-test", devtest.FetchActionTest) @@ -1754,8 +1768,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 0000000000..552b055b70 --- /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/artifacts.go b/services/actions/artifacts.go new file mode 100644 index 0000000000..4884eb42e8 --- /dev/null +++ b/services/actions/artifacts.go @@ -0,0 +1,65 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "net/http" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/services/context" +) + +// IsArtifactV4 detects whether the artifact is likely from v4. +// V4 backend stores the files as a single combined zip file per artifact, and ensures ContentEncoding contains a slash +// (otherwise this uses application/zip instead of the custom mime type), which is not the case for the old backend. +func IsArtifactV4(art *actions_model.ActionArtifact) bool { + return strings.Contains(art.ContentEncodingOrType, "/") +} + +func GetArtifactV4ServeDirectURL(art *actions_model.ActionArtifact, method string) (string, error) { + contentType := art.ContentEncodingOrType + u, err := storage.ActionsArtifacts.ServeDirectURL(art.StoragePath, art.ArtifactPath, method, &storage.ServeDirectOptions{ContentType: contentType}) + if err != nil { + return "", err + } + return u.String(), nil +} + +func DownloadArtifactV4ServeDirect(ctx *context.Base, art *actions_model.ActionArtifact) bool { + if !setting.Actions.ArtifactStorage.ServeDirect() { + return false + } + u, err := GetArtifactV4ServeDirectURL(art, ctx.Req.Method) + if err != nil { + log.Error("GetArtifactV4ServeDirectURL: %v", err) + return false + } + ctx.Redirect(u, http.StatusFound) + return true +} + +func DownloadArtifactV4ReadStorage(ctx *context.Base, art *actions_model.ActionArtifact) error { + f, err := storage.ActionsArtifacts.Open(art.StoragePath) + if err != nil { + return err + } + defer f.Close() + httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, f, httplib.ServeHeaderOptions{ + Filename: art.ArtifactPath, + ContentType: art.ContentEncodingOrType, // v4 guarantees that the field is Content-Type + }) + return nil +} + +func DownloadArtifactV4(ctx *context.Base, art *actions_model.ActionArtifact) error { + if DownloadArtifactV4ServeDirect(ctx, art) { + return nil + } + return DownloadArtifactV4ReadStorage(ctx, art) +} diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go index d0cc63e538..5f605cd265 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, @@ -246,6 +250,8 @@ func DeleteRun(ctx context.Context, run *actions_model.ActionRun) error { return err } + actions_model.UpdateRepoRunsNumbers(ctx, repoID) + // Delete files on storage for _, tas := range tasks { removeTaskLog(ctx, tas) diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index e49bda1b16..940f1d8454 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,34 +35,16 @@ func StopEndlessTasks(ctx context.Context) error { }) } -func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.ActionRunJob) { - if len(jobs) == 0 { - return - } - 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 job := jobs[0]; job.Run != nil && job.Run.Repo != nil { - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.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 } @@ -78,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 { @@ -170,7 +149,7 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error { remove() } - notifyWorkflowJobStatusUpdate(ctx, jobs) + NotifyWorkflowJobsAndRunsStatusUpdate(ctx, jobs) EmitJobsIfReadyByJobs(jobs) return nil @@ -189,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 { @@ -206,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) @@ -217,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 95b848f4fb..76c11da7cb 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 fmt.Sprintf("Cancelled after %s", job.Duration()) + case actions_model.StatusSkipped: + return "Skipped" + case actions_model.StatusRunning: + return "In progress" + 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 0000000000..fa95a46383 --- /dev/null +++ b/services/actions/commit_status_test.go @@ -0,0 +1,158 @@ +// 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" + actions_module "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/commitstatus" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCommitStatusDescription(t *testing.T) { + cases := []struct { + status actions_model.Status + started, stopped timeutil.TimeStamp + want string + }{ + {actions_model.StatusSuccess, 100, 102, "Successful in 2s"}, + {actions_model.StatusFailure, 100, 130, "Failing after 30s"}, + {actions_model.StatusCancelled, 100, 145, "Cancelled after 45s"}, + {actions_model.StatusSkipped, 0, 0, "Skipped"}, + {actions_model.StatusRunning, 0, 0, "In progress"}, + {actions_model.StatusWaiting, 0, 0, "Waiting to run"}, + {actions_model.StatusBlocked, 0, 0, "Blocked by required conditions"}, + {actions_model.StatusUnknown, 0, 0, "Unknown status: 0"}, + } + for _, tc := range cases { + job := &actions_model.ActionRunJob{Status: tc.status, Started: tc.started, Stopped: tc.stopped} + assert.Equal(t, tc.want, toCommitStatusDescription(job), tc.status.String()) + } +} + +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, "In progress", 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 TestGetCommitActionsStatusMap(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + branch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo.ID, Name: repo.DefaultBranch}) + + run := &actions_model.ActionRun{ + RepoID: repo.ID, Repo: repo, OwnerID: repo.OwnerID, TriggerUserID: repo.OwnerID, + WorkflowID: "test.yaml", CommitSHA: branch.CommitID, + } + require.NoError(t, db.Insert(t.Context(), run)) + + cases := []struct { + jobName string + status actions_model.Status + }{ + {"running-job", actions_model.StatusRunning}, + {"waiting-job", actions_model.StatusWaiting}, + {"unknown-job", actions_model.StatusUnknown}, + } + for _, tc := range cases { + job := &actions_model.ActionRunJob{ + RunID: run.ID, RepoID: repo.ID, OwnerID: repo.OwnerID, Name: tc.jobName, Status: tc.status, + } + require.NoError(t, db.Insert(t.Context(), job)) + require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, run, job)) + } + + statuses, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, branch.CommitID, db.ListOptionsAll) + require.NoError(t, err) + + info := actions_module.GetCommitActionsStatusMap(t.Context(), statuses) + got := map[string]string{} + for _, s := range statuses { + got[s.Context] = info.IconStatus(s) + } + for _, tc := range cases { + key := "test.yaml / " + tc.jobName + " (push)" + want := tc.status.String() + assert.Equal(t, want, got[key], "icon status for %s", tc.jobName) + } + + // Nil receiver returns "" without panicking — used by callers that skip enrichment. + var nilInfo actions_module.CommitActionsStatusMap + assert.Empty(t, nilInfo.IconStatus(statuses[0])) +} + +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 878e5c483b..990c0e9a0b 100644 --- a/services/actions/concurrency.go +++ b/services/actions/concurrency.go @@ -12,20 +12,20 @@ import ( "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" - act_model "github.com/nektos/act/pkg/model" + act_model "gitea.com/gitea/runner/act/model" "go.yaml.in/yaml/v4" ) // 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 626ae6ee6b..1d4a08459d 100644 --- a/services/actions/context.go +++ b/services/actions/context.go @@ -14,17 +14,23 @@ 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" - "github.com/nektos/act/pkg/model" + "gitea.com/gitea/runner/act/model" ) 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 74ef694021..d86ec47a3c 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 "gitea.com/gitea/runner/act/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/init_test.go b/services/actions/init_test.go index e61b3759e1..4db765839e 100644 --- a/services/actions/init_test.go +++ b/services/actions/init_test.go @@ -35,7 +35,7 @@ func TestInitToken(t *testing.T) { }) t.Run("EnvToken", func(t *testing.T) { - tokenValue, _ := util.CryptoRandomString(32) + tokenValue := util.CryptoRandomString(32) t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", tokenValue) t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "") err := initGlobalRunnerToken(t.Context()) @@ -52,7 +52,7 @@ func TestInitToken(t *testing.T) { }) t.Run("EnvFileToken", func(t *testing.T) { - tokenValue, _ := util.CryptoRandomString(32) + tokenValue := util.CryptoRandomString(32) f := t.TempDir() + "/token" _ = os.WriteFile(f, []byte(tokenValue), 0o644) t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "") diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index 20a4f81eab..b81ec9fe6c 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,105 +116,148 @@ 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]) - // check run (workflow-level) concurrency - if run.ConcurrencyGroup != "" { - concurrentRun, err := findBlockedRunByConcurrency(ctx, run.RepoID, run.ConcurrencyGroup) + collect := func(concurrencyGroup string) error { + concurrentRunID, err := findBlockedRunIDByConcurrency(ctx, run.RepoID, concurrencyGroup) if err != nil { - return nil, nil, fmt.Errorf("find blocked run by concurrency: %w", err) + 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 nil, nil, err + return err } jobs = append(jobs, js...) updatedJobs = append(updatedJobs, ujs...) + cancelledJobs = append(cancelledJobs, cjs...) + } + checkedConcurrencyGroup.Add(concurrencyGroup) + return nil + } + + // check run (workflow-level) concurrency + 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 } - checkedConcurrencyGroup.Add(run.ConcurrencyGroup) } // 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() { continue } - if job.ConcurrencyGroup == "" && checkedConcurrencyGroup.Contains(job.ConcurrencyGroup) { + if job.ConcurrencyGroup == "" || checkedConcurrencyGroup.Contains(job.ConcurrencyGroup) { continue } - concurrentRun, err := findBlockedRunByConcurrency(ctx, job.RepoID, job.ConcurrencyGroup) - if err != nil { - return nil, nil, fmt.Errorf("find blocked run by concurrency: %w", err) + if err := collect(job.ConcurrencyGroup); err != nil { + return nil, nil, nil, err } - if concurrentRun != nil && !concurrentRun.NeedApproval { - js, ujs, err := checkJobsOfRun(ctx, concurrentRun) - if err != nil { - return nil, nil, err - } - jobs = append(jobs, js...) - updatedJobs = append(updatedJobs, ujs...) - } - checkedConcurrencyGroup.Add(job.ConcurrencyGroup) } - 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 + } + // The resolver below only considers needs and job-level concurrency, so a run blocked + // solely by run-level concurrency would have its jobs unblocked here. checkRunConcurrency + // re-evaluates when the holding run finishes. + if run.Status.IsBlocked() { + attempt, has, err := run.GetLatestAttempt(ctx) + if err != nil { + return nil, nil, nil, fmt.Errorf("GetLatestAttempt: %w", err) + } + if has { + shouldBlock, err := shouldBlockRunByConcurrency(ctx, attempt) + if err != nil { + return nil, nil, nil, fmt.Errorf("shouldBlockRunByConcurrency: %w", err) + } + if shouldBlock { + return jobs, nil, nil, nil + } + } } 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 @@ -226,26 +271,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 { @@ -344,9 +381,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...) } } @@ -362,8 +402,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 a2152fb270..9a40927e06 100644 --- a/services/actions/job_emitter_test.go +++ b/services/actions/job_emitter_test.go @@ -7,6 +7,8 @@ import ( "testing" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" ) @@ -134,3 +136,160 @@ jobs: }) } } + +// Test_checkRunConcurrency_NoDuplicateConcurrencyGroupCheck verifies that when a run's +// ConcurrencyGroup has already been checked at the run level, the same group is not +// re-checked for individual jobs. +func Test_checkRunConcurrency_NoDuplicateConcurrencyGroupCheck(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + // 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, + RunID: runA.ID, + Attempt: 1, + Status: actions_model.StatusRunning, + ConcurrencyGroup: "test-cg", + } + 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", + Name: "job1", + Status: actions_model.StatusSuccess, + ConcurrencyGroup: "test-cg", + } + assert.NoError(t, db.Insert(ctx, jobADone)) + + // 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, + } + 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, + RunAttemptID: runBAttempt.ID, + AttemptJobID: 1, + RepoID: 4, + OwnerID: 1, + JobID: "job1", + Name: "job1", + Status: actions_model.StatusBlocked, + } + assert.NoError(t, db.Insert(ctx, jobBBlocked)) + + 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) { + assert.Equal(t, jobBBlocked.ID, jobs[0].ID) + } +} + +// Test_checkJobsOfCurrentRunAttempt_RunLevelConcurrencyKeepsJobsBlocked verifies that +// the resolver does not transition a job out of Blocked while another run still holds +// the workflow-level concurrency group. Regression for #37446. +func Test_checkJobsOfCurrentRunAttempt_RunLevelConcurrencyKeepsJobsBlocked(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + const group = "test-run-level-concurrency-keeps-blocked" + + // Holder run: Running attempt in the concurrency group. + holderRun := &actions_model.ActionRun{ + RepoID: 4, OwnerID: 1, TriggerUserID: 1, + WorkflowID: "test.yml", Index: 9911, Ref: "refs/heads/main", + Status: actions_model.StatusRunning, + } + assert.NoError(t, db.Insert(ctx, holderRun)) + holderAttempt := &actions_model.ActionRunAttempt{ + RepoID: 4, RunID: holderRun.ID, Attempt: 1, + Status: actions_model.StatusRunning, ConcurrencyGroup: group, + } + assert.NoError(t, db.Insert(ctx, holderAttempt)) + _, err := db.Exec(ctx, "UPDATE `action_run` SET latest_attempt_id = ? WHERE id = ?", holderAttempt.ID, holderRun.ID) + assert.NoError(t, err) + + // Blocked run: Blocked attempt in the same group, with one Blocked job that has + // no needs and no job-level concurrency. Without the run-level guard in + // checkJobsOfCurrentRunAttempt, the resolver would transition this job to Waiting. + blockedRun := &actions_model.ActionRun{ + RepoID: 4, OwnerID: 1, TriggerUserID: 1, + WorkflowID: "test.yml", Index: 9912, Ref: "refs/heads/main", + Status: actions_model.StatusBlocked, + } + assert.NoError(t, db.Insert(ctx, blockedRun)) + blockedAttempt := &actions_model.ActionRunAttempt{ + RepoID: 4, RunID: blockedRun.ID, Attempt: 1, + Status: actions_model.StatusBlocked, ConcurrencyGroup: group, + } + assert.NoError(t, db.Insert(ctx, blockedAttempt)) + _, err = db.Exec(ctx, "UPDATE `action_run` SET latest_attempt_id = ? WHERE id = ?", blockedAttempt.ID, blockedRun.ID) + assert.NoError(t, err) + blockedRun.LatestAttemptID = blockedAttempt.ID + blockedJob := &actions_model.ActionRunJob{ + RunID: blockedRun.ID, RunAttemptID: blockedAttempt.ID, AttemptJobID: 1, + RepoID: 4, OwnerID: 1, JobID: "job1", Name: "job1", + Status: actions_model.StatusBlocked, + WorkflowPayload: []byte(` +name: test +on: push +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo +`), + } + assert.NoError(t, db.Insert(ctx, blockedJob)) + + _, updated, _, err := checkJobsOfCurrentRunAttempt(ctx, blockedRun) + assert.NoError(t, err) + assert.Empty(t, updated) + + refreshed := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: blockedJob.ID}) + assert.Equal(t, actions_model.StatusBlocked, refreshed.Status) +} diff --git a/services/actions/notifier.go b/services/actions/notifier.go index c4bfe5c11b..4b2e87afad 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -5,6 +5,7 @@ package actions import ( "context" + "errors" actions_model "code.gitea.io/gitea/models/actions" issues_model "code.gitea.io/gitea/models/issues" @@ -20,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/convert" notify_service "code.gitea.io/gitea/services/notify" @@ -47,7 +49,7 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu log.Error("issue.LoadPoster: %v", err) return } - permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) + permission, _ := access_model.GetIndividualUserRepoPermission(ctx, issue.Repo, issue.Poster) newNotifyInputFromIssue(issue, webhook_module.HookEventIssues).WithPayload(&api.IssuePayload{ Action: api.HookIssueOpened, @@ -76,7 +78,7 @@ func (n *actionsNotifier) notifyIssueChangeWithTitleOrContent(ctx context.Contex return } - permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) + permission, _ := access_model.GetIndividualUserRepoPermission(ctx, issue.Repo, issue.Poster) if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { log.Error("loadPullRequest: %v", err) @@ -110,7 +112,7 @@ func (n *actionsNotifier) notifyIssueChangeWithTitleOrContent(ctx context.Contex // IssueChangeStatus notifies close or reopen issue to notifiers func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, _ *issues_model.Comment, isClosed bool) { ctx = withMethod(ctx, "IssueChangeStatus") - permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) + permission, _ := access_model.GetIndividualUserRepoPermission(ctx, issue.Repo, issue.Poster) if issue.IsPull { if err := issue.LoadPullRequest(ctx); err != nil { log.Error("LoadPullRequest: %v", err) @@ -259,7 +261,7 @@ func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues return } - permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) + permission, _ := access_model.GetIndividualUserRepoPermission(ctx, issue.Repo, issue.Poster) payload := &api.IssuePayload{ Action: action, Index: issue.Index, @@ -322,7 +324,7 @@ func notifyIssueCommentChange(ctx context.Context, doer *user_model.User, commen return } - permission, _ := access_model.GetUserRepoPermission(ctx, comment.Issue.Repo, doer) + permission, _ := access_model.GetDoerRepoPermission(ctx, comment.Issue.Repo, doer) payload := &api.IssueCommentPayload{ Action: action, @@ -376,7 +378,7 @@ func (n *actionsNotifier) NewPullRequest(ctx context.Context, pull *issues_model return } - permission, _ := access_model.GetUserRepoPermission(ctx, pull.Issue.Repo, pull.Issue.Poster) + permission, _ := access_model.GetIndividualUserRepoPermission(ctx, pull.Issue.Repo, pull.Issue.Poster) newNotifyInputFromIssue(pull.Issue, webhook_module.HookEventPullRequest). WithPayload(&api.PullRequestPayload{ @@ -404,8 +406,8 @@ func (n *actionsNotifier) CreateRepository(ctx context.Context, doer, u *user_mo func (n *actionsNotifier) ForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) { ctx = withMethod(ctx, "ForkRepository") - oldPermission, _ := access_model.GetUserRepoPermission(ctx, oldRepo, doer) - permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer) + oldPermission, _ := access_model.GetDoerRepoPermission(ctx, oldRepo, doer) + permission, _ := access_model.GetDoerRepoPermission(ctx, repo, doer) // forked webhook newNotifyInput(oldRepo, doer, webhook_module.HookEventFork).WithPayload(&api.ForkPayload{ @@ -452,9 +454,9 @@ func (n *actionsNotifier) PullRequestReview(ctx context.Context, pr *issues_mode return } - permission, err := access_model.GetUserRepoPermission(ctx, review.Issue.Repo, review.Issue.Poster) + permission, err := access_model.GetIndividualUserRepoPermission(ctx, review.Issue.Repo, review.Issue.Poster) if err != nil { - log.Error("models.GetUserRepoPermission: %v", err) + log.Error("models.GetIndividualUserRepoPermission: %v", err) return } @@ -481,7 +483,7 @@ func (n *actionsNotifier) PullRequestReviewRequest(ctx context.Context, doer *us ctx = withMethod(ctx, "PullRequestReviewRequest") - permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) + permission, _ := access_model.GetDoerRepoPermission(ctx, issue.Repo, doer) if err := issue.LoadPullRequest(ctx); err != nil { log.Error("LoadPullRequest failed: %v", err) return @@ -525,9 +527,9 @@ func (*actionsNotifier) MergePullRequest(ctx context.Context, doer *user_model.U return } - permission, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, doer) + permission, err := access_model.GetDoerRepoPermission(ctx, pr.Issue.Repo, doer) if err != nil { - log.Error("models.GetUserRepoPermission: %v", err) + log.Error("models.GetDoerRepoPermission: %v", err) return } @@ -723,7 +725,7 @@ func (n *actionsNotifier) PullRequestChangeTargetBranch(ctx context.Context, doe return } - permission, _ := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, pr.Issue.Poster) + permission, _ := access_model.GetIndividualUserRepoPermission(ctx, pr.Issue.Repo, pr.Issue.Poster) newNotifyInput(pr.Issue.Repo, doer, webhook_module.HookEventPullRequest). WithPayload(&api.PullRequestPayload{ Action: api.HookIssueEdited, @@ -805,23 +807,29 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep } defer gitRepo.Close() - convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) + convertedWorkflow, err := convert.GetActionWorkflowByRef(ctx, gitRepo, repo, run.WorkflowID, git.RefName(run.Ref)) + if err != nil && errors.Is(err, util.ErrNotExist) { + convertedWorkflow, err = convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) + } if err != nil { log.Error("GetActionWorkflow: %v", err) return } - convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run) + run.Repo = repo + convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil) if err != nil { log.Error("ToActionWorkflowRun: %v", err) return } - newNotifyInput(repo, sender, webhook_module.HookEventWorkflowRun).WithPayload(&api.WorkflowRunPayload{ - Action: status, - Workflow: convertedWorkflow, - WorkflowRun: convertedRun, - Organization: org, - Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), - Sender: convert.ToUser(ctx, sender, nil), - }).Notify(ctx) + newNotifyInput(repo, sender, webhook_module.HookEventWorkflowRun). + WithRef(git.RefNameFromBranch(repo.DefaultBranch).String()). + WithPayload(&api.WorkflowRunPayload{ + Action: status, + Workflow: convertedWorkflow, + WorkflowRun: convertedRun, + Organization: org, + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), + Sender: convert.ToUser(ctx, sender, nil), + }).Notify(ctx) } diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 10b36a5a52..05f7f918df 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -28,7 +28,7 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/convert" - "github.com/nektos/act/pkg/model" + "gitea.com/gitea/runner/act/model" ) type methodCtxKeyType struct{} @@ -256,7 +256,7 @@ func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) log.Debug("repo %s: skipped run for pr %v because of %s string", input.Repo.RelativePath(), input.PullRequest.Issue.ID, s) return true } - if strings.Contains(commit.CommitMessage, s) { + if strings.Contains(commit.MessageRaw, s) { log.Debug("repo %s with commit %s: skipped run because of %s string", input.Repo.RelativePath(), commit.ID, s) return true } @@ -320,7 +320,7 @@ func handleWorkflows( for _, dwf := range detectedWorkflows { run := &actions_model.ActionRun{ - Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0], + Title: commit.MessageTitle(), RepoID: input.Repo.ID, Repo: input.Repo, OwnerID: input.Repo.OwnerID, @@ -362,7 +362,7 @@ func notifyRelease(ctx context.Context, doer *user_model.User, rel *repo_model.R return } - permission, _ := access_model.GetUserRepoPermission(ctx, rel.Repo, doer) + permission, _ := access_model.GetDoerRepoPermission(ctx, rel.Repo, doer) newNotifyInput(rel.Repo, doer, webhook_module.HookEventRelease). WithRef(git.RefNameFromTag(rel.TagName).String()). @@ -413,8 +413,8 @@ func ifNeedApproval(ctx context.Context, run *actions_model.ActionRun, repo *rep } // don't need approval if the user can write - if perm, err := access_model.GetUserRepoPermission(ctx, repo, user); err != nil { - return false, fmt.Errorf("GetUserRepoPermission: %w", err) + if perm, err := access_model.GetDoerRepoPermission(ctx, repo, user); err != nil { + return false, fmt.Errorf("GetDoerRepoPermission: %w", err) } else if perm.CanWrite(unit_model.TypeActions) { log.Trace("do not need approval because user %d can write", user.ID) return false, nil @@ -483,7 +483,7 @@ func handleSchedules( } run := &actions_model.ActionSchedule{ - Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0], + Title: commit.MessageTitle(), RepoID: input.Repo.ID, Repo: input.Repo, OwnerID: input.Repo.OwnerID, diff --git a/services/actions/notify.go b/services/actions/notify.go new file mode 100644 index 0000000000..21ca8c7a14 --- /dev/null +++ b/services/actions/notify.go @@ -0,0 +1,148 @@ +// 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) + + // Recomputes the repository's num_action_runs / num_closed_action_runs counters since the run's status changed + actions_model.UpdateRepoRunsNumbers(ctx, run.RepoID) +} + +// 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 1596d9bfc5..d4027b7c02 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" + "gitea.com/gitea/runner/act/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 3b4dc5483f..3077298061 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 e9fcdcaf43..ed3b3728f7 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 "gitea.com/gitea/runner/act/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 037bf5cddd..3a0dff490a 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,32 @@ 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 == "" { + return eventPayload + } + + // eventPayload originates from json.Marshal(input.Payload) in handleSchedules, + // so a nil payload is stored as the literal "null" and pre-existing rows may be + // empty. Both cases start from a fresh map so the schedule field can still be set. + var event map[string]any + if eventPayload != "" { + if err := json.Unmarshal([]byte(eventPayload), &event); err != nil { + log.Error("withScheduleInEventPayload: unmarshal: %v", err) + return eventPayload + } + } + if event == nil { + event = map[string]any{} + } + + 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 0000000000..f2c7e656e6 --- /dev/null +++ b/services/actions/schedule_tasks_test.go @@ -0,0 +1,52 @@ +// 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("adds schedule to null payload", func(t *testing.T) { + updated := withScheduleInEventPayload("null", "37 12 5 1 2") + + event := map[string]any{} + assert.NoError(t, json.Unmarshal([]byte(updated), &event)) + assert.Equal(t, "37 12 5 1 2", event["schedule"]) + }) + + t.Run("adds schedule to empty payload", func(t *testing.T) { + updated := withScheduleInEventPayload("", "37 12 5 1 2") + + event := map[string]any{} + assert.NoError(t, json.Unmarshal([]byte(updated), &event)) + assert.Equal(t, "37 12 5 1 2", event["schedule"]) + }) + + 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 a21b600998..9dc3c9a34b 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,18 +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.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 57e6af1d9b..3593caa2c5 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/actions/workflow.go b/services/actions/workflow.go index b41741403f..e60ae1ec2e 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -5,7 +5,6 @@ package actions import ( "fmt" - "strings" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/perm" @@ -22,7 +21,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" - "github.com/nektos/act/pkg/model" + "gitea.com/gitea/runner/act/model" "go.yaml.in/yaml/v4" ) @@ -98,7 +97,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re var entry *git.TreeEntry run := &actions_model.ActionRun{ - Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0], + Title: runTargetCommit.MessageTitle(), RepoID: repo.ID, Repo: repo, OwnerID: repo.OwnerID, diff --git a/services/agit/agit.go b/services/agit/agit.go index fa2ddd9baf..c7c46651c0 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -150,17 +150,13 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. if err != nil { return nil, fmt.Errorf("failed to get commit %s in repository: %s Error: %w", opts.NewCommitIDs[i], repo.FullName(), err) } - } - - // create a new pull request - if title == "" { - title = strings.Split(commit.CommitMessage, "\n")[0] - } - if description == "" { - _, description, _ = strings.Cut(commit.CommitMessage, "\n\n") - } - if description == "" { - description = title + // create a new pull request + if title == "" { + title = commit.MessageTitle() + } + if description == "" { + description = commit.MessageBody() + } } prIssue := &issues_model.Issue{ @@ -282,12 +278,15 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. if err != nil { return nil, fmt.Errorf("failed to load pull issue. Error: %w", err) } - comment, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i], forcePush.Value()) - if err == nil && comment != nil { + + isForcePush := forcePush.Value() + comment, commentCreated, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i], isForcePush) + if err != nil { + log.Error("CreatePushPullComment: %v", err) + } else if commentCreated { notify_service.PullRequestPushCommits(ctx, pusher, pr, comment) } notify_service.PullRequestSynchronized(ctx, pusher, pr) - isForcePush := comment != nil && comment.IsForcePush results = append(results, private.HookProcReceiveRefResult{ OldOID: oldCommitID, diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index cffefe08ae..8c28717e5d 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/attachment/attachment.go b/services/attachment/attachment.go index d69253dd59..a824cdb246 100644 --- a/services/attachment/attachment.go +++ b/services/attachment/attachment.go @@ -54,12 +54,17 @@ func NewLimitedUploaderMaxBytesReader(r io.ReadCloser, w http.ResponseWriter) *U return &UploaderFile{rd: r, size: -1, respWriter: w} } -func UploadAttachmentGeneralSizeLimit(ctx context.Context, file *UploaderFile, allowedTypes string, attach *repo_model.Attachment) (*repo_model.Attachment, error) { - return uploadAttachment(ctx, file, allowedTypes, setting.Attachment.MaxSize<<20, attach) +type UploadAttachmentFunc func(ctx context.Context, file *UploaderFile, attach *repo_model.Attachment) (*repo_model.Attachment, error) + +func UploadAttachmentForIssue(ctx context.Context, file *UploaderFile, attach *repo_model.Attachment) (*repo_model.Attachment, error) { + return uploadAttachment(ctx, file, setting.Attachment.AllowedTypes, setting.Attachment.MaxSize<<20, attach) } -func UploadAttachmentReleaseSizeLimit(ctx context.Context, file *UploaderFile, allowedTypes string, attach *repo_model.Attachment) (*repo_model.Attachment, error) { - return uploadAttachment(ctx, file, allowedTypes, setting.Repository.Release.FileMaxSize<<20, attach) +func UploadAttachmentForRelease(ctx context.Context, file *UploaderFile, attach *repo_model.Attachment) (*repo_model.Attachment, error) { + // FIXME: although the release attachment has different settings from the issue attachment, + // it still uses the same attachment table, the same storage and the same upload logic + // So if the "issue attachment [attachment]" is not enabled, it will also affect the release attachment, which is not expected. + return uploadAttachment(ctx, file, setting.Repository.Release.AllowedTypes, setting.Repository.Release.FileMaxSize<<20, attach) } func uploadAttachment(ctx context.Context, file *UploaderFile, allowedTypes string, maxFileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) { diff --git a/services/auth/auth_token.go b/services/auth/auth_token.go index 8897bbd19c..7fcf3ba0df 100644 --- a/services/auth/auth_token.go +++ b/services/auth/auth_token.go @@ -64,10 +64,7 @@ func CheckAuthToken(ctx context.Context, value string) (*auth_model.AuthToken, e } func RegenerateAuthToken(ctx context.Context, t *auth_model.AuthToken) (*auth_model.AuthToken, string, error) { - token, hash, err := generateTokenAndHash() - if err != nil { - return nil, "", err - } + token, hash := generateTokenAndHash() newToken := &auth_model.AuthToken{ ID: t.ID, @@ -89,16 +86,9 @@ func CreateAuthTokenForUserID(ctx context.Context, userID int64) (*auth_model.Au ExpiresUnix: timeutil.TimeStampNow().AddDuration(time.Duration(setting.LogInRememberDays*24) * time.Hour), } - var err error - t.ID, err = util.CryptoRandomString(10) - if err != nil { - return nil, "", err - } + t.ID = util.CryptoRandomString(10) - token, hash, err := generateTokenAndHash() - if err != nil { - return nil, "", err - } + token, hash := generateTokenAndHash() t.TokenHash = hash @@ -109,15 +99,12 @@ func CreateAuthTokenForUserID(ctx context.Context, userID int64) (*auth_model.Au return t, token, nil } -func generateTokenAndHash() (string, string, error) { - buf, err := util.CryptoRandomBytes(32) - if err != nil { - return "", "", err - } +func generateTokenAndHash() (string, string) { + buf := util.CryptoRandomBytes(32) token := hex.EncodeToString(buf) hashedToken := sha256.Sum256([]byte(token)) - return token, hex.EncodeToString(hashedToken[:]), nil + return token, hex.EncodeToString(hashedToken[:]) } diff --git a/services/auth/basic.go b/services/auth/basic.go index dda6451c36..e8a4a2e8f7 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -69,7 +69,7 @@ func (b *Basic) parseAuthBasic(req *http.Request) (ret struct{ authToken, uname, // VerifyAuthToken only the access token provided as parameter, used by other auth methods that want to reuse access token verification logic func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore, authToken string) (*user_model.User, error) { // get oauth2 token's user's ID - _, uid := GetOAuthAccessTokenScopeAndUserID(req.Context(), authToken) + accessTokenScope, uid := GetOAuthAccessTokenScopeAndUserID(req.Context(), authToken) if uid != 0 { log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid) @@ -81,6 +81,7 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store store.GetData()["LoginMethod"] = OAuth2TokenMethodName store.GetData()["IsApiToken"] = true + store.GetData()["ApiTokenScope"] = accessTokenScope return u, nil } diff --git a/services/auth/signin.go b/services/auth/signin.go index e116a088e0..a8fa0f17dd 100644 --- a/services/auth/signin.go +++ b/services/auth/signin.go @@ -51,7 +51,7 @@ func UserSignIn(ctx context.Context, username, password string) (*user_model.Use } if user != nil { - hasUser, err := user_model.GetUser(ctx, user) + hasUser, err := user_model.GetIndividualUser(ctx, user) if err != nil { return nil, nil, err } diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go index f6c032492f..a0a03022ec 100644 --- a/services/auth/source/ldap/source_search.go +++ b/services/auth/source/ldap/source_search.go @@ -116,11 +116,12 @@ func dial(source *Source) (*ldap.Conn, error) { InsecureSkipVerify: source.SkipVerify, } + hostPort := net.JoinHostPort(source.Host, strconv.Itoa(source.Port)) if source.SecurityProtocol == SecurityProtocolLDAPS { - return ldap.DialTLS("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port)), tlsConfig) //nolint:staticcheck // DialTLS is deprecated + return ldap.DialURL("ldaps://"+hostPort, ldap.DialWithTLSConfig(tlsConfig)) } - conn, err := ldap.Dial("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port))) //nolint:staticcheck // Dial is deprecated + conn, err := ldap.DialURL("ldap://" + hostPort) if err != nil { return nil, fmt.Errorf("error during Dial: %w", err) } diff --git a/services/auth/source/oauth2/init.go b/services/auth/source/oauth2/init.go index 2a165bac85..5ab0d5394d 100644 --- a/services/auth/source/oauth2/init.go +++ b/services/auth/source/oauth2/init.go @@ -72,7 +72,7 @@ func initOAuth2Sources(ctx context.Context) error { } err := oauth2Source.RegisterSource() if err != nil { - log.Critical("Unable to register source: %s due to Error: %v.", source.Name, err) + log.Error("Unable to register source: %s due to Error: %v.", source.Name, err) } } return nil diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go index 68bd4a1d4c..f719d23fc3 100644 --- a/services/auth/source/oauth2/providers.go +++ b/services/auth/source/oauth2/providers.go @@ -62,7 +62,7 @@ func (p *AuthSourceProvider) DisplayName() string { func (p *AuthSourceProvider) IconHTML(size int) template.HTML { if p.iconURL != "" { - img := fmt.Sprintf(`%s`, + img := fmt.Sprintf(`%s`, size, size, html.EscapeString(p.iconURL), html.EscapeString(p.DisplayName()), diff --git a/services/auth/source/oauth2/providers_base.go b/services/auth/source/oauth2/providers_base.go index d34597d6d9..bc761e9742 100644 --- a/services/auth/source/oauth2/providers_base.go +++ b/services/auth/source/oauth2/providers_base.go @@ -42,10 +42,10 @@ func (b *BaseProvider) IconHTML(size int) template.HTML { case "github": svgName = "octicon-mark-github" } - svgHTML := svg.RenderHTML(svgName, size, "tw-mr-2") + svgHTML := svg.RenderHTML(svgName, size) if svgHTML == "" { log.Error("No SVG icon for oauth2 provider %q", b.name) - svgHTML = svg.RenderHTML("gitea-openid", size, "tw-mr-2") + svgHTML = svg.RenderHTML("gitea-openid", size) } return svgHTML } diff --git a/services/auth/source/oauth2/providers_custom.go b/services/auth/source/oauth2/providers_custom.go index 65cf538ad7..6a4098dda6 100644 --- a/services/auth/source/oauth2/providers_custom.go +++ b/services/auth/source/oauth2/providers_custom.go @@ -120,4 +120,25 @@ func init() { }), nil }, )) + + RegisterGothProvider(&AwsCognitoProvider{}) } + +const ProviderNameAwsCognito = "aws-cognito" + +// AwsCognitoProvider is a GothProvider for AWS Cognito (based on OpenID Connect) +type AwsCognitoProvider struct { + OpenIDProvider +} + +// Name provides the technical name for this provider +func (c *AwsCognitoProvider) Name() string { + return ProviderNameAwsCognito +} + +// DisplayName returns the friendly name for this provider +func (c *AwsCognitoProvider) DisplayName() string { + return "AWS Cognito" +} + +var _ GothProvider = &AwsCognitoProvider{} diff --git a/services/auth/source/oauth2/providers_openid.go b/services/auth/source/oauth2/providers_openid.go index e86dc48232..557fe6cb01 100644 --- a/services/auth/source/oauth2/providers_openid.go +++ b/services/auth/source/oauth2/providers_openid.go @@ -33,7 +33,7 @@ func (o *OpenIDProvider) DisplayName() string { // IconHTML returns icon HTML for this provider func (o *OpenIDProvider) IconHTML(size int) template.HTML { - return svg.RenderHTML("gitea-openid", size, "tw-mr-2") + return svg.RenderHTML("gitea-openid", size) } // CreateGothProvider creates a GothProvider from this Provider @@ -46,8 +46,14 @@ func (o *OpenIDProvider) CreateGothProvider(providerName, callbackURL string, so provider, err := openidConnect.New(source.ClientID, source.ClientSecret, callbackURL, source.OpenIDConnectAutoDiscoveryURL, scopes...) if err != nil { log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, source.OpenIDConnectAutoDiscoveryURL, err) + return nil, err } - return provider, err + if source.ExternalIDClaim != "" { + // UserIdClaims is a fallback list; goth returns the first non-empty matching claim. + // A single entry is sufficient because the admin explicitly chooses one claim (e.g. "oid" for Azure AD). + provider.UserIdClaims = []string{source.ExternalIDClaim} + } + return provider, nil } // CustomURLSettings returns the custom url settings for this provider diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go index 00d89b3481..3f69c08fab 100644 --- a/services/auth/source/oauth2/source.go +++ b/services/auth/source/oauth2/source.go @@ -30,6 +30,7 @@ type Source struct { SSHPublicKeyClaimName string FullNameClaimName string + ExternalIDClaim string } // FromDB fills up an OAuth2Config from serialized format. diff --git a/services/auth/source/oauth2/source_sync.go b/services/auth/source/oauth2/source_sync.go index c2e3dfb1a8..445e281a06 100644 --- a/services/auth/source/oauth2/source_sync.go +++ b/services/auth/source/oauth2/source_sync.go @@ -67,7 +67,7 @@ func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *us LoginSource: u.LoginSourceID, } - hasUser, err := user_model.GetUser(ctx, user) + hasUser, err := user_model.GetIndividualUser(ctx, user) if err != nil { return err } diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index 10a31f4d7b..1629b2b95e 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 @@ -245,13 +245,13 @@ func handlePullRequestAutoMerge(pullID int64, sha string) { return } - perm, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, doer) + perm, err := access_model.GetDoerRepoPermission(ctx, pr.BaseRepo, doer) if err != nil { - log.Error("GetUserRepoPermission %-v: %v", pr.BaseRepo, err) + log.Error("GetDoerRepoPermission %-v: %v", pr.BaseRepo, err) 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 e8cc4512a7..8cfdf3a539 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 b49bf9b42c..3f9f3e1cdd 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.go b/services/context/base.go index 4baea95ccf..c5ec4b419a 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -159,26 +159,24 @@ func (b *Base) Redirect(location string, status ...int) { // So in this case, we should remove the session cookie from the response header removeSessionCookieHeader(b.Resp) } - // in case the request is made by htmx, have it redirect the browser instead of trying to follow the redirect inside htmx - if b.Req.Header.Get("HX-Request") == "true" { - b.Resp.Header().Set("HX-Redirect", location) - // we have to return a non-redirect status code so XMLHTTPRequest will not immediately follow the redirect - // so as to give htmx redirect logic a chance to run - b.Status(http.StatusNoContent) + // In case the request is made by "fetch-action" module, make JS redirect to the new location + // Otherwise, the JS fetch will follow the redirection and read a "login" page, embed it to the current page, which is not expected. + if b.Req.Header.Get("X-Gitea-Fetch-Action") != "" { + b.JSON(http.StatusOK, map[string]any{"redirect": location}) return } http.Redirect(b.Resp, b.Req, location, code) } -type ServeHeaderOptions httplib.ServeHeaderOptions +type ServeHeaderOptions = httplib.ServeHeaderOptions -func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) { - httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opt)) +func (b *Base) SetServeHeaders(opts ServeHeaderOptions) { + httplib.ServeSetHeaders(b.Resp, opts) } // ServeContent serves content to http request -func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { - httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opts)) +func (b *Base) ServeContent(r io.ReadSeeker, opts ServeHeaderOptions) { + httplib.ServeSetHeaders(b.Resp, opts) http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r) } diff --git a/services/context/base_form.go b/services/context/base_form.go index 81fd7cd328..b734ab199a 100644 --- a/services/context/base_form.go +++ b/services/context/base_form.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/util" ) @@ -35,6 +36,11 @@ func (b *Base) FormStrings(key string) []string { return nil } +func (b *Base) FormStringInt64s(key string) []int64 { + vals, _ := base.StringsToInt64s(strings.Split(b.FormString(key), ",")) + return vals +} + // FormTrim returns the first value for the provided key in the form as a space trimmed string func (b *Base) FormTrim(key string) string { return strings.TrimSpace(b.Req.FormValue(key)) diff --git a/services/context/base_path.go b/services/context/base_path.go index 63e60c8654..8c353f7aca 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/base_test.go b/services/context/base_test.go index 2a4f86dddf..f9bbe71729 100644 --- a/services/context/base_test.go +++ b/services/context/base_test.go @@ -38,9 +38,10 @@ func TestRedirect(t *testing.T) { req, _ = http.NewRequest(http.MethodGet, "/", nil) resp := httptest.NewRecorder() - req.Header.Add("HX-Request", "true") + req.Header.Add("X-Gitea-Fetch-Action", "1") b := NewBaseContextForTest(resp, req) b.Redirect("/other") - assert.Equal(t, "/other", resp.Header().Get("HX-Redirect")) - assert.Equal(t, http.StatusNoContent, resp.Code) + assert.Contains(t, resp.Header().Get("Content-Type"), "application/json") + assert.JSONEq(t, `{"redirect":"/other"}`, resp.Body.String()) + assert.Equal(t, http.StatusOK, resp.Code) } diff --git a/services/context/captcha.go b/services/context/captcha.go index b1129a05b2..50beddb208 100644 --- a/services/context/captcha.go +++ b/services/context/captcha.go @@ -48,7 +48,7 @@ func GetImageCaptcha() *captcha.Captcha { const ( gRecaptchaResponseField = "g-recaptcha-response" hCaptchaResponseField = "h-captcha-response" - mCaptchaResponseField = "m-captcha-response" + mCaptchaResponseField = "mcaptcha__token" // this form key is hard-coded in the mcaptcha frontend library cfTurnstileResponseField = "cf-turnstile-response" ) diff --git a/services/context/context.go b/services/context/context.go index a6a861ecaa..b4e9904cd4 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -63,8 +63,6 @@ type Context struct { Package *Package } -type TemplateContext map[string]any - func init() { web.RegisterResponseStatusProvider[*Base](func(req *http.Request) web_types.ResponseStatusProvider { return req.Context().Value(BaseContextKey).(*Base) @@ -106,6 +104,7 @@ func NewTemplateContextForWeb(ctx reqctx.RequestContext, req *http.Request, loca tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx) tmplCtx["RenderUtils"] = templates.NewRenderUtils(ctx) tmplCtx["MiscUtils"] = templates.NewMiscUtils(ctx) + tmplCtx["ActionsUtils"] = templates.NewActionsUtils(ctx) tmplCtx["RootData"] = ctx.GetData() tmplCtx["Consts"] = map[string]any{ "RepoUnitTypeCode": unit.TypeCode, @@ -165,6 +164,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 @@ -198,10 +198,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() @@ -211,7 +207,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 d057f8d41e..1fd7a0b447 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 52c7461187..0f083d097e 100644 --- a/services/context/context_template.go +++ b/services/context/context_template.go @@ -5,15 +5,23 @@ package context import ( "context" + "html" + "html/template" "net/http" "strconv" + "strings" "time" + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/webtheme" ) +type TemplateContext map[string]any + var _ context.Context = TemplateContext(nil) func NewTemplateContext(ctx context.Context, req *http.Request) TemplateContext { @@ -69,3 +77,65 @@ func (c TemplateContext) CurrentWebBanner() *setting.WebBannerType { } return nil } + +// AppFullLink returns a full URL link with AppSubURL for the given app link (no AppSubURL) +// If no link is given, it returns the current app full URL with sub-path but without trailing slash (that's why it is not named as AppURL) +func (c TemplateContext) AppFullLink(link ...string) template.URL { + s := httplib.GuessCurrentAppURL(c.parentContext()) + s = strings.TrimSuffix(s, "/") + if len(link) == 0 { + return template.URL(s) + } + return template.URL(s + "/" + strings.TrimPrefix(link[0], "/")) +} + +func (c TemplateContext) ScriptImport(path string, typ ...string) template.HTML { + if len(typ) > 0 { + if typ[0] == "module" { + return template.HTML(``) + } + panic("unsupported script type: " + typ[0]) + } + return template.HTML(``) +} + +func (c TemplateContext) CspScriptNonce() (ret string) { + // Generate a random nonce for each request and cache it in the context to make it usable during the whole rendering process. + // + // Some " diff --git a/templates/base/footer_content.tmpl b/templates/base/footer_content.tmpl index 66c9d718ea..3b0af6ddc3 100644 --- a/templates/base/footer_content.tmpl +++ b/templates/base/footer_content.tmpl @@ -4,17 +4,22 @@ {{ctx.Locale.Tr "powered_by" "Gitea"}} {{end}} {{if (or .ShowFooterVersion .PageIsAdmin)}} + {{ctx.Locale.Tr "version"}}: {{if .IsAdmin}} {{AppVer}} {{else}} {{AppVer}} {{end}} + {{end}} {{if and .TemplateLoadTimes ShowFooterTemplateLoadTime}} - {{ctx.Locale.Tr "page"}}: {{LoadTimes .PageStartTime}} - {{ctx.Locale.Tr "template"}}{{if .TemplateName}} {{.TemplateName}}{{end}}: {{call .TemplateLoadTimes}} + + {{ctx.Locale.Tr "page"}}: {{LoadTimes .PageStartTime}} + {{ctx.Locale.Tr "template"}}{{if .TemplateName}} {{.TemplateName}}{{end}}: {{call .TemplateLoadTimes}} + {{end}} + {{if $.ViteModeIsDev}}ViteDevMode{{end}}