diff --git a/.changelog.yml b/.changelog.yml index a7df8779de1..748676569a3 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 4f82a5d8c6a..4863391f394 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Gitea DevContainer", - "image": "mcr.microsoft.com/devcontainers/go:1.25-trixie", + "image": "mcr.microsoft.com/devcontainers/go:1.26-trixie", "containerEnv": { // override "local" from packaged version "GOTOOLCHAIN": "auto" @@ -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 c88fb144fe5..f61d8aea3f3 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 bf1cf757cc6..703a8348184 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 00000000000..d280ea26ce7 --- /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 00000000000..04d4bac3673 --- /dev/null +++ b/.github/actions/go-cache/action.yml @@ -0,0 +1,47 @@ +name: go-caches +description: Restore and save go module, build, and golangci-lint caches + +inputs: + cache-name: + description: Short identifier used in the per-caller build cache key + required: true + build-cache: + description: Whether to include ~/.cache/go-build + default: "true" + build-cache-rotate: + description: Whether to rotate the build cache key per run so Go's test result cache can accumulate across runs + default: "false" + lint-cache: + description: Whether to include ~/.cache/golangci-lint + default: "false" + +runs: + using: composite + steps: + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/go/pkg/mod + key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} + restore-keys: gomod-${{ runner.os }}-${{ runner.arch }} + - if: ${{ inputs.build-cache == 'true' && inputs.build-cache-rotate == 'true' }} + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cache/go-build + key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}-${{ github.run_id }} + restore-keys: | + gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }} + gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }} + gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} + gobuild-${{ runner.os }}-${{ runner.arch }} + - if: ${{ inputs.build-cache == 'true' && inputs.build-cache-rotate != 'true' }} + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cache/go-build + key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} + restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }} + - if: ${{ inputs.lint-cache == 'true' }} + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cache/golangci-lint + key: golangci-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum', '.golangci.yml') }} + restore-keys: golangci-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index be33b8975fc..00000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: 2 - -updates: - - package-ecosystem: github-actions - labels: [modifies/dependencies] - directory: / - schedule: - interval: daily - cooldown: - default-days: 5 diff --git a/.github/labeler.yml b/.github/labeler.yml index 68a0f30fd64..937e69ef208 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 b7594a1ba7f..91d001e0789 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 00000000000..b0f3ca97de0 --- /dev/null +++ b/.github/workflows/cache-seeder.yml @@ -0,0 +1,73 @@ +# 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" } + - { job: lint-go-windows, tags: "bindata", target: "lint-go-windows" } + - { job: lint-go-gogit, tags: "bindata gogit", target: "lint-go" } + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + check-latest: true + cache: false + - uses: ./.github/actions/go-cache + with: + cache-name: ${{ matrix.job }} + lint-cache: "true" + - run: make deps-backend deps-tools + - run: make ${{ matrix.target }} + env: + TAGS: ${{ matrix.tags }} diff --git a/.github/workflows/cron-flake-updater.yml b/.github/workflows/cron-flake-updater.yml deleted file mode 100644 index 105802e5587..00000000000 --- a/.github/workflows/cron-flake-updater.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: cron-flake-updater - -on: - workflow_dispatch: - schedule: - - cron: '0 0 * * 0' # runs weekly on Sunday at 00:00 - -jobs: - nix-flake-update: - permissions: - contents: write - issues: write - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: DeterminateSystems/determinate-nix-action@v3 - - uses: DeterminateSystems/update-flake-lock@main - with: - pr-title: "Update Nix flake" - pr-labels: | - dependencies diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index ee1c3e0c750..edb6f2e1576 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -12,15 +12,15 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod check-latest: true - run: make generate-gitignore timeout-minutes: 40 - name: push translations to repo - uses: appleboy/git-push-action@v1.2.0 + uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0 with: author_email: "teabot@gitea.io" author_name: GiteaBot diff --git a/.github/workflows/cron-renovate.yml b/.github/workflows/cron-renovate.yml new file mode 100644 index 00000000000..9ebe1fec325 --- /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@f66d8679fcfcfa051abde6e7a623007173bf5164 # v46.1.12 + 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 56a30fb5ba6..17f29d4e0c5 100644 --- a/.github/workflows/cron-translations.yml +++ b/.github/workflows/cron-translations.yml @@ -12,8 +12,8 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v6 - - uses: crowdin/github-action@v2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 with: upload_sources: true upload_translations: false @@ -29,7 +29,7 @@ jobs: - name: update locales run: ./build/update-locales.sh - name: push translations to repo - uses: appleboy/git-push-action@v1.2.0 + uses: appleboy/git-push-action@3b2c8661652360dbf1afe1b319a49dbb739c39f1 # v1.2.0 with: author_email: "teabot@gitea.io" author_name: GiteaBot diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index 332e9e0d6ff..50aa2045e2b 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@v3 + - 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,8 @@ jobs: json: - "**/*.json" + + 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 fb81622bd69..6a3ec2d73e5 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -7,124 +7,82 @@ 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: make lint-backend env: - TAGS: bindata sqlite sqlite_unlock_notify + TAGS: bindata - 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@v4 - - 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@v4 - - 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@v4 - - 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 + - if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + - if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' + run: uv python install 3.14 && make deps-py lint-templates lint-yaml + + - 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 + 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 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod check-latest: true + cache: false + - uses: ./.github/actions/go-cache + with: + cache-name: lint-go-windows + lint-cache: "true" - run: make deps-backend deps-tools - - run: make lint-go-windows lint-go-gitea-vet + - run: make lint-go-windows env: - TAGS: bindata sqlite sqlite_unlock_notify + TAGS: bindata GOOS: windows GOARCH: amd64 @@ -132,31 +90,37 @@ 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 + cache: false + - uses: ./.github/actions/go-cache + with: + cache-name: lint-go-gogit + lint-cache: "true" - run: make deps-backend deps-tools - run: make lint-go env: - TAGS: bindata gogit sqlite sqlite_unlock_notify + TAGS: bindata gogit 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 +128,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@v4 - - 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 +146,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 +172,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@v4 - - 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 d168c2ecc5f..b750a9c2645 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 @@ -35,18 +34,22 @@ jobs: 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:2023.12.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.14 env: discovery.type: single-node + xpack.security.enabled: false ports: - "9200:9200" meilisearch: @@ -119,7 +127,7 @@ jobs: ports: - 6379:6379 minio: - image: bitnamilegacy/minio:2021.3.17 + image: bitnamilegacy/minio:2021.12.29 env: MINIO_ACCESS_KEY: 123456 MINIO_SECRET_KEY: 12345678 @@ -130,11 +138,16 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod check-latest: true + cache: false + - uses: ./.github/actions/go-cache + with: + cache-name: unit + build-cache-rotate: "true" - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts' - run: make deps-backend @@ -142,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,9 +184,10 @@ jobs: options: >- --mount type=tmpfs,destination=/bitnami/mysql/data elasticsearch: - image: elasticsearch:7.5.0 + image: docker.elastic.co/elasticsearch/elasticsearch:8.19.14 env: discovery.type: single-node + xpack.security.enabled: false ports: - "9200:9200" smtpimap: @@ -185,11 +198,15 @@ jobs: - "587:587" - "993:993" steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod check-latest: true + cache: false + - uses: ./.github/actions/go-cache + with: + cache-name: mysql - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts' - run: make deps-backend @@ -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 @@ -226,20 +239,24 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod check-latest: true + cache: false + - uses: ./.github/actions/go-cache + with: + cache-name: mssql - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts' - run: make deps-backend - 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 bcc19e3eba8..43a4f48669d 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@v3 - - uses: docker/setup-buildx-action@v3 - - name: Build regular container image - uses: docker/build-push-action@v6 + - 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@v6 + 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 c77f7af3f08..f81026a5ae0 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@v4 - - 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 d05483e56ca..f9e2e5e07b7 100644 --- a/.github/workflows/pull-labeler.yml +++ b/.github/workflows/pull-labeler.yml @@ -15,6 +15,6 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/labeler@v6 + - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: sync-labels: true diff --git a/.github/workflows/pull-pr-title.yml b/.github/workflows/pull-pr-title.yml new file mode 100644 index 00000000000..59b0e78c409 --- /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 a7b2fda0426..e1fd87b7599 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@v4 - - 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,10 +32,10 @@ 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@v6 + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -52,7 +52,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -67,18 +67,18 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Get cleaned branch name id: clean_name run: | REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - - uses: docker/metadata-action@v5 + - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 id: meta with: images: |- @@ -88,7 +88,7 @@ jobs: type=raw,value=${{ steps.clean_name.outputs.branch }} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@v5 + - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 id: meta_rootless with: images: |- @@ -102,18 +102,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v3 + 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@v3 + 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@v6 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -123,7 +123,7 @@ jobs: cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max - name: build rootless docker image - uses: docker/build-push-action@v6 + 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 fab468c9b4b..b7d5cbcd229 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@v4 - - 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,10 +33,10 @@ 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@v6 + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -53,7 +53,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -62,7 +62,7 @@ jobs: run: | aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress - name: Install GH CLI - uses: dev-hanz-ops/install-gh-cli-action@v0.2.1 + uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1 with: gh-cli-version: 2.39.1 - name: create github release @@ -77,13 +77,13 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - uses: docker/metadata-action@v5 + - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 id: meta with: images: |- @@ -96,7 +96,7 @@ jobs: type=semver,pattern={{version}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@v5 + - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 id: meta_rootless with: images: |- @@ -112,18 +112,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v3 + 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@v3 + 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@v6 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -131,7 +131,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@v6 + 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 113a33c3c7f..82a1930f0ab 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@v4 - - 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,10 +36,10 @@ 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@v6 + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -56,7 +56,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -65,7 +65,7 @@ jobs: run: | aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress - name: Install GH CLI - uses: dev-hanz-ops/install-gh-cli-action@v0.2.1 + uses: dev-hanz-ops/install-gh-cli-action@af38ce09b1ec248aeb08eea2b16bbecea9e059f8 # v0.2.1 with: gh-cli-version: 2.39.1 - name: create github release @@ -80,13 +80,13 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - uses: docker/metadata-action@v5 + - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 id: meta with: images: |- @@ -103,7 +103,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@v5 + - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 id: meta_rootless with: images: |- @@ -124,18 +124,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v3 + 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@v3 + 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@v6 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -143,7 +143,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@v6 + 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 45e8e9295fd..76a75786461 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 8671edc47cc..00000000000 --- 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 afd91d65e59..570942bdd32 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -13,6 +13,7 @@ linters: - forbidigo - gocheckcompilerdirectives - gocritic + - goheader - govet - ineffassign - mirror @@ -51,6 +52,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 @@ -109,6 +118,11 @@ linters: enable: - nilness - unusedwrite + goheader: + values: + regexp: + HEADER: '((Copyright [^\n]+|All rights reserved\.)\n)*Copyright \d{4} (The (Gogs|Gitea) Authors|Gitea Authors|Gitea)\.( All rights reserved\.)?(\n(Copyright [^\n]+|All rights reserved\.))*\nSPDX-License-Identifier: [\w.-]+' + template: '{{ HEADER }}' exclusions: generated: lax presets: @@ -158,9 +172,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 +191,6 @@ formatters: - .venv - public - web_src - - third_party$ - - builtin$ - - examples$ run: timeout: 10m diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 790a49a6eb9..00000000000 --- 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 402a9d6945e..16770e49fcf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,16 @@ -# Instructions for agents - - Use `make help` to find available development targets -- Before committing `.go` changes, run `make fmt` to format, and run `make lint-go` to lint -- Before committing `.ts` changes, run `make lint-js` to lint -- Before committing `go.mod` changes, run `make tidy` -- Before committing new `.go` files, add the current year into the copyright header -- Before committing any files, remove all trailing whitespace from source code lines -- Never force-push to pull request branches -- Always start issue and pull request comments with an authorship attribution +- 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 +- 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 +- 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 b662cb4ad5b..f807f16b3f4 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 33b329182c7..27103a991b5 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 9922cee9c41..c69b5a65d1b 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 a1742e3d51f..6f029067ded 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/MAINTAINERS b/MAINTAINERS index 1c7afc6f6c7..26192eff447 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -64,3 +64,4 @@ metiftikci (@metiftikci) Christopher Homberger (@ChristopherHX) Tobias Balle-Petersen (@tobiasbp) TheFox (@TheFox0x7) +Nicolas (@bircni) \ No newline at end of file diff --git a/Makefile b/Makefile index 4d1bd96ea51..1087dc8410b 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 @@ -344,11 +345,6 @@ 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)" ./... - .PHONY: lint-editorconfig lint-editorconfig: @echo "Running editorconfig check..." @@ -369,36 +365,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 +404,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 +417,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 +445,48 @@ 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) + @pnpm exec playwright install $(if $(GITHUB_ACTIONS),,--with-deps) chromium firefox $(PLAYWRIGHT_FLAGS) .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 $(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 +513,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 +577,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 +587,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 +595,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 +651,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"; \ @@ -828,11 +676,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 ed000971a75..4c4fedd3b8e 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 8d9531e8e4e..52c77818311 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 875d31e28a8..77ad6828db2 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/go-licenses.json b/assets/go-licenses.json index 30f56e5f872..f6fcd2fbdfa 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/v84", + "path": "github.com/google/go-github/v84/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", @@ -1184,6 +1244,11 @@ "path": "github.com/zeebo/blake3/LICENSE", "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": "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", "path": "gitlab.com/gitlab-org/api/client-go/LICENSE", @@ -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-go-licenses.go b/build/generate-go-licenses.go index b710fdb841f..057e6a6e49a 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 00000000000..7b37a5bbba9 --- /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 00000000000..04587b33039 --- /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 00000000000..a9a715e6c2a --- /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 00000000000..dd116205493 --- /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 00000000000..2e5fe99db0f --- /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/cmd/actions.go b/cmd/actions.go index 2c51c6a1bcc..44b6b7b54cf 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -13,17 +13,18 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdActions represents the available actions sub-commands. - CmdActions = &cli.Command{ +func newActionsCommand() *cli.Command { + return &cli.Command{ Name: "actions", Usage: "Manage Gitea Actions", Commands: []*cli.Command{ - subcmdActionsGenRunnerToken, + newActionsGenerateRunnerTokenCommand(), }, } +} - subcmdActionsGenRunnerToken = &cli.Command{ +func newActionsGenerateRunnerTokenCommand() *cli.Command { + return &cli.Command{ Name: "generate-runner-token", Usage: "Generate a new token for a runner to use to register with the server", Action: runGenerateActionsRunnerToken, @@ -37,7 +38,7 @@ var ( }, }, } -) +} func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error { setting.MustInstalled() diff --git a/cmd/admin.go b/cmd/admin.go index dbd48e57279..c0e21731cbe 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -18,36 +18,41 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdAdmin represents the available admin sub-command. - CmdAdmin = &cli.Command{ +func newAdminCommand() *cli.Command { + return &cli.Command{ Name: "admin", Usage: "Perform common administrative operations", Commands: []*cli.Command{ - subcmdUser, - subcmdRepoSyncReleases, - subcmdRegenerate, - subcmdAuth, - subcmdSendMail, + newUserCommand(), + newRepoSyncReleasesCommand(), + newRegenerateCommand(), + newAuthCommand(), + newSendMailCommand(), }, } +} - subcmdRepoSyncReleases = &cli.Command{ +func newRepoSyncReleasesCommand() *cli.Command { + return &cli.Command{ Name: "repo-sync-releases", Usage: "Synchronize repository releases with tags", Action: runRepoSyncReleases, } +} - subcmdRegenerate = &cli.Command{ +func newRegenerateCommand() *cli.Command { + return &cli.Command{ Name: "regenerate", Usage: "Regenerate specific files", Commands: []*cli.Command{ - microcmdRegenHooks, - microcmdRegenKeys, + newRegenerateHooksCommand(), + newRegenerateKeysCommand(), }, } +} - subcmdAuth = &cli.Command{ +func newAuthCommand() *cli.Command { + return &cli.Command{ Name: "auth", Usage: "Modify external auth providers", Commands: []*cli.Command{ @@ -59,12 +64,14 @@ var ( microcmdAuthUpdateLdapSimpleAuth(), microcmdAuthAddSMTP(), microcmdAuthUpdateSMTP(), - microcmdAuthList, - microcmdAuthDelete, + newAuthListCommand(), + newAuthDeleteCommand(), }, } +} - subcmdSendMail = &cli.Command{ +func newSendMailCommand() *cli.Command { + return &cli.Command{ Name: "sendmail", Usage: "Send a message to all users", Action: runSendMail, @@ -86,7 +93,7 @@ var ( }, }, } -) +} func idFlag() *cli.Int64Flag { return &cli.Int64Flag{ diff --git a/cmd/admin_auth.go b/cmd/admin_auth.go index 1a093667229..b55bb1481cf 100644 --- a/cmd/admin_auth.go +++ b/cmd/admin_auth.go @@ -17,14 +17,17 @@ import ( "github.com/urfave/cli/v3" ) -var ( - microcmdAuthDelete = &cli.Command{ +func newAuthDeleteCommand() *cli.Command { + return &cli.Command{ Name: "delete", Usage: "Delete specific auth source", Flags: []cli.Flag{idFlag()}, Action: runDeleteAuth, } - microcmdAuthList = &cli.Command{ +} + +func newAuthListCommand() *cli.Command { + return &cli.Command{ Name: "list", Usage: "List auth sources", Action: runListAuth, @@ -55,7 +58,7 @@ var ( }, }, } -) +} func runListAuth(ctx context.Context, c *cli.Command) error { if err := initDB(ctx); err != nil { diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go index a5f1bd51059..aa235441bac 100644 --- a/cmd/admin_regenerate.go +++ b/cmd/admin_regenerate.go @@ -13,19 +13,21 @@ import ( "github.com/urfave/cli/v3" ) -var ( - microcmdRegenHooks = &cli.Command{ +func newRegenerateHooksCommand() *cli.Command { + return &cli.Command{ Name: "hooks", Usage: "Regenerate git-hooks", Action: runRegenerateHooks, } +} - microcmdRegenKeys = &cli.Command{ +func newRegenerateKeysCommand() *cli.Command { + return &cli.Command{ Name: "keys", Usage: "Regenerate authorized_keys file", Action: runRegenerateKeys, } -) +} func runRegenerateHooks(ctx context.Context, _ *cli.Command) error { if err := initDB(ctx); err != nil { diff --git a/cmd/admin_user.go b/cmd/admin_user.go index 3a24c3e56f1..8dd8bb4eca1 100644 --- a/cmd/admin_user.go +++ b/cmd/admin_user.go @@ -7,15 +7,17 @@ import ( "github.com/urfave/cli/v3" ) -var subcmdUser = &cli.Command{ - Name: "user", - Usage: "Modify users", - Commands: []*cli.Command{ - microcmdUserCreate(), - microcmdUserList, - microcmdUserChangePassword(), - microcmdUserDelete(), - microcmdUserGenerateAccessToken, - microcmdUserMustChangePassword(), - }, +func newUserCommand() *cli.Command { + return &cli.Command{ + Name: "user", + Usage: "Modify users", + Commands: []*cli.Command{ + microcmdUserCreate(), + newUserListCommand(), + microcmdUserChangePassword(), + microcmdUserDelete(), + newUserGenerateAccessTokenCommand(), + microcmdUserMustChangePassword(), + }, + } } diff --git a/cmd/admin_user_change_password_test.go b/cmd/admin_user_change_password_test.go index 902632f3e49..cf497517f74 100644 --- a/cmd/admin_user_change_password_test.go +++ b/cmd/admin_user_change_password_test.go @@ -4,6 +4,7 @@ package cmd import ( + "io" "testing" "code.gitea.io/gitea/models/db" @@ -82,7 +83,9 @@ func TestChangePasswordCommand(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - err := microcmdUserChangePassword().Run(ctx, tc.args) + cmd := microcmdUserChangePassword() + cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard + err := cmd.Run(ctx, tc.args) require.Error(t, err) require.Contains(t, err.Error(), tc.expectedErr) }) diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go index 61064fdef4b..7f8330ff7e4 100644 --- a/cmd/admin_user_generate_access_token.go +++ b/cmd/admin_user_generate_access_token.go @@ -14,32 +14,34 @@ import ( "github.com/urfave/cli/v3" ) -var microcmdUserGenerateAccessToken = &cli.Command{ - Name: "generate-access-token", - Usage: "Generate an access token for a specific user", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Usage: "Username", +func newUserGenerateAccessTokenCommand() *cli.Command { + return &cli.Command{ + Name: "generate-access-token", + Usage: "Generate an access token for a specific user", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "Username", + }, + &cli.StringFlag{ + Name: "token-name", + Aliases: []string{"t"}, + Usage: "Token name", + Value: "gitea-admin", + }, + &cli.BoolFlag{ + Name: "raw", + Usage: "Display only the token value", + }, + &cli.StringFlag{ + Name: "scopes", + Value: "all", + Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, + }, }, - &cli.StringFlag{ - Name: "token-name", - Aliases: []string{"t"}, - Usage: "Token name", - Value: "gitea-admin", - }, - &cli.BoolFlag{ - Name: "raw", - Usage: "Display only the token value", - }, - &cli.StringFlag{ - Name: "scopes", - Value: "all", - Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, - }, - }, - Action: runGenerateAccessToken, + Action: runGenerateAccessToken, + } } func runGenerateAccessToken(ctx context.Context, c *cli.Command) error { diff --git a/cmd/admin_user_list.go b/cmd/admin_user_list.go index e3d345e2f24..2958fe0cc8b 100644 --- a/cmd/admin_user_list.go +++ b/cmd/admin_user_list.go @@ -14,16 +14,18 @@ import ( "github.com/urfave/cli/v3" ) -var microcmdUserList = &cli.Command{ - Name: "list", - Usage: "List users", - Action: runListUsers, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "admin", - Usage: "List only admin users", +func newUserListCommand() *cli.Command { + return &cli.Command{ + Name: "list", + Usage: "List users", + Action: runListUsers, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "admin", + Usage: "List only admin users", + }, }, - }, + } } func runListUsers(ctx context.Context, c *cli.Command) error { diff --git a/cmd/cert_test.go b/cmd/cert_test.go index 4242d8915b3..c5775e52048 100644 --- a/cmd/cert_test.go +++ b/cmd/cert_test.go @@ -4,6 +4,7 @@ package cmd import ( + "io" "path/filepath" "testing" @@ -107,6 +108,7 @@ func TestCertCommandFailures(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { app := cmdCert() + app.Writer, app.ErrWriter = io.Discard, io.Discard tempDir := t.TempDir() certFile := filepath.Join(tempDir, "cert.pem") diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go deleted file mode 100644 index a36d05c76e0..00000000000 --- a/cmd/cmd_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package cmd - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v3" -) - -func TestDefaultCommand(t *testing.T) { - test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) { - called := false - cmd := &cli.Command{ - DefaultCommand: "test", - Commands: []*cli.Command{ - { - Name: "test", - Action: func(ctx context.Context, command *cli.Command) error { - retName, retValid := isValidDefaultSubCommand(command) - assert.Equal(t, expectedRetName, retName) - assert.Equal(t, expectedRetValid, retValid) - called = true - return nil - }, - }, - }, - } - assert.NoError(t, cmd.Run(t.Context(), args)) - assert.True(t, called) - } - test(t, []string{"./gitea"}, "", true) - test(t, []string{"./gitea", "test"}, "", true) - test(t, []string{"./gitea", "other"}, "other", false) -} diff --git a/cmd/cmdtest/cmd_test.go b/cmd/cmdtest/cmd_test.go new file mode 100644 index 00000000000..4ff854f8abf --- /dev/null +++ b/cmd/cmdtest/cmd_test.go @@ -0,0 +1,237 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// Tests here reload the config system multiple times with uncontrollable details. +// So they must be in a separate package, to avoid affecting other tests + +package cmdtest + +import ( + "context" + "errors" + "fmt" + "io" + "path/filepath" + "strings" + "testing" + + "code.gitea.io/gitea/cmd" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v3" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} + +func makePathOutput(workPath, customPath, customConf string) string { + return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf) +} + +func newTestApp(testCmd cli.Command) *cli.Command { + app := cmd.NewMainApp(cmd.AppVersion{}) + testCmd.Name = util.IfZero(testCmd.Name, "test-cmd") + cmd.PrepareSubcommandWithGlobalFlags(&testCmd) + app.Commands = append(app.Commands, &testCmd) + app.DefaultCommand = testCmd.Name + return app +} + +type runResult struct { + Stdout string + Stderr string + ExitCode int +} + +func runTestApp(app *cli.Command, args ...string) (runResult, error) { + outBuf := new(strings.Builder) + errBuf := new(strings.Builder) + app.Writer = outBuf + app.ErrWriter = errBuf + exitCode := -1 + defer test.MockVariableValue(&cli.ErrWriter, app.ErrWriter)() + defer test.MockVariableValue(&cli.OsExiter, func(code int) { + if exitCode == -1 { + exitCode = code // save the exit code once and then reset the writer (to simulate the exit) + app.Writer, app.ErrWriter, cli.ErrWriter = io.Discard, io.Discard, io.Discard + } + })() + err := cmd.RunMainApp(app, args...) + return runResult{outBuf.String(), errBuf.String(), exitCode}, err +} + +func TestCliCmd(t *testing.T) { + defaultWorkPath := filepath.FromSlash("/tmp/mocked-work-path") + defaultCustomPath := filepath.Join(defaultWorkPath, "custom") + defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini") + defer setting.MockBuiltinPaths(defaultWorkPath, "", "")() + + cli.CommandHelpTemplate = "(command help template)" + cli.RootCommandHelpTemplate = "(app help template)" + cli.SubcommandHelpTemplate = "(subcommand help template)" + + cases := []struct { + env map[string]string + cmd string + exp string + }{ + // help commands + { + cmd: "./gitea -h", + exp: "DEFAULT CONFIGURATION:", + }, + { + cmd: "./gitea help", + exp: "DEFAULT CONFIGURATION:", + }, + + { + cmd: "./gitea -c /dev/null -h", + exp: "ConfigFile: /dev/null", + }, + + { + cmd: "./gitea -c /dev/null help", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea help -c /dev/null", + exp: "ConfigFile: /dev/null", + }, + + { + cmd: "./gitea -c /dev/null test-cmd -h", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea test-cmd -c /dev/null -h", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea test-cmd -h -c /dev/null", + exp: "ConfigFile: /dev/null", + }, + + { + cmd: "./gitea -c /dev/null test-cmd help", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea test-cmd -c /dev/null help", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea test-cmd help -c /dev/null", + exp: "ConfigFile: /dev/null", + }, + + // parse paths + { + cmd: "./gitea test-cmd", + exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf), + }, + { + cmd: "./gitea -c /tmp/app.ini test-cmd", + exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"), + }, + { + cmd: "./gitea test-cmd -c /tmp/app.ini", + exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"), + }, + { + env: map[string]string{"GITEA_WORK_DIR": "/tmp"}, + cmd: "./gitea test-cmd", + exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"), + }, + { + env: map[string]string{"GITEA_WORK_DIR": "/tmp"}, + cmd: "./gitea test-cmd --work-path /tmp/other", + exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"), + }, + { + env: map[string]string{"GITEA_WORK_DIR": "/tmp"}, + cmd: "./gitea test-cmd --config /tmp/app-other.ini", + exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"), + }, + } + + for _, c := range cases { + t.Run(c.cmd, func(t *testing.T) { + app := newTestApp(cli.Command{ + Action: func(ctx context.Context, cmd *cli.Command) error { + _, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) + return nil + }, + }) + for k, v := range c.env { + t.Setenv(k, v) + } + args := strings.Split(c.cmd, " ") // for test only, "split" is good enough + r, err := runTestApp(app, args...) + assert.NoError(t, err, c.cmd) + assert.NotEmpty(t, c.exp, c.cmd) + if !assert.Contains(t, r.Stdout, c.exp, c.cmd) { + t.Log("Full output:\n" + r.Stdout) + t.Log("Expected:\n" + c.exp) + } + }) + } +} + +func TestCliCmdError(t *testing.T) { + app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }}) + r, err := runTestApp(app, "./gitea", "test-cmd") + assert.Error(t, err) + assert.Equal(t, 1, r.ExitCode) + assert.Empty(t, r.Stdout) + assert.Equal(t, "Command error: normal error\n", r.Stderr) + + app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }}) + r, err = runTestApp(app, "./gitea", "test-cmd") + assert.Error(t, err) + assert.Equal(t, 2, r.ExitCode) + assert.Empty(t, r.Stdout) + assert.Equal(t, "exit error\n", r.Stderr) + + app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }}) + r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such") + assert.Error(t, err) + assert.Equal(t, 1, r.ExitCode) + assert.Empty(t, r.Stdout) + assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr) + + app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }}) + r, err = runTestApp(app, "./gitea", "test-cmd") + assert.NoError(t, err) + assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called + assert.Empty(t, r.Stdout) + assert.Empty(t, r.Stderr) +} + +func TestCliCmdBefore(t *testing.T) { + ctxNew := context.WithValue(context.Background(), any("key"), "value") + configValues := map[string]string{} + setting.CustomConf = "/tmp/any.ini" + var actionCtx context.Context + app := newTestApp(cli.Command{ + Before: func(context.Context, *cli.Command) (context.Context, error) { + configValues["before"] = setting.CustomConf + return ctxNew, nil + }, + Action: func(ctx context.Context, cmd *cli.Command) error { + configValues["action"] = setting.CustomConf + actionCtx = ctx + return nil + }, + }) + _, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd") + assert.NoError(t, err) + assert.Equal(t, ctxNew, actionCtx) + assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config") + assert.Equal(t, "/dev/null", configValues["action"]) +} diff --git a/cmd/docs.go b/cmd/docs.go index 098c0e9a8a1..8e0f9428df3 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -13,23 +13,24 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDocs represents the available docs sub-command. -var CmdDocs = &cli.Command{ - Name: "docs", - Usage: "Output CLI documentation", - Description: "A command to output Gitea's CLI documentation, optionally to a file.", - Action: runDocs, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "man", - Usage: "Output man pages instead", +func newDocsCommand() *cli.Command { + return &cli.Command{ + Name: "docs", + Usage: "Output CLI documentation", + Description: "A command to output Gitea's CLI documentation, optionally to a file.", + Action: runDocs, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "man", + Usage: "Output man pages instead", + }, + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "Path to output to instead of stdout (will overwrite if exists)", + }, }, - &cli.StringFlag{ - Name: "output", - Aliases: []string{"o"}, - Usage: "Path to output to instead of stdout (will overwrite if exists)", - }, - }, + } } func runDocs(_ context.Context, cmd *cli.Command) error { diff --git a/cmd/doctor.go b/cmd/doctor.go index 596dd611786..188740dbcea 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -24,73 +24,77 @@ import ( "xorm.io/xorm" ) -// CmdDoctor represents the available doctor sub-command. -var CmdDoctor = &cli.Command{ - Name: "doctor", - Usage: "Diagnose and optionally fix problems, convert or re-create database tables", - Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", - - Commands: []*cli.Command{ - cmdDoctorCheck, - cmdRecreateTable, - cmdDoctorConvert, - }, +func newDoctorCommand() *cli.Command { + return &cli.Command{ + Name: "doctor", + Usage: "Diagnose and optionally fix problems, convert or re-create database tables", + Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", + Commands: []*cli.Command{ + newDoctorCheckCommand(), + newRecreateTableCommand(), + newDoctorConvertCommand(), + }, + } } -var cmdDoctorCheck = &cli.Command{ - Name: "check", - Usage: "Diagnose and optionally fix problems", - Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", - Action: runDoctorCheck, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "list", - Usage: "List the available checks", +func newDoctorCheckCommand() *cli.Command { + return &cli.Command{ + Name: "check", + Usage: "Diagnose and optionally fix problems", + Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", + Action: runDoctorCheck, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "list", + Usage: "List the available checks", + }, + &cli.BoolFlag{ + Name: "default", + Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)", + }, + &cli.StringSliceFlag{ + Name: "run", + Usage: "Run the provided checks - (if --default is set, the default checks will also run)", + }, + &cli.BoolFlag{ + Name: "all", + Usage: "Run all the available checks", + }, + &cli.BoolFlag{ + Name: "fix", + Usage: "Automatically fix what we can", + }, + &cli.StringFlag{ + Name: "log-file", + Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`, + }, + &cli.BoolFlag{ + Name: "color", + Aliases: []string{"H"}, + Usage: "Use color for outputted information", + }, }, - &cli.BoolFlag{ - Name: "default", - Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)", - }, - &cli.StringSliceFlag{ - Name: "run", - Usage: "Run the provided checks - (if --default is set, the default checks will also run)", - }, - &cli.BoolFlag{ - Name: "all", - Usage: "Run all the available checks", - }, - &cli.BoolFlag{ - Name: "fix", - Usage: "Automatically fix what we can", - }, - &cli.StringFlag{ - Name: "log-file", - Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`, - }, - &cli.BoolFlag{ - Name: "color", - Aliases: []string{"H"}, - Usage: "Use color for outputted information", - }, - }, + } } -var cmdRecreateTable = &cli.Command{ - Name: "recreate-table", - Usage: "Recreate tables from XORM definitions and copy the data.", - ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "debug", - Usage: "Print SQL commands sent", +func newRecreateTableCommand() *cli.Command { + return &cli.Command{ + Name: "recreate-table", + Usage: "Recreate tables from XORM definitions and copy the data.", + ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "Print SQL commands sent", + }, }, - }, - Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns. + Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns. This command will cause Xorm to recreate tables, copying over the data and deleting the old table. You should back-up your database before doing this and ensure that your database is up-to-date first.`, - Action: runRecreateTable, + Action: runRecreateTable, + } } func runRecreateTable(ctx context.Context, cmd *cli.Command) error { diff --git a/cmd/doctor_convert.go b/cmd/doctor_convert.go index 8cb718d3839..f4867912ab3 100644 --- a/cmd/doctor_convert.go +++ b/cmd/doctor_convert.go @@ -14,12 +14,13 @@ import ( "github.com/urfave/cli/v3" ) -// cmdDoctorConvert represents the available convert sub-command. -var cmdDoctorConvert = &cli.Command{ - Name: "convert", - Usage: "Convert the database", - Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar", - Action: runDoctorConvert, +func newDoctorConvertCommand() *cli.Command { + return &cli.Command{ + Name: "convert", + Usage: "Convert the database", + Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar", + Action: runDoctorConvert, + } } func runDoctorConvert(ctx context.Context, cmd *cli.Command) error { diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index da942b38b60..7d2f3589475 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -23,7 +23,7 @@ func TestDoctorRun(t *testing.T) { SkipDatabaseInitialization: true, }) app := &cli.Command{ - Commands: []*cli.Command{cmdDoctorCheck}, + Commands: []*cli.Command{newDoctorCheckCommand()}, } err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"}) assert.NoError(t, err) diff --git a/cmd/dump.go b/cmd/dump.go index 7f0b23ed984..40b73f69aa1 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -23,78 +23,79 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDump represents the available dump sub-command. -var CmdDump = &cli.Command{ - Name: "dump", - Usage: "Dump Gitea files and database", - Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`, - Action: runDump, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "file", - Aliases: []string{"f"}, - Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`, +func newDumpCommand() *cli.Command { + return &cli.Command{ + Name: "dump", + Usage: "Dump Gitea files and database", + Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`, + Action: runDump, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`, + }, + &cli.BoolFlag{ + Name: "verbose", + Aliases: []string{"V"}, + Usage: "Show process details", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display warnings and errors", + }, + &cli.StringFlag{ + Name: "tempdir", + Aliases: []string{"t"}, + Value: os.TempDir(), + Usage: "Temporary dir path", + }, + &cli.StringFlag{ + Name: "database", + Aliases: []string{"d"}, + Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres", + }, + &cli.BoolFlag{ + Name: "skip-repository", + Aliases: []string{"R"}, + Usage: "Skip the repository dumping", + }, + &cli.BoolFlag{ + Name: "skip-log", + Aliases: []string{"L"}, + Usage: "Skip the log dumping", + }, + &cli.BoolFlag{ + Name: "skip-custom-dir", + Usage: "Skip custom directory", + }, + &cli.BoolFlag{ + Name: "skip-lfs-data", + Usage: "Skip LFS data", + }, + &cli.BoolFlag{ + Name: "skip-attachment-data", + Usage: "Skip attachment data", + }, + &cli.BoolFlag{ + Name: "skip-package-data", + Usage: "Skip package data", + }, + &cli.BoolFlag{ + Name: "skip-index", + Usage: "Skip bleve index data", + }, + &cli.BoolFlag{ + Name: "skip-db", + Usage: "Skip database", + }, + &cli.StringFlag{ + Name: "type", + Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "), + }, }, - &cli.BoolFlag{ - Name: "verbose", - Aliases: []string{"V"}, - Usage: "Show process details", - }, - &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Only display warnings and errors", - }, - &cli.StringFlag{ - Name: "tempdir", - Aliases: []string{"t"}, - Value: os.TempDir(), - Usage: "Temporary dir path", - }, - &cli.StringFlag{ - Name: "database", - Aliases: []string{"d"}, - Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres", - }, - &cli.BoolFlag{ - Name: "skip-repository", - Aliases: []string{"R"}, - Usage: "Skip the repository dumping", - }, - &cli.BoolFlag{ - Name: "skip-log", - Aliases: []string{"L"}, - Usage: "Skip the log dumping", - }, - &cli.BoolFlag{ - Name: "skip-custom-dir", - Usage: "Skip custom directory", - }, - &cli.BoolFlag{ - Name: "skip-lfs-data", - Usage: "Skip LFS data", - }, - &cli.BoolFlag{ - Name: "skip-attachment-data", - Usage: "Skip attachment data", - }, - &cli.BoolFlag{ - Name: "skip-package-data", - Usage: "Skip package data", - }, - &cli.BoolFlag{ - Name: "skip-index", - Usage: "Skip bleve index data", - }, - &cli.BoolFlag{ - Name: "skip-db", - Usage: "Skip database", - }, - &cli.StringFlag{ - Name: "type", - Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "), - }, - }, + } } func fatal(format string, args ...any) { @@ -202,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/dump_repo.go b/cmd/dump_repo.go index beda305c85f..367454366ec 100644 --- a/cmd/dump_repo.go +++ b/cmd/dump_repo.go @@ -22,61 +22,62 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDumpRepository represents the available dump repository sub-command. -var CmdDumpRepository = &cli.Command{ - Name: "dump-repo", - Usage: "Dump the repository from git/github/gitea/gitlab", - Description: "This is a command for dumping the repository data.", - Action: runDumpRepository, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "git_service", - Value: "", - Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", - }, - &cli.StringFlag{ - Name: "repo_dir", - Aliases: []string{"r"}, - Value: "./data", - Usage: "Repository dir path to store the data", - }, - &cli.StringFlag{ - Name: "clone_addr", - Value: "", - Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", - }, - &cli.StringFlag{ - Name: "auth_username", - Value: "", - Usage: "The username to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "auth_password", - Value: "", - Usage: "The password to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "auth_token", - Value: "", - Usage: "The personal token to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "owner_name", - Value: "", - Usage: "The data will be stored on a directory with owner name if not empty", - }, - &cli.StringFlag{ - Name: "repo_name", - Value: "", - Usage: "The data will be stored on a directory with repository name if not empty", - }, - &cli.StringFlag{ - Name: "units", - Value: "", - Usage: `Which items will be migrated, one or more units should be separated as comma. +func newDumpRepositoryCommand() *cli.Command { + return &cli.Command{ + Name: "dump-repo", + Usage: "Dump the repository from git/github/gitea/gitlab", + Description: "This is a command for dumping the repository data.", + Action: runDumpRepository, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "git_service", + Value: "", + Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", + }, + &cli.StringFlag{ + Name: "repo_dir", + Aliases: []string{"r"}, + Value: "./data", + Usage: "Repository dir path to store the data", + }, + &cli.StringFlag{ + Name: "clone_addr", + Value: "", + Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", + }, + &cli.StringFlag{ + Name: "auth_username", + Value: "", + Usage: "The username to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "auth_password", + Value: "", + Usage: "The password to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "auth_token", + Value: "", + Usage: "The personal token to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "owner_name", + Value: "", + Usage: "The data will be stored on a directory with owner name if not empty", + }, + &cli.StringFlag{ + Name: "repo_name", + Value: "", + Usage: "The data will be stored on a directory with repository name if not empty", + }, + &cli.StringFlag{ + Name: "units", + Value: "", + Usage: `Which items will be migrated, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, + }, }, - }, + } } func runDumpRepository(ctx context.Context, cmd *cli.Command) error { diff --git a/cmd/embedded.go b/cmd/embedded.go index 9180407fd18..e2110756b8f 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -23,20 +23,23 @@ import ( "github.com/urfave/cli/v3" ) -// CmdEmbedded represents the available extract sub-command. -var ( - CmdEmbedded = &cli.Command{ +var matchedAssetFiles []assetFile + +func newEmbeddedCommand() *cli.Command { + return &cli.Command{ Name: "embedded", Usage: "Extract embedded resources", Description: "A command for extracting embedded resources, like templates and images", Commands: []*cli.Command{ - subcmdList, - subcmdView, - subcmdExtract, + newEmbeddedListCommand(), + newEmbeddedViewCommand(), + newEmbeddedExtractCommand(), }, } +} - subcmdList = &cli.Command{ +func newEmbeddedListCommand() *cli.Command { + return &cli.Command{ Name: "list", Usage: "List files matching the given pattern", Action: runList, @@ -48,8 +51,10 @@ var ( }, }, } +} - subcmdView = &cli.Command{ +func newEmbeddedViewCommand() *cli.Command { + return &cli.Command{ Name: "view", Usage: "View a file matching the given pattern", Action: runView, @@ -61,8 +66,10 @@ var ( }, }, } +} - subcmdExtract = &cli.Command{ +func newEmbeddedExtractCommand() *cli.Command { + return &cli.Command{ Name: "extract", Usage: "Extract resources", Action: runExtract, @@ -91,9 +98,7 @@ var ( }, }, } - - matchedAssetFiles []assetFile -) +} type assetFile struct { fs *assetfs.LayeredFS diff --git a/cmd/generate.go b/cmd/generate.go index 9cb4cf39171..21f8b42bff7 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -15,45 +15,52 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdGenerate represents the available generate sub-command. - CmdGenerate = &cli.Command{ +func newGenerateCommand() *cli.Command { + return &cli.Command{ Name: "generate", Usage: "Generate Gitea's secrets/keys/tokens", Commands: []*cli.Command{ - subcmdSecret, + newGenerateSecretCommand(), }, } +} - subcmdSecret = &cli.Command{ +func newGenerateSecretCommand() *cli.Command { + return &cli.Command{ Name: "secret", Usage: "Generate a secret token", Commands: []*cli.Command{ - microcmdGenerateInternalToken, - microcmdGenerateLfsJwtSecret, - microcmdGenerateSecretKey, + newGenerateInternalTokenCommand(), + newGenerateLfsJWTSecretCommand(), + newGenerateSecretKeyCommand(), }, } +} - microcmdGenerateInternalToken = &cli.Command{ +func newGenerateInternalTokenCommand() *cli.Command { + return &cli.Command{ Name: "INTERNAL_TOKEN", Usage: "Generate a new INTERNAL_TOKEN", Action: runGenerateInternalToken, } +} - microcmdGenerateLfsJwtSecret = &cli.Command{ +func newGenerateLfsJWTSecretCommand() *cli.Command { + return &cli.Command{ Name: "JWT_SECRET", Aliases: []string{"LFS_JWT_SECRET"}, Usage: "Generate a new JWT_SECRET", Action: runGenerateLfsJwtSecret, } +} - microcmdGenerateSecretKey = &cli.Command{ +func newGenerateSecretKeyCommand() *cli.Command { + return &cli.Command{ Name: "SECRET_KEY", Usage: "Generate a new SECRET_KEY", Action: runGenerateSecretKey, } -) +} func runGenerateInternalToken(_ context.Context, c *cli.Command) error { internalToken, err := generate.NewInternalToken() @@ -71,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 25e90a16950..ca4cddb49d7 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/hook.go b/cmd/hook.go index 4f6492b0f05..4a6c7c2905a 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -28,23 +28,24 @@ const ( hookBatchSize = 500 ) -var ( - // CmdHook represents the available hooks sub-command. - CmdHook = &cli.Command{ +func newHookCommand() *cli.Command { + return &cli.Command{ Name: "hook", Usage: "(internal) Should only be called by Git", Hidden: true, // internal commands shouldn't be visible Description: "Delegate commands to corresponding Git hooks", Before: PrepareConsoleLoggerLevel(log.FATAL), Commands: []*cli.Command{ - subcmdHookPreReceive, - subcmdHookUpdate, - subcmdHookPostReceive, - subcmdHookProcReceive, + newHookPreReceiveCommand(), + newHookUpdateCommand(), + newHookPostReceiveCommand(), + newHookProcReceiveCommand(), }, } +} - subcmdHookPreReceive = &cli.Command{ +func newHookPreReceiveCommand() *cli.Command { + return &cli.Command{ Name: "pre-receive", Usage: "Delegate pre-receive Git hook", Description: "This command should only be called by Git", @@ -55,7 +56,10 @@ var ( }, }, } - subcmdHookUpdate = &cli.Command{ +} + +func newHookUpdateCommand() *cli.Command { + return &cli.Command{ Name: "update", Usage: "Delegate update Git hook", Description: "This command should only be called by Git", @@ -66,7 +70,10 @@ var ( }, }, } - subcmdHookPostReceive = &cli.Command{ +} + +func newHookPostReceiveCommand() *cli.Command { + return &cli.Command{ Name: "post-receive", Usage: "Delegate post-receive Git hook", Description: "This command should only be called by Git", @@ -77,8 +84,11 @@ var ( }, }, } - // Note: new hook since git 2.29 - subcmdHookProcReceive = &cli.Command{ +} + +// Note: new hook since git 2.29 +func newHookProcReceiveCommand() *cli.Command { + return &cli.Command{ Name: "proc-receive", Usage: "Delegate proc-receive Git hook", Description: "This command should only be called by Git", @@ -89,7 +99,7 @@ var ( }, }, } -) +} type delayWriter struct { internal io.Writer @@ -194,7 +204,7 @@ Gitea or set your environment appropriately.`, "") userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64) deployKeyID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvDeployKeyID), 10, 64) - actionPerm, _ := strconv.Atoi(os.Getenv(repo_module.EnvActionPerm)) + actionsTaskID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvActionsTaskID), 10, 64) hookOptions := private.HookOptions{ UserID: userID, @@ -204,7 +214,7 @@ Gitea or set your environment appropriately.`, "") GitPushOptions: pushOptions(), PullRequestID: prID, DeployKeyID: deployKeyID, - ActionPerm: actionPerm, + ActionsTaskID: actionsTaskID, IsWiki: isWiki, } @@ -276,6 +286,9 @@ Gitea or set your environment appropriately.`, "") lastline = 0 } } + if err := scanner.Err(); err != nil { + return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err) + } if count > 0 { hookOptions.OldCommitIDs = oldCommitIDs[:count] @@ -415,6 +428,11 @@ Gitea or set your environment appropriately.`, "") count = 0 } } + if err := scanner.Err(); err != nil { + _ = dWriter.Close() + hookPrintResults(results) + return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err) + } if count == 0 { if wasEmpty && masterPushed { diff --git a/cmd/keys.go b/cmd/keys.go index 035d39bfb86..912cf250915 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -15,40 +15,42 @@ import ( "github.com/urfave/cli/v3" ) -// CmdKeys represents the available keys sub-command -var CmdKeys = &cli.Command{ - Name: "keys", - Usage: "(internal) Should only be called by SSH server", - Hidden: true, // internal commands shouldn't be visible - Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint", - Before: PrepareConsoleLoggerLevel(log.FATAL), - Action: runKeys, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "expected", - Aliases: []string{"e"}, - Value: "git", - Usage: "Expected user for whom provide key commands", +// NewKeysCommand returns the internal SSH key lookup sub-command. +func NewKeysCommand() *cli.Command { + return &cli.Command{ + Name: "keys", + Usage: "(internal) Should only be called by SSH server", + Hidden: true, // internal commands shouldn't be visible + Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint", + Before: PrepareConsoleLoggerLevel(log.FATAL), + Action: runKeys, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "expected", + Aliases: []string{"e"}, + Value: "git", + Usage: "Expected user for whom provide key commands", + }, + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Value: "", + Usage: "Username trying to log in by SSH", + }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "", + Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)", + }, + &cli.StringFlag{ + Name: "content", + Aliases: []string{"k"}, + Value: "", + Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)", + }, }, - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Value: "", - Usage: "Username trying to log in by SSH", - }, - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Value: "", - Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)", - }, - &cli.StringFlag{ - Name: "content", - Aliases: []string{"k"}, - Value: "", - Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)", - }, - }, + } } func runKeys(ctx context.Context, c *cli.Command) error { diff --git a/cmd/main.go b/cmd/main.go index 2ee00382d75..27d8cba2e90 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -48,7 +48,7 @@ DEFAULT CONFIGURATION: } } -func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) { +func PrepareSubcommandWithGlobalFlags(originCmd *cli.Command) { originBefore := originCmd.Before originCmd.Before = func(ctxOrig context.Context, cmd *cli.Command) (ctx context.Context, err error) { ctx = ctxOrig @@ -112,39 +112,40 @@ func NewMainApp(appVer AppVersion) *cli.Command { Usage: "Set custom path (defaults to '{WorkPath}/custom')", }, } + webCmd := newWebCommand() // these sub-commands need to use a config file subCmdWithConfig := []*cli.Command{ - CmdWeb, - CmdServ, - CmdHook, - CmdKeys, - CmdDump, - CmdAdmin, - CmdMigrate, - CmdDoctor, - CmdManager, - CmdEmbedded, - CmdMigrateStorage, - CmdDumpRepository, - CmdRestoreRepository, - CmdActions, + webCmd, + newServCommand(), + newHookCommand(), + NewKeysCommand(), + newDumpCommand(), + newAdminCommand(), + newMigrateCommand(), + newDoctorCommand(), + newManagerCommand(), + newEmbeddedCommand(), + newMigrateStorageCommand(), + newDumpRepositoryCommand(), + newRestoreRepositoryCommand(), + newActionsCommand(), } // these sub-commands do not need the config file, and they do not depend on any path or environment variable. subCmdStandalone := []*cli.Command{ cmdConfig(), cmdCert(), - CmdGenerate, - CmdDocs, + newGenerateCommand(), + newDocsCommand(), } // TODO: we should eventually drop the default command, // but not sure whether it would break Windows users who used to double-click the EXE to run. - app.DefaultCommand = CmdWeb.Name + app.DefaultCommand = webCmd.Name app.Before = PrepareConsoleLoggerLevel(log.INFO) for i := range subCmdWithConfig { - prepareSubcommandWithGlobalFlags(subCmdWithConfig[i]) + PrepareSubcommandWithGlobalFlags(subCmdWithConfig[i]) } app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdStandalone...) diff --git a/cmd/main_test.go b/cmd/main_test.go index b1f6bb3ba9b..f367bf12e42 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -5,17 +5,9 @@ package cmd import ( "context" - "errors" - "fmt" - "io" - "path/filepath" - "strings" "testing" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" "github.com/urfave/cli/v3" @@ -25,209 +17,28 @@ func TestMain(m *testing.M) { unittest.MainTest(m) } -func makePathOutput(workPath, customPath, customConf string) string { - return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf) -} - -func newTestApp(testCmd cli.Command) *cli.Command { - app := NewMainApp(AppVersion{}) - testCmd.Name = util.IfZero(testCmd.Name, "test-cmd") - prepareSubcommandWithGlobalFlags(&testCmd) - app.Commands = append(app.Commands, &testCmd) - app.DefaultCommand = testCmd.Name - return app -} - -type runResult struct { - Stdout string - Stderr string - ExitCode int -} - -func runTestApp(app *cli.Command, args ...string) (runResult, error) { - outBuf := new(strings.Builder) - errBuf := new(strings.Builder) - app.Writer = outBuf - app.ErrWriter = errBuf - exitCode := -1 - defer test.MockVariableValue(&cli.ErrWriter, app.ErrWriter)() - defer test.MockVariableValue(&cli.OsExiter, func(code int) { - if exitCode == -1 { - exitCode = code // save the exit code once and then reset the writer (to simulate the exit) - app.Writer, app.ErrWriter, cli.ErrWriter = io.Discard, io.Discard, io.Discard - } - })() - err := RunMainApp(app, args...) - return runResult{outBuf.String(), errBuf.String(), exitCode}, err -} - -func TestCliCmd(t *testing.T) { - defaultWorkPath := filepath.Dir(setting.AppPath) - defaultCustomPath := filepath.Join(defaultWorkPath, "custom") - defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini") - - cli.CommandHelpTemplate = "(command help template)" - cli.RootCommandHelpTemplate = "(app help template)" - cli.SubcommandHelpTemplate = "(subcommand help template)" - - cases := []struct { - env map[string]string - cmd string - exp string - }{ - // help commands - { - cmd: "./gitea -h", - exp: "DEFAULT CONFIGURATION:", - }, - { - cmd: "./gitea help", - exp: "DEFAULT CONFIGURATION:", - }, - - { - cmd: "./gitea -c /dev/null -h", - exp: "ConfigFile: /dev/null", - }, - - { - cmd: "./gitea -c /dev/null help", - exp: "ConfigFile: /dev/null", - }, - { - cmd: "./gitea help -c /dev/null", - exp: "ConfigFile: /dev/null", - }, - - { - cmd: "./gitea -c /dev/null test-cmd -h", - exp: "ConfigFile: /dev/null", - }, - { - cmd: "./gitea test-cmd -c /dev/null -h", - exp: "ConfigFile: /dev/null", - }, - { - cmd: "./gitea test-cmd -h -c /dev/null", - exp: "ConfigFile: /dev/null", - }, - - { - cmd: "./gitea -c /dev/null test-cmd help", - exp: "ConfigFile: /dev/null", - }, - { - cmd: "./gitea test-cmd -c /dev/null help", - exp: "ConfigFile: /dev/null", - }, - { - cmd: "./gitea test-cmd help -c /dev/null", - exp: "ConfigFile: /dev/null", - }, - - // parse paths - { - cmd: "./gitea test-cmd", - exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf), - }, - { - cmd: "./gitea -c /tmp/app.ini test-cmd", - exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"), - }, - { - cmd: "./gitea test-cmd -c /tmp/app.ini", - exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"), - }, - { - env: map[string]string{"GITEA_WORK_DIR": "/tmp"}, - cmd: "./gitea test-cmd", - exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"), - }, - { - env: map[string]string{"GITEA_WORK_DIR": "/tmp"}, - cmd: "./gitea test-cmd --work-path /tmp/other", - exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"), - }, - { - env: map[string]string{"GITEA_WORK_DIR": "/tmp"}, - cmd: "./gitea test-cmd --config /tmp/app-other.ini", - exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"), - }, - } - - for _, c := range cases { - t.Run(c.cmd, func(t *testing.T) { - defer test.MockVariableValue(&setting.InstallLock, false)() - app := newTestApp(cli.Command{ - Action: func(ctx context.Context, cmd *cli.Command) error { - _, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) - return nil +func TestDefaultCommand(t *testing.T) { + test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) { + called := false + cmd := &cli.Command{ + DefaultCommand: "test", + Commands: []*cli.Command{ + { + Name: "test", + Action: func(ctx context.Context, command *cli.Command) error { + retName, retValid := isValidDefaultSubCommand(command) + assert.Equal(t, expectedRetName, retName) + assert.Equal(t, expectedRetValid, retValid) + called = true + return nil + }, }, - }) - for k, v := range c.env { - t.Setenv(k, v) - } - args := strings.Split(c.cmd, " ") // for test only, "split" is good enough - r, err := runTestApp(app, args...) - assert.NoError(t, err, c.cmd) - assert.NotEmpty(t, c.exp, c.cmd) - if !assert.Contains(t, r.Stdout, c.exp, c.cmd) { - t.Log("Full output:\n" + r.Stdout) - t.Log("Expected:\n" + c.exp) - } - }) + }, + } + assert.NoError(t, cmd.Run(t.Context(), args)) + assert.True(t, called) } -} - -func TestCliCmdError(t *testing.T) { - app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }}) - r, err := runTestApp(app, "./gitea", "test-cmd") - assert.Error(t, err) - assert.Equal(t, 1, r.ExitCode) - assert.Empty(t, r.Stdout) - assert.Equal(t, "Command error: normal error\n", r.Stderr) - - app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }}) - r, err = runTestApp(app, "./gitea", "test-cmd") - assert.Error(t, err) - assert.Equal(t, 2, r.ExitCode) - assert.Empty(t, r.Stdout) - assert.Equal(t, "exit error\n", r.Stderr) - - app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }}) - r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such") - assert.Error(t, err) - assert.Equal(t, 1, r.ExitCode) - assert.Empty(t, r.Stdout) - assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr) - - app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }}) - r, err = runTestApp(app, "./gitea", "test-cmd") - assert.NoError(t, err) - assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called - assert.Empty(t, r.Stdout) - assert.Empty(t, r.Stderr) -} - -func TestCliCmdBefore(t *testing.T) { - ctxNew := context.WithValue(context.Background(), any("key"), "value") - configValues := map[string]string{} - setting.CustomConf = "/tmp/any.ini" - var actionCtx context.Context - app := newTestApp(cli.Command{ - Before: func(context.Context, *cli.Command) (context.Context, error) { - configValues["before"] = setting.CustomConf - return ctxNew, nil - }, - Action: func(ctx context.Context, cmd *cli.Command) error { - configValues["action"] = setting.CustomConf - actionCtx = ctx - return nil - }, - }) - _, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd") - assert.NoError(t, err) - assert.Equal(t, ctxNew, actionCtx) - assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config") - assert.Equal(t, "/dev/null", configValues["action"]) + test(t, []string{"./gitea"}, "", true) + test(t, []string{"./gitea", "test"}, "", true) + test(t, []string{"./gitea", "other"}, "other", false) } diff --git a/cmd/manager.go b/cmd/manager.go index f0935ea0657..586c65990bc 100644 --- a/cmd/manager.go +++ b/cmd/manager.go @@ -13,22 +13,24 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdManager represents the manager command - CmdManager = &cli.Command{ +func newManagerCommand() *cli.Command { + return &cli.Command{ Name: "manager", Usage: "Manage the running gitea process", Description: "This is a command for managing the running gitea process", Commands: []*cli.Command{ - subcmdShutdown, - subcmdRestart, - subcmdReloadTemplates, - subcmdFlushQueues, - subcmdLogging, - subCmdProcesses, + newShutdownCommand(), + newRestartCommand(), + newReloadTemplatesCommand(), + newFlushQueuesCommand(), + newLoggingCommand(), + newProcessesCommand(), }, } - subcmdShutdown = &cli.Command{ +} + +func newShutdownCommand() *cli.Command { + return &cli.Command{ Name: "shutdown", Usage: "Gracefully shutdown the running process", Flags: []cli.Flag{ @@ -38,7 +40,10 @@ var ( }, Action: runShutdown, } - subcmdRestart = &cli.Command{ +} + +func newRestartCommand() *cli.Command { + return &cli.Command{ Name: "restart", Usage: "Gracefully restart the running process - (not implemented for windows servers)", Flags: []cli.Flag{ @@ -48,7 +53,10 @@ var ( }, Action: runRestart, } - subcmdReloadTemplates = &cli.Command{ +} + +func newReloadTemplatesCommand() *cli.Command { + return &cli.Command{ Name: "reload-templates", Usage: "Reload template files in the running process", Flags: []cli.Flag{ @@ -58,7 +66,10 @@ var ( }, Action: runReloadTemplates, } - subcmdFlushQueues = &cli.Command{ +} + +func newFlushQueuesCommand() *cli.Command { + return &cli.Command{ Name: "flush-queues", Usage: "Flush queues in the running process", Action: runFlushQueues, @@ -77,7 +88,10 @@ var ( }, }, } - subCmdProcesses = &cli.Command{ +} + +func newProcessesCommand() *cli.Command { + return &cli.Command{ Name: "processes", Usage: "Display running processes within the current process", Action: runProcesses, @@ -107,7 +121,7 @@ var ( }, }, } -) +} func runShutdown(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go index ac29e7d3e50..5812e707e20 100644 --- a/cmd/manager_logging.go +++ b/cmd/manager_logging.go @@ -15,8 +15,8 @@ import ( "github.com/urfave/cli/v3" ) -var ( - defaultLoggingFlags = []cli.Flag{ +func defaultLoggingFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "logger", Usage: `Logger name - will default to "default"`, @@ -57,8 +57,10 @@ var ( Name: "debug", }, } +} - subcmdLogging = &cli.Command{ +func newLoggingCommand() *cli.Command { + return &cli.Command{ Name: "logging", Usage: "Adjust logging commands", Commands: []*cli.Command{ @@ -109,7 +111,7 @@ var ( { Name: "file", Usage: "Add a file logger", - Flags: append(defaultLoggingFlags, []cli.Flag{ + Flags: append(defaultLoggingFlags(), []cli.Flag{ &cli.StringFlag{ Name: "filename", Aliases: []string{"f"}, @@ -150,7 +152,7 @@ var ( }, { Name: "conn", Usage: "Add a net conn logger", - Flags: append(defaultLoggingFlags, []cli.Flag{ + Flags: append(defaultLoggingFlags(), []cli.Flag{ &cli.BoolFlag{ Name: "reconnect-on-message", Aliases: []string{"R"}, @@ -191,7 +193,7 @@ var ( }, }, } -) +} func runRemoveLogger(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) diff --git a/cmd/migrate.go b/cmd/migrate.go index e24dc9e5720..016f8a0db56 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -14,12 +14,13 @@ import ( "github.com/urfave/cli/v3" ) -// CmdMigrate represents the available migrate sub-command. -var CmdMigrate = &cli.Command{ - Name: "migrate", - Usage: "Migrate the database", - Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, - Action: runMigrate, +func newMigrateCommand() *cli.Command { + return &cli.Command{ + Name: "migrate", + Usage: "Migrate the database", + Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, + Action: runMigrate, + } } func runMigrate(ctx context.Context, c *cli.Command) error { diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index a6bf9fa4b54..c9b82055c13 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -25,107 +25,108 @@ import ( "github.com/urfave/cli/v3" ) -// CmdMigrateStorage represents the available migrate storage sub-command. -var CmdMigrateStorage = &cli.Command{ - Name: "migrate-storage", - Usage: "Migrate the storage", - Description: "Copies stored files from storage configured in app.ini to parameter-configured storage", - Action: runMigrateStorage, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Value: "", - Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'", +func newMigrateStorageCommand() *cli.Command { + return &cli.Command{ + Name: "migrate-storage", + Usage: "Migrate the storage", + Description: "Copies stored files from storage configured in app.ini to parameter-configured storage", + Action: runMigrateStorage, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "", + Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'", + }, + &cli.StringFlag{ + Name: "storage", + Aliases: []string{"s"}, + Value: "", + Usage: "New storage type: local (default), minio or azureblob", + }, + &cli.StringFlag{ + Name: "path", + Aliases: []string{"p"}, + Value: "", + Usage: "New storage placement if store is local (leave blank for default)", + }, + // Minio Storage special configurations + &cli.StringFlag{ + Name: "minio-endpoint", + Value: "", + Usage: "Minio storage endpoint", + }, + &cli.StringFlag{ + Name: "minio-access-key-id", + Value: "", + Usage: "Minio storage accessKeyID", + }, + &cli.StringFlag{ + Name: "minio-secret-access-key", + Value: "", + Usage: "Minio storage secretAccessKey", + }, + &cli.StringFlag{ + Name: "minio-bucket", + Value: "", + Usage: "Minio storage bucket", + }, + &cli.StringFlag{ + Name: "minio-location", + Value: "", + Usage: "Minio storage location to create bucket", + }, + &cli.StringFlag{ + Name: "minio-base-path", + Value: "", + Usage: "Minio storage base path on the bucket", + }, + &cli.BoolFlag{ + Name: "minio-use-ssl", + Usage: "Enable SSL for minio", + }, + &cli.BoolFlag{ + Name: "minio-insecure-skip-verify", + Usage: "Skip SSL verification", + }, + &cli.StringFlag{ + Name: "minio-checksum-algorithm", + Value: "", + Usage: "Minio checksum algorithm (default/md5)", + }, + &cli.StringFlag{ + Name: "minio-bucket-lookup-type", + Value: "", + Usage: "Minio bucket lookup type", + }, + // Azure Blob Storage special configurations + &cli.StringFlag{ + Name: "azureblob-endpoint", + Value: "", + Usage: "Azure Blob storage endpoint", + }, + &cli.StringFlag{ + Name: "azureblob-account-name", + Value: "", + Usage: "Azure Blob storage account name", + }, + &cli.StringFlag{ + Name: "azureblob-account-key", + Value: "", + Usage: "Azure Blob storage account key", + }, + &cli.StringFlag{ + Name: "azureblob-container", + Value: "", + Usage: "Azure Blob storage container", + }, + &cli.StringFlag{ + Name: "azureblob-base-path", + Value: "", + Usage: "Azure Blob storage base path", + }, }, - &cli.StringFlag{ - Name: "storage", - Aliases: []string{"s"}, - Value: "", - Usage: "New storage type: local (default), minio or azureblob", - }, - &cli.StringFlag{ - Name: "path", - Aliases: []string{"p"}, - Value: "", - Usage: "New storage placement if store is local (leave blank for default)", - }, - // Minio Storage special configurations - &cli.StringFlag{ - Name: "minio-endpoint", - Value: "", - Usage: "Minio storage endpoint", - }, - &cli.StringFlag{ - Name: "minio-access-key-id", - Value: "", - Usage: "Minio storage accessKeyID", - }, - &cli.StringFlag{ - Name: "minio-secret-access-key", - Value: "", - Usage: "Minio storage secretAccessKey", - }, - &cli.StringFlag{ - Name: "minio-bucket", - Value: "", - Usage: "Minio storage bucket", - }, - &cli.StringFlag{ - Name: "minio-location", - Value: "", - Usage: "Minio storage location to create bucket", - }, - &cli.StringFlag{ - Name: "minio-base-path", - Value: "", - Usage: "Minio storage base path on the bucket", - }, - &cli.BoolFlag{ - Name: "minio-use-ssl", - Usage: "Enable SSL for minio", - }, - &cli.BoolFlag{ - Name: "minio-insecure-skip-verify", - Usage: "Skip SSL verification", - }, - &cli.StringFlag{ - Name: "minio-checksum-algorithm", - Value: "", - Usage: "Minio checksum algorithm (default/md5)", - }, - &cli.StringFlag{ - Name: "minio-bucket-lookup-type", - Value: "", - Usage: "Minio bucket lookup type", - }, - // Azure Blob Storage special configurations - &cli.StringFlag{ - Name: "azureblob-endpoint", - Value: "", - Usage: "Azure Blob storage endpoint", - }, - &cli.StringFlag{ - Name: "azureblob-account-name", - Value: "", - Usage: "Azure Blob storage account name", - }, - &cli.StringFlag{ - Name: "azureblob-account-key", - Value: "", - Usage: "Azure Blob storage account key", - }, - &cli.StringFlag{ - Name: "azureblob-container", - Value: "", - Usage: "Azure Blob storage container", - }, - &cli.StringFlag{ - Name: "azureblob-base-path", - Value: "", - Usage: "Azure Blob storage base path", - }, - }, + } } func migrateAttachments(ctx context.Context, dstStorage storage.ObjectStorage) error { diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index c61f5a582ef..26b4682f136 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -13,40 +13,41 @@ import ( "github.com/urfave/cli/v3" ) -// CmdRestoreRepository represents the available restore a repository sub-command. -var CmdRestoreRepository = &cli.Command{ - Name: "restore-repo", - Usage: "Restore the repository from disk", - Description: "This is a command for restoring the repository data.", - Action: runRestoreRepository, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "repo_dir", - Aliases: []string{"r"}, - Value: "./data", - Usage: "Repository dir path to restore from", - }, - &cli.StringFlag{ - Name: "owner_name", - Value: "", - Usage: "Restore destination owner name", - }, - &cli.StringFlag{ - Name: "repo_name", - Value: "", - Usage: "Restore destination repository name", - }, - &cli.StringFlag{ - Name: "units", - Value: "", - Usage: `Which items will be restored, one or more units should be separated as comma. +func newRestoreRepositoryCommand() *cli.Command { + return &cli.Command{ + Name: "restore-repo", + Usage: "Restore the repository from disk", + Description: "This is a command for restoring the repository data.", + Action: runRestoreRepository, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "repo_dir", + Aliases: []string{"r"}, + Value: "./data", + Usage: "Repository dir path to restore from", + }, + &cli.StringFlag{ + Name: "owner_name", + Value: "", + Usage: "Restore destination owner name", + }, + &cli.StringFlag{ + Name: "repo_name", + Value: "", + Usage: "Restore destination repository name", + }, + &cli.StringFlag{ + Name: "units", + Value: "", + Usage: `Which items will be restored, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, + }, + &cli.BoolFlag{ + Name: "validation", + Usage: "Sanity check the content of the files before trying to load them", + }, }, - &cli.BoolFlag{ - Name: "validation", - Usage: "Sanity check the content of the files before trying to load them", - }, - }, + } } func runRestoreRepository(ctx context.Context, c *cli.Command) error { diff --git a/cmd/serv.go b/cmd/serv.go index 4110fda0d50..a35d476c860 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -35,22 +35,23 @@ import ( "github.com/urfave/cli/v3" ) -// CmdServ represents the available serv sub-command. -var CmdServ = &cli.Command{ - Name: "serv", - Usage: "(internal) Should only be called by SSH shell", - Description: "Serv provides access auth for repositories", - Hidden: true, // Internal commands shouldn't be visible in help - Before: PrepareConsoleLoggerLevel(log.FATAL), - Action: runServ, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "enable-pprof", +func newServCommand() *cli.Command { + return &cli.Command{ + Name: "serv", + Usage: "(internal) Should only be called by SSH shell", + Description: "Serv provides access auth for repositories", + Hidden: true, // Internal commands shouldn't be visible in help + Before: PrepareConsoleLoggerLevel(log.FATAL), + Action: runServ, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "enable-pprof", + }, + &cli.BoolFlag{ + Name: "debug", + }, }, - &cli.BoolFlag{ - Name: "debug", - }, - }, + } } func setup(ctx context.Context, debug bool) { diff --git a/cmd/web.go b/cmd/web.go index 5000e780c56..994c481fc08 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -34,42 +34,43 @@ import ( // PIDFile could be set from build tag var PIDFile = "/run/gitea.pid" -// CmdWeb represents the available web sub-command. -var CmdWeb = &cli.Command{ - Name: "web", - Usage: "Start Gitea web server", - Description: `Gitea web server is the only thing you need to run, +func newWebCommand() *cli.Command { + return &cli.Command{ + Name: "web", + Usage: "Start Gitea web server", + Description: `Gitea web server is the only thing you need to run, and it takes care of all the other things for you`, - Before: PrepareConsoleLoggerLevel(log.INFO), - Action: runWeb, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "port", - Aliases: []string{"p"}, - Value: "3000", - Usage: "Temporary port number to prevent conflict", + Before: PrepareConsoleLoggerLevel(log.INFO), + Action: runWeb, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "port", + Aliases: []string{"p"}, + Value: "3000", + Usage: "Temporary port number to prevent conflict", + }, + &cli.StringFlag{ + Name: "install-port", + Value: "3000", + Usage: "Temporary port number to run the install page on to prevent conflict", + }, + &cli.StringFlag{ + Name: "pid", + Aliases: []string{"P"}, + Value: PIDFile, + Usage: "Custom pid file path", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display Fatal logging errors until logging is set-up", + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Set initial logging to TRACE level until logging is properly set-up", + }, }, - &cli.StringFlag{ - Name: "install-port", - Value: "3000", - Usage: "Temporary port number to run the install page on to prevent conflict", - }, - &cli.StringFlag{ - Name: "pid", - Aliases: []string{"P"}, - Value: PIDFile, - Usage: "Custom pid file path", - }, - &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Only display Fatal logging errors until logging is set-up", - }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Set initial logging to TRACE level until logging is properly set-up", - }, - }, + } } func runHTTPRedirector() { diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go index 5811291b42e..2af980e5e3c 100644 --- a/contrib/backport/backport.go +++ b/contrib/backport/backport.go @@ -16,7 +16,7 @@ import ( "strconv" "strings" - "github.com/google/go-github/v74/github" + "github.com/google/go-github/v84/github" "github.com/urfave/cli/v3" "gopkg.in/yaml.v3" ) diff --git a/contrib/ide/vscode/launch.json b/contrib/ide/vscode/launch.json index b80b826fc0b..048428fc27f 100644 --- a/contrib/ide/vscode/launch.json +++ b/contrib/ide/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/ide/vscode/settings.json b/contrib/ide/vscode/settings.json index e33bccf9028..d968e4daca4 100644 --- a/contrib/ide/vscode/settings.json +++ b/contrib/ide/vscode/settings.json @@ -1,4 +1,4 @@ { - "go.buildTags": "'sqlite sqlite_unlock_notify'", + "go.buildTags": "", "go.testFlags": ["-v"] -} \ No newline at end of file +} diff --git a/contrib/ide/vscode/tasks.json b/contrib/ide/vscode/tasks.json index e35ae303b27..490e30338b1 100644 --- a/contrib/ide/vscode/tasks.json +++ b/contrib/ide/vscode/tasks.json @@ -22,28 +22,6 @@ "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/upgrade.sh b/contrib/upgrade.sh index e5e296ea8b2..2593d245093 100755 --- a/contrib/upgrade.sh +++ b/contrib/upgrade.sh @@ -108,7 +108,9 @@ curl --connect-timeout 10 --silent --show-error --fail --location -O "$binurl{,. sha256sum -c "${binname}.xz.sha256" if [[ -z "${ignore_gpg:-}" ]]; then require gpg - gpg --keyserver keys.openpgp.org --recv 7C9E68152594688862D62AF62D9AE806EC1592E2 + # try to use curl first, it uses standard tcp 443 port and works better behind strict firewall rules + curl -fsSL --connect-timeout 10 "https://keys.openpgp.org/vks/v1/by-fingerprint/7C9E68152594688862D62AF62D9AE806EC1592E2" | gpg --import \ + || gpg --keyserver keys.openpgp.org --recv 7C9E68152594688862D62AF62D9AE806EC1592E2 gpg --verify "${binname}.xz.asc" "${binname}.xz" || { echo 'Signature does not match'; exit 1; } fi rm "${binname}".xz.{sha256,asc} @@ -127,6 +129,8 @@ echo "Creating backup in $giteahome" giteacmd dump $backupopts echo "Updating binary at $giteabin" cp -f "$giteabin" "$giteabin.bak" && mv -f "$binname" "$giteabin" +# Restore SELinux context if applicable (e.g. RHEL/Fedora) +command -v restorecon &>/dev/null && restorecon -v "$giteabin" || true $service_start $service_status diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 42ccbcf81a9..909e3b66dd8 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. @@ -69,13 +69,12 @@ RUN_USER = ; git ;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy. ;ROOT_URL = ;; -;; Controls how to detect the public URL. -;; Although it defaults to "legacy" (to avoid breaking existing users), most instances should use the "auto" behavior, +;; Controls how to detect the public URL. Most instances should use the "auto" behavior, ;; especially when the Gitea instance needs to be accessed in a container network. -;; * legacy: detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL". -;; * auto: always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL". +;; * legacy: (default <= 1.25) detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL". +;; * auto: (default >= 1.26) always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL". ;; * never: always use "ROOT_URL", never detect from request headers. -;PUBLIC_URL_DETECTION = legacy +;PUBLIC_URL_DETECTION = auto ;; ;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy. ;; DO NOT USE IT IN PRODUCTION!!! @@ -176,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 ;; @@ -383,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 ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -461,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 @@ -520,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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -587,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 = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1153,15 +1166,20 @@ LEVEL = Info ;; Add co-authored-by and co-committed-by trailers if committer does not match author ;ADD_CO_COMMITTER_TRAILERS = true ;; -;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply -;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = false -;; ;; 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 +;; +;; Set the default value for "Delete pull request branch after merge by default" for new repositories +;DEFAULT_DELETE_BRANCH_AFTER_MERGE = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1179,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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1506,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 @@ -1533,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 @@ -1995,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 @@ -2277,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 + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2338,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, ...) @@ -2460,6 +2508,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] @@ -2713,6 +2825,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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2881,6 +2995,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 06bd986563d..8e89b35c517 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 00000000000..dbf24813295 --- /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 00000000000..bc3e71113f2 --- /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 00000000000..80ebe821777 --- /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 00000000000..be8d9e1abf2 --- /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 8ed0cf789a7..29016ed808b 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,5 +1,6 @@ import arrayFunc from 'eslint-plugin-array-func'; import comments from '@eslint-community/eslint-plugin-eslint-comments'; +import deMorgan from 'eslint-plugin-de-morgan'; import github from 'eslint-plugin-github'; import globals from 'globals'; import importPlugin from 'eslint-plugin-import-x'; @@ -63,6 +64,7 @@ export default defineConfig([ '@stylistic': stylistic, '@typescript-eslint': typescriptPlugin.plugin, 'array-func': arrayFunc, + 'de-morgan': deMorgan, 'import-x': importPlugin as unknown as ESLint.Plugin, // https://github.com/un-ts/eslint-plugin-import-x/issues/203 regexp, sonarjs, @@ -179,7 +181,7 @@ export default defineConfig([ '@typescript-eslint/naming-convention': [0], '@typescript-eslint/no-array-constructor': [2], '@typescript-eslint/no-array-delete': [2], - '@typescript-eslint/no-base-to-string': [0], + '@typescript-eslint/no-base-to-string': [2], '@typescript-eslint/no-confusing-non-null-assertion': [2], '@typescript-eslint/no-confusing-void-expression': [0], '@typescript-eslint/no-deprecated': [2], @@ -254,10 +256,10 @@ export default defineConfig([ '@typescript-eslint/prefer-function-type': [2], '@typescript-eslint/prefer-includes': [2], '@typescript-eslint/prefer-literal-enum-member': [0], - '@typescript-eslint/prefer-namespace-keyword': [0], + '@typescript-eslint/prefer-namespace-keyword': [2], '@typescript-eslint/prefer-nullish-coalescing': [0], '@typescript-eslint/prefer-optional-chain': [2, {requireNullish: true}], - '@typescript-eslint/prefer-promise-reject-errors': [0], + '@typescript-eslint/prefer-promise-reject-errors': [2], '@typescript-eslint/prefer-readonly': [0], '@typescript-eslint/prefer-readonly-parameter-types': [0], '@typescript-eslint/prefer-reduce-type-parameter': [0], @@ -268,7 +270,7 @@ export default defineConfig([ '@typescript-eslint/require-array-sort-compare': [0], '@typescript-eslint/require-await': [0], '@typescript-eslint/restrict-plus-operands': [2], - '@typescript-eslint/restrict-template-expressions': [0], + '@typescript-eslint/restrict-template-expressions': [2], '@typescript-eslint/return-await': [0], '@typescript-eslint/strict-boolean-expressions': [0], '@typescript-eslint/strict-void-return': [0], @@ -295,6 +297,8 @@ export default defineConfig([ 'consistent-this': [0], 'constructor-super': [2], 'curly': [0], + 'de-morgan/no-negated-conjunction': [2], + 'de-morgan/no-negated-disjunction': [2], 'default-case-last': [2], 'default-case': [0], 'default-param-last': [0], @@ -325,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], @@ -568,7 +572,9 @@ export default defineConfig([ '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], @@ -586,6 +592,7 @@ export default defineConfig([ 'no-undef-init': [2], 'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time 'no-undefined': [0], + 'no-unassigned-vars': [2], 'no-underscore-dangle': [0], 'no-unexpected-multiline': [2], 'no-unmodified-loop-condition': [2], @@ -626,19 +633,20 @@ export default defineConfig([ 'prefer-numeric-literals': [2], 'prefer-object-has-own': [2], 'prefer-object-spread': [2], - 'prefer-promise-reject-errors': [2, {allowEmptyReject: false}], + 'prefer-promise-reject-errors': [0], // handled by @typescript-eslint/prefer-promise-reject-errors 'prefer-regex-literals': [2], 'prefer-rest-params': [2], 'prefer-spread': [2], 'prefer-template': [2], - 'radix': [2, 'as-needed'], + 'preserve-caught-error': [0], + 'radix': [0], 'regexp/confusing-quantifier': [2], 'regexp/control-character-escape': [2], 'regexp/hexadecimal-escape': [0], 'regexp/letter-case': [0], 'regexp/match-any': [2], 'regexp/negation': [2], - 'regexp/no-contradiction-with-assertion': [0], + 'regexp/no-contradiction-with-assertion': [2], 'regexp/no-control-character': [0], 'regexp/no-dupe-characters-character-class': [2], 'regexp/no-dupe-disjunctions': [2], @@ -654,8 +662,8 @@ export default defineConfig([ 'regexp/no-invisible-character': [2], 'regexp/no-lazy-ends': [2], 'regexp/no-legacy-features': [2], - 'regexp/no-misleading-capturing-group': [0], - 'regexp/no-misleading-unicode-character': [0], + 'regexp/no-misleading-capturing-group': [2], + 'regexp/no-misleading-unicode-character': [2], 'regexp/no-missing-g-flag': [2], 'regexp/no-non-standard-flag': [2], 'regexp/no-obscure-range': [2], @@ -756,6 +764,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], @@ -811,6 +820,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], @@ -860,6 +870,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], @@ -878,6 +889,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], @@ -912,6 +924,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], @@ -991,6 +1004,7 @@ export default defineConfig([ 'vitest/require-top-level-describe': [0], 'vitest/valid-describe-callback': [2], 'vitest/valid-expect': [2, {maxArgs: 2}], + 'vitest/valid-expect-in-promise': [2], 'vitest/valid-title': [2], }, }, @@ -1002,11 +1016,11 @@ 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}}, }, ]); diff --git a/flake.lock b/flake.lock index 3af57938421..839eaed572d 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1773389992, - "narHash": "sha256-wvfdLLWJ2I9oEpDd9PfMA8osfIZicoQ5MT1jIwNs9Tk=", + "lastModified": 1776877367, + "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c06b4ae3d6599a672a6210b7021d699c351eebda", + "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 6fb38919638..ca88b581a22 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 f0bb361f2da..20d26abc237 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,34 +9,36 @@ 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.24.1 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/ProtonMail/go-crypto v1.3.0 - github.com/PuerkitoBio/goquery v1.11.0 + 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/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.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 @@ -44,164 +46,166 @@ 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.8.0 + github.com/go-git/go-git/v5 v5.18.0 + github.com/go-ldap/ldap/v3 v3.4.13 + github.com/go-redsync/redsync/v4 v4.16.0 + github.com/go-sql-driver/mysql v1.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/v84 v84.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 v1.46.0 + 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.43.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/Microsoft/go-winio v0.6.2 // 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/dlclark/regexp2 v1.11.5 // 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,87 @@ 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.5.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 + go4.org v0.0.0-20260112195520-a5071408f32f // 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 + 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 +301,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 02e65325429..82849a19f61 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.24.1 h1:hpaqcdGcBmfMpV7JSbBJVwE99qo+WqGreJYKrDKEyW8= +code.gitea.io/sdk/gitea v0.24.1/go.mod h1:5/77BL3sHneCMEiZaMT9lfTvnnibsYxyO48mceCF3qA= +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.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= +github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= 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.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= +github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= 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/v84 v84.0.0 h1:I/0Xn5IuChMe8TdmI2bbim5nyhaRFJ7DEdzmD2w+yVA= +github.com/google/go-github/v84 v84.0.0/go.mod h1:WwYL1z1ajRdlaPszjVu/47x1L0PXukJBn73xsiYrRRQ= +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.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= +github.com/pjbgf/sha1cd v0.5.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 v1.46.0 h1:YxBWFZIFYKcGESCb9fpkwzouo+apyB9pr/XTWzNoL24= +gitlab.com/gitlab-org/api/client-go v1.46.0/go.mod h1:FtgyU6g2HS5+fMhw6nLK96GBEEBx5MzntOiJWfIaiN8= 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/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-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/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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.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 757bd13acd7..1de83a29c58 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, 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("run_id=? AND (status=? OR status=?)", 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/config.go b/models/actions/config.go new file mode 100644 index 00000000000..4f5357c5605 --- /dev/null +++ b/models/actions/config.go @@ -0,0 +1,74 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + "code.gitea.io/gitea/models/perm" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" + + "xorm.io/xorm/convert" +) + +// OwnerActionsConfig defines the Actions configuration for a user or organization +type OwnerActionsConfig struct { + // TokenPermissionMode defines the default permission mode (permissive, restricted) + TokenPermissionMode repo_model.ActionsTokenPermissionMode `json:"token_permission_mode,omitempty"` + + // MaxTokenPermissions defines the absolute maximum permissions any token can have in this context. + MaxTokenPermissions *repo_model.ActionsTokenPermissions `json:"max_token_permissions,omitempty"` + + // AllowedCrossRepoIDs is a list of specific repo IDs that can be accessed cross-repo + AllowedCrossRepoIDs []int64 `json:"allowed_cross_repo_ids,omitempty"` +} + +var _ convert.ConversionFrom = (*OwnerActionsConfig)(nil) + +func (cfg *OwnerActionsConfig) FromDB(bytes []byte) error { + _ = json.Unmarshal(bytes, cfg) + cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode) + return nil +} + +// GetOwnerActionsConfig loads the OwnerActionsConfig for a user or organization from user settings +// It returns a default config if no setting is found +func GetOwnerActionsConfig(ctx context.Context, userID int64) (ret OwnerActionsConfig, err error) { + return user_model.GetUserSettingJSON(ctx, userID, user_model.SettingsKeyActionsConfig, ret) +} + +// SetOwnerActionsConfig saves the OwnerActionsConfig for a user or organization to user settings +func SetOwnerActionsConfig(ctx context.Context, userID int64, cfg OwnerActionsConfig) error { + return user_model.SetUserSettingJSON(ctx, userID, user_model.SettingsKeyActionsConfig, cfg) +} + +// GetDefaultTokenPermissions returns the default token permissions by its TokenPermissionMode. +func (cfg *OwnerActionsConfig) GetDefaultTokenPermissions() repo_model.ActionsTokenPermissions { + switch cfg.TokenPermissionMode { + case repo_model.ActionsTokenPermissionModeRestricted: + return repo_model.MakeRestrictedPermissions() + case repo_model.ActionsTokenPermissionModePermissive: + return repo_model.MakeActionsTokenPermissions(perm.AccessModeWrite) + default: + return repo_model.MakeActionsTokenPermissions(perm.AccessModeNone) + } +} + +// GetMaxTokenPermissions returns the maximum allowed permissions +func (cfg *OwnerActionsConfig) GetMaxTokenPermissions() repo_model.ActionsTokenPermissions { + if cfg.MaxTokenPermissions != nil { + return *cfg.MaxTokenPermissions + } + // Default max is write for everything + return repo_model.MakeActionsTokenPermissions(perm.AccessModeWrite) +} + +// ClampPermissions ensures that the given permissions don't exceed the maximum +func (cfg *OwnerActionsConfig) ClampPermissions(perms repo_model.ActionsTokenPermissions) repo_model.ActionsTokenPermissions { + maxPerms := cfg.GetMaxTokenPermissions() + return repo_model.ClampActionsTokenPermissions(perms, maxPerms) +} diff --git a/models/actions/run.go b/models/actions/run.go index e293a6056fb..a44b0ff343e 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -30,7 +30,7 @@ import ( type ActionRun struct { ID int64 Title string - RepoID int64 `xorm:"unique(repo_index) index(repo_concurrency)"` + RepoID int64 `xorm:"unique(repo_index)"` Repo *repo_model.Repository `xorm:"-"` OwnerID int64 `xorm:"index"` WorkflowID string `xorm:"index"` // the name of workflow file @@ -50,15 +50,20 @@ type ActionRun struct { Status Status `xorm:"index"` Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed RawConcurrency string // raw concurrency - ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` - ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` - // Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0 + + // Started and Stopped are identical to the latest attempt after ActionRunAttempt was introduced. + // When a rerun creates a new latest attempt, they are reset until the new attempt starts and stops. Started timeutil.TimeStamp Stopped timeutil.TimeStamp - // PreviousDuration is used for recording previous duration + + // PreviousDuration is kept only for legacy runs created before ActionRunAttempt existed. + // New runs and reruns no longer update this field and use attempt-scoped durations instead. PreviousDuration time.Duration - Created timeutil.TimeStamp `xorm:"created"` - Updated timeutil.TimeStamp `xorm:"updated"` + + LatestAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"` + + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` } func init() { @@ -66,11 +71,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 +121,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 +129,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 +154,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) { @@ -322,16 +352,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 @@ -402,14 +432,11 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { type ActionRunIndex db.ResourceIndex -func GetConcurrentRunsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRun, []*ActionRunJob, error) { - runs, err := db.Find[ActionRun](ctx, &FindRunOptions{ - RepoID: repoID, - ConcurrencyGroup: concurrencyGroup, - Status: status, - }) +// GetConcurrentRunAttemptsAndJobs returns run attempts and jobs in the same concurrency group by statuses. +func GetConcurrentRunAttemptsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRunAttempt, []*ActionRunJob, error) { + attempts, err := FindConcurrentRunAttempts(ctx, repoID, concurrencyGroup, status) if err != nil { - return nil, nil, fmt.Errorf("find runs: %w", err) + return nil, nil, fmt.Errorf("find run attempts: %w", err) } jobs, err := db.Find[ActionRunJob](ctx, &FindRunJobOptions{ @@ -421,36 +448,34 @@ func GetConcurrentRunsAndJobs(ctx context.Context, repoID int64, concurrencyGrou return nil, nil, fmt.Errorf("find jobs: %w", err) } - return runs, jobs, nil + return attempts, jobs, nil } -func CancelPreviousJobsByRunConcurrency(ctx context.Context, actionRun *ActionRun) ([]*ActionRunJob, error) { - if actionRun.ConcurrencyGroup == "" { +func CancelPreviousJobsByRunConcurrency(ctx context.Context, attempt *ActionRunAttempt) ([]*ActionRunJob, error) { + if attempt.ConcurrencyGroup == "" { return nil, nil } var jobsToCancel []*ActionRunJob statusFindOption := []Status{StatusWaiting, StatusBlocked} - if actionRun.ConcurrencyCancel { + if attempt.ConcurrencyCancel { statusFindOption = append(statusFindOption, StatusRunning) } - runs, jobs, err := GetConcurrentRunsAndJobs(ctx, actionRun.RepoID, actionRun.ConcurrencyGroup, statusFindOption) + attempts, jobs, err := GetConcurrentRunAttemptsAndJobs(ctx, attempt.RepoID, attempt.ConcurrencyGroup, statusFindOption) if err != nil { return nil, fmt.Errorf("find concurrent runs and jobs: %w", err) } jobsToCancel = append(jobsToCancel, jobs...) // cancel runs in the same concurrency group - for _, run := range runs { - if run.ID == actionRun.ID { + for _, concurrentAttempt := range attempts { + if concurrentAttempt.RunID == attempt.RunID { continue } - jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{ - RunID: run.ID, - }) + jobs, err := GetRunJobsByRunAndAttemptID(ctx, concurrentAttempt.RunID, concurrentAttempt.ID) if err != nil { - return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err) + return nil, fmt.Errorf("find run %d attempt %d jobs: %w", concurrentAttempt.RunID, concurrentAttempt.ID, err) } jobsToCancel = append(jobsToCancel, jobs...) } diff --git a/models/actions/run_attempt.go b/models/actions/run_attempt.go new file mode 100644 index 00000000000..857247b0689 --- /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 00000000000..77a5b8f15c9 --- /dev/null +++ b/models/actions/run_attempt_list.go @@ -0,0 +1,46 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" +) + +type ActionRunAttemptList []*ActionRunAttempt + +// GetUserIDs returns a slice of user's id +func (attempts ActionRunAttemptList) GetUserIDs() []int64 { + return container.FilterSlice(attempts, func(attempt *ActionRunAttempt) (int64, bool) { + return attempt.TriggerUserID, true + }) +} + +func (attempts ActionRunAttemptList) LoadTriggerUser(ctx context.Context) error { + userIDs := attempts.GetUserIDs() + users := make(map[int64]*user_model.User, len(userIDs)) + if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil { + return err + } + for _, attempt := range attempts { + if attempt.TriggerUserID == user_model.ActionsUserID { + attempt.TriggerUser = user_model.NewActionsUser() + } else { + attempt.TriggerUser = users[attempt.TriggerUserID] + if attempt.TriggerUser == nil { + attempt.TriggerUser = user_model.NewGhostUser() + } + } + } + return nil +} + +// ListRunAttemptsByRunID returns all attempts of a run, ordered by attempt number DESC (newest first). +func ListRunAttemptsByRunID(ctx context.Context, runID int64) (ActionRunAttemptList, error) { + var attempts ActionRunAttemptList + return attempts, db.GetEngine(ctx).Where("run_id=?", runID).OrderBy("attempt DESC").Find(&attempts) +} diff --git a/models/actions/run_job.go b/models/actions/run_job.go index c752e61b7d7..f0d41ef4b47 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 @@ -51,6 +62,19 @@ type ActionRunJob struct { ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` // evaluated concurrency.group ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` // evaluated concurrency.cancel-in-progress + // TokenPermissions stores the explicit permissions from workflow/job YAML (no org/repo clamps applied). + // Org/repo clamps are enforced when the token is used at runtime. + // 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"` @@ -62,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 { @@ -89,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 } @@ -142,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 @@ -186,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) + } } } @@ -259,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) } @@ -267,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 10f76d3641b..db7554593dd 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "xorm.io/builder" @@ -55,8 +56,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,6 +73,7 @@ func (jobs ActionJobList) LoadAttributes(ctx context.Context, withRepo bool) err type FindRunJobOptions struct { db.ListOptions RunID int64 + RunAttemptID optional.Option[int64] // use optional to allow filtering by zero (legacy jobs have run_attempt_id=0) RepoID int64 OwnerID int64 CommitSHA string @@ -83,6 +87,9 @@ func (opts FindRunJobOptions) ToConds() builder.Cond { if opts.RunID > 0 { cond = cond.And(builder.Eq{"`action_run_job`.run_id": opts.RunID}) } + if opts.RunAttemptID.Has() { + cond = cond.And(builder.Eq{"`action_run_job`.run_attempt_id": opts.RunAttemptID.Value()}) + } if opts.RepoID > 0 { cond = cond.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID}) } diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 2628c4712f5..0a0840648da 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 bd2b92f4f66..e82cbe84b51 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" ) @@ -30,6 +32,16 @@ func TestUpdateRepoRunsNumbers(t *testing.T) { err = UpdateRepoRunsNumbers(t.Context(), repo) assert.NoError(t, err) 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 f5d40ca7d66..f0088491bb5 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 bbd2af73b65..f7b7c9fdf0b 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 5361b94801a..6b5cae94fe9 100644 --- a/models/actions/schedule_list.go +++ b/models/actions/schedule_list.go @@ -4,62 +4,13 @@ package actions import ( - "context" - "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/container" "xorm.io/builder" ) type ScheduleList []*ActionSchedule -// GetUserIDs returns a slice of user's id -func (schedules ScheduleList) GetUserIDs() []int64 { - return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) { - return schedule.TriggerUserID, true - }) -} - -func (schedules ScheduleList) GetRepoIDs() []int64 { - return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) { - return schedule.RepoID, true - }) -} - -func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error { - userIDs := schedules.GetUserIDs() - users := make(map[int64]*user_model.User, len(userIDs)) - if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil { - return err - } - for _, schedule := range schedules { - if schedule.TriggerUserID == user_model.ActionsUserID { - schedule.TriggerUser = user_model.NewActionsUser() - } else { - schedule.TriggerUser = users[schedule.TriggerUserID] - if schedule.TriggerUser == nil { - schedule.TriggerUser = user_model.NewGhostUser() - } - } - } - return nil -} - -func (schedules ScheduleList) LoadRepos(ctx context.Context) error { - repoIDs := schedules.GetRepoIDs() - repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs) - if err != nil { - return err - } - for _, schedule := range schedules { - schedule.Repo = repos[schedule.RepoID] - } - return nil -} - type FindScheduleOptions struct { db.ListOptions RepoID int64 diff --git a/models/actions/task.go b/models/actions/task.go index e092d6fbbd9..7a97eadc798 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 03ffbf19316..3b477d8483c 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/token_permissions.go b/models/actions/token_permissions.go new file mode 100644 index 00000000000..985f6cc97be --- /dev/null +++ b/models/actions/token_permissions.go @@ -0,0 +1,60 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" +) + +// ComputeTaskTokenPermissions computes the effective permissions for a job token against the target repository. +// It uses the job's stored permissions (if any), then applies org/repo clamps and fork/cross-repo restrictions. +// Note: target repository access policy checks are enforced in GetActionsUserRepoPermission; this function only computes the job token's effective permission ceiling. +func ComputeTaskTokenPermissions(ctx context.Context, task *ActionTask, targetRepo *repo_model.Repository) (ret repo_model.ActionsTokenPermissions, err error) { + if err := task.LoadJob(ctx); err != nil { + return ret, err + } + if err := task.Job.LoadRepo(ctx); err != nil { + return ret, err + } + runRepo := task.Job.Repo + + if err := runRepo.LoadOwner(ctx); err != nil { + return ret, err + } + + repoActionsCfg := runRepo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + ownerActionsCfg, err := GetOwnerActionsConfig(ctx, runRepo.OwnerID) + if err != nil { + return ret, err + } + + var jobDeclaredPerms repo_model.ActionsTokenPermissions + if task.Job.TokenPermissions != nil { + jobDeclaredPerms = *task.Job.TokenPermissions + } else if repoActionsCfg.OverrideOwnerConfig { + jobDeclaredPerms = repoActionsCfg.GetDefaultTokenPermissions() + } else { + jobDeclaredPerms = ownerActionsCfg.GetDefaultTokenPermissions() + } + + var effectivePerms repo_model.ActionsTokenPermissions + if repoActionsCfg.OverrideOwnerConfig { + effectivePerms = repoActionsCfg.ClampPermissions(jobDeclaredPerms) + } else { + effectivePerms = ownerActionsCfg.ClampPermissions(jobDeclaredPerms) + } + + // Cross-repository access and fork pull requests are strictly read-only for security. + // This ensures a "task repo" cannot gain write access to other repositories via CrossRepoAccess settings. + isSameRepo := task.Job.RepoID == targetRepo.ID + restrictCrossRepoAccess := task.IsForkPullRequest || !isSameRepo + if restrictCrossRepoAccess { + effectivePerms = repo_model.ClampActionsTokenPermissions(effectivePerms, repo_model.MakeRestrictedPermissions()) + } + + return effectivePerms, nil +} diff --git a/models/actions/utils.go b/models/actions/utils.go index f6ba661ae30..e5704d0377d 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 98c048d4ef8..2f7e7da3607 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 8e589eda88d..4ffdca842a4 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -186,15 +186,7 @@ func (a *Action) LoadActUser(ctx context.Context) { if a.ActUser != nil { return } - var err error - a.ActUser, err = user_model.GetPossibleUserByID(ctx, a.ActUserID) - if err == nil { - return - } else if user_model.IsErrUserNotExist(err) { - a.ActUser = user_model.NewGhostUser() - } else { - log.Error("GetUserByID(%d): %v", a.ActUserID, err) - } + a.ActUserID, a.ActUser, _ = user_model.GetPossibleUserByID(ctx, a.ActUserID) } func (a *Action) LoadRepo(ctx context.Context) error { diff --git a/models/activities/action_list.go b/models/activities/action_list.go index 29ff2fdf7a2..5b07a8e080a 100644 --- a/models/activities/action_list.go +++ b/models/activities/action_list.go @@ -282,9 +282,3 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err return actions, count, nil } - -func CountUserFeeds(ctx context.Context, userID int64) (int64, error) { - return db.GetEngine(ctx).Where("user_id = ?", userID). - And("is_deleted = ?", false). - Count(&Action{}) -} diff --git a/models/activities/user_heatmap.go b/models/activities/user_heatmap.go index e24d44c5195..2d1635917ef 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/asymkey/error.go b/models/asymkey/error.go index b7656245791..5df7beb8cd7 100644 --- a/models/asymkey/error.go +++ b/models/asymkey/error.go @@ -192,28 +192,6 @@ func (err ErrGPGKeyIDAlreadyUsed) Unwrap() error { return util.ErrAlreadyExist } -// ErrGPGKeyAccessDenied represents a "GPGKeyAccessDenied" kind of Error. -type ErrGPGKeyAccessDenied struct { - UserID int64 - KeyID int64 -} - -// IsErrGPGKeyAccessDenied checks if an error is a ErrGPGKeyAccessDenied. -func IsErrGPGKeyAccessDenied(err error) bool { - _, ok := err.(ErrGPGKeyAccessDenied) - return ok -} - -// Error pretty-prints an error of type ErrGPGKeyAccessDenied. -func (err ErrGPGKeyAccessDenied) Error() string { - return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d]", - err.UserID, err.KeyID) -} - -func (err ErrGPGKeyAccessDenied) Unwrap() error { - return util.ErrPermissionDenied -} - // ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error. type ErrKeyAccessDenied struct { UserID int64 diff --git a/models/asymkey/ssh_key.go b/models/asymkey/ssh_key.go index 98784b36bd3..1873c30859d 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 4ab84eabcf6..ea3d93e8c81 100644 --- a/models/asymkey/ssh_key_deploy.go +++ b/models/asymkey/ssh_key_deploy.go @@ -105,14 +105,6 @@ func addDeployKey(ctx context.Context, keyID, repoID int64, name, fingerprint st return key, db.Insert(ctx, key) } -// HasDeployKey returns true if public key is a deploy key of given repository. -func HasDeployKey(ctx context.Context, keyID, repoID int64) bool { - has, _ := db.GetEngine(ctx). - Where("key_id = ? AND repo_id = ?", keyID, repoID). - Get(new(DeployKey)) - return has -} - // AddDeployKey add new deploy key to database and authorized_keys file. func AddDeployKey(ctx context.Context, repoID int64, name, content string, readOnly bool) (*DeployKey, error) { fingerprint, err := CalcFingerprint(content) diff --git a/models/auth/access_token.go b/models/auth/access_token.go index 63331b48412..7578528be87 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 e2bb72b722a..d5a5e2af8e8 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -185,10 +185,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 +217,7 @@ func (app *OAuth2Application) GetGrantByUserID(ctx context.Context, userID int64 return grant, nil } -// CreateGrant generates a grant for an user +// CreateGrant generates a grant for a user func (app *OAuth2Application) CreateGrant(ctx context.Context, userID int64, scope string) (*OAuth2Grant, error) { grant := &OAuth2Grant{ ApplicationID: app.ID, @@ -464,7 +461,7 @@ func GetOAuth2AuthorizationByCode(ctx context.Context, code string) (auth *OAuth ////////////////////////////////////////////////////// -// OAuth2Grant represents the permission of an user for a specific application to access resources +// OAuth2Grant represents the permission of a user for a specific application to access resources type OAuth2Grant struct { ID int64 `xorm:"pk autoincr"` UserID int64 `xorm:"INDEX unique(user_application)"` @@ -484,10 +481,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) @@ -633,7 +627,7 @@ func GetActiveOAuth2SourceByAuthName(ctx context.Context, name string) (*Source, } if !has { - return nil, fmt.Errorf("oauth2 source not found, name: %q", name) + return nil, util.NewNotExistErrorf("oauth2 source not found, name: %q", name) } return authSource, nil diff --git a/models/auth/oauth2_test.go b/models/auth/oauth2_test.go index 88ae065652c..d72e1cb1d52 100644 --- a/models/auth/oauth2_test.go +++ b/models/auth/oauth2_test.go @@ -12,19 +12,30 @@ import ( "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestOAuth2AuthorizationCodeValidity(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func TestOAuth2AuthorizationCode(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) t.Run("GenerateSetsValidUntil", func(t *testing.T) { grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1}) expectedValidUntil := timeutil.TimeStamp(time.Now().Unix() + 600) code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, expectedValidUntil, code.ValidUntil) assert.False(t, code.IsExpired()) + assert.Equal(t, int64(1), code.ID) + + code2, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), code.Code) + require.NoError(t, err) + assert.Equal(t, code.Code, code2.Code) + assert.NoError(t, code.Invalidate(t.Context())) + + code, err = auth_model.GetOAuth2AuthorizationByCode(t.Context(), "does not exist") + require.NoError(t, err) + require.Nil(t, code) }) t.Run("Expired", func(t *testing.T) { @@ -34,13 +45,14 @@ func TestOAuth2AuthorizationCodeValidity(t *testing.T) { assert.True(t, code.IsExpired()) }) - t.Run("InvalidateTwice", func(t *testing.T) { - code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode") - assert.NoError(t, err) - if assert.NotNil(t, code) { - assert.NoError(t, code.Invalidate(t.Context())) - assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated) - } + t.Run("Invalidate", func(t *testing.T) { + grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1}) + code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "") + require.NoError(t, err) + require.NotNil(t, code) + require.NoError(t, code.Invalidate(t.Context())) + unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: code.Code}) + assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated) }) } @@ -224,19 +236,6 @@ func TestRevokeOAuth2Grant(t *testing.T) { //////////////////// Authorization Code -func TestGetOAuth2AuthorizationByCode(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode") - assert.NoError(t, err) - assert.NotNil(t, code) - assert.Equal(t, "authcode", code.Code) - assert.Equal(t, int64(1), code.ID) - - code, err = auth_model.GetOAuth2AuthorizationByCode(t.Context(), "does not exist") - assert.NoError(t, err) - assert.Nil(t, code) -} - func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) { // test plain code := &auth_model.OAuth2AuthorizationCode{ @@ -284,13 +283,6 @@ func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) { assert.Equal(t, "https://example.com/callback?code=thecode", redirect.String()) } -func TestOAuth2AuthorizationCode_Invalidate(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - code := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"}) - assert.NoError(t, code.Invalidate(t.Context())) - unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"}) -} - func TestOAuth2AuthorizationCode_TableName(t *testing.T) { assert.Equal(t, "oauth2_authorization_code", new(auth_model.OAuth2AuthorizationCode).TableName()) } diff --git a/models/auth/source.go b/models/auth/source.go index c0b262f870a..16f36eb0c24 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -12,6 +12,7 @@ import ( "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/timeutil" "code.gitea.io/gitea/modules/util" @@ -99,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) @@ -139,7 +140,10 @@ func init() { // BeforeSet is invoked from XORM before setting the value of a field of this object. func (source *Source) BeforeSet(colName string, val xorm.Cell) { if colName == "type" { - typ := Type(db.Cell2Int64(val)) + typ, _, err := db.CellToInt(val, NoType) + if err != nil { + setting.PanicInDevOrTesting("Unable to convert login source (id=%d) type: %v", source.ID, err) + } constructor, ok := registeredConfigs[typ] if !ok { return diff --git a/models/auth/source_test.go b/models/auth/source_test.go index ebc462c5811..fbd663a64fa 100644 --- a/models/auth/source_test.go +++ b/models/auth/source_test.go @@ -17,13 +17,9 @@ import ( ) type TestSource struct { - auth_model.ConfigBase + auth_model.ConfigBase `json:"-"` - Provider string - ClientID string - ClientSecret string - OpenIDConnectAutoDiscoveryURL string - IconURL string + TestField string } // FromDB fills up a LDAPConfig from serialized format. @@ -37,27 +33,23 @@ func (source *TestSource) ToDB() ([]byte, error) { } func TestDumpAuthSource(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + require.NoError(t, unittest.PrepareTestDatabase()) authSourceSchema, err := unittest.GetXORMEngine().TableInfo(new(auth_model.Source)) - assert.NoError(t, err) + require.NoError(t, err) auth_model.RegisterTypeConfig(auth_model.OAuth2, new(TestSource)) + source := &auth_model.Source{ + Type: auth_model.OAuth2, + Name: "TestSource", + Cfg: &TestSource{TestField: "TestValue"}, + } + require.NoError(t, auth_model.CreateSource(t.Context(), source)) - auth_model.CreateSource(t.Context(), &auth_model.Source{ - Type: auth_model.OAuth2, - Name: "TestSource", - IsActive: false, - Cfg: &TestSource{ - Provider: "ConvertibleSourceName", - ClientID: "42", - }, - }) - - sb := new(strings.Builder) - - // TODO: this test is quite hacky, it should use a low-level "select" (without model processors) but not a database dump - engine := unittest.GetXORMEngine() - require.NoError(t, engine.DumpTables([]*schemas.Table{authSourceSchema}, sb)) - assert.Contains(t, sb.String(), `"Provider":"ConvertibleSourceName"`) + // intentionally test the "dump" to make sure the dumped JSON is correct: https://github.com/go-gitea/gitea/pull/16847 + sb := &strings.Builder{} + require.NoError(t, unittest.GetXORMEngine().DumpTables([]*schemas.Table{authSourceSchema}, sb)) + // the dumped SQL is something like: + // INSERT INTO `login_source` (`id`, `type`, `name`, `is_active`, `is_sync_enabled`, `two_factor_policy`, `cfg`, `created_unix`, `updated_unix`) VALUES (1,6,'TestSource',0,0,'','{"TestField":"TestValue"}',1774179784,1774179784); + assert.Contains(t, sb.String(), `'{"TestField":"TestValue"}'`) } diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go index 4263495650f..80c34ba6adb 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 6d8b5429579..7bd79ed3f52 100644 --- a/models/auth/webauthn.go +++ b/models/auth/webauthn.go @@ -200,13 +200,3 @@ func DeleteCredential(ctx context.Context, id, userID int64) (bool, error) { had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{}) return had > 0, err } - -// WebAuthnCredentials implements the webauthn.User interface -func WebAuthnCredentials(ctx context.Context, userID int64) ([]webauthn.Credential, error) { - dbCreds, err := GetWebAuthnCredentialsByUID(ctx, userID) - if err != nil { - return nil, err - } - - return dbCreds.ToCredentials(), nil -} diff --git a/models/db/conn.go b/models/db/conn.go new file mode 100644 index 00000000000..944b6e7b970 --- /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 a742d54f8cb..ba33d252f2b 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 8bb14f1389b..5e74f458ec7 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/convert.go b/models/db/convert.go index 80b0f7b04b4..374dbfd8a82 100644 --- a/models/db/convert.go +++ b/models/db/convert.go @@ -5,12 +5,11 @@ package db import ( "fmt" - "strconv" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "xorm.io/xorm" + "xorm.io/xorm/convert" "xorm.io/xorm/schemas" ) @@ -74,15 +73,14 @@ WHERE ST.name ='varchar'`) return err } -// Cell2Int64 converts a xorm.Cell type to int64, -// and handles possible irregular cases. -func Cell2Int64(val xorm.Cell) int64 { - switch (*val).(type) { - case []uint8: - log.Trace("Cell2Int64 ([]uint8): %v", *val) - - v, _ := strconv.ParseInt(string((*val).([]uint8)), 10, 64) - return v +// CellToInt converts a xorm.Cell field value to an int value +func CellToInt[T ~int | int64](cell xorm.Cell, def T) (ret T, has bool, err error) { + if *cell == nil { + return def, false, nil } - return (*val).(int64) + val, err := convert.AsInt64(*cell) + if err != nil { + return def, false, err + } + return T(val), true, err } 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 812fe4a6a61..b6735007631 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 00000000000..1fe2e35cba6 --- /dev/null +++ b/models/db/driver_sqlite_mattn.go @@ -0,0 +1,31 @@ +//go:build sqlite_mattn && sqlite_unlock_notify + +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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 00000000000..8202d5f6f49 --- /dev/null +++ b/models/db/driver_sqlite_modernc.go @@ -0,0 +1,41 @@ +//go:build !sqlite_mattn + +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// 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 b08799210e8..fbcc3fa15e8 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 63f2d4e093c..1d8d555b448 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 8709a2c2a13..8d8ed3992cf 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 ef5db3ff5ea..65192d3327a 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 1c218df77f3..6a6264b5353 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 596046e9502..c0aa06c86d2 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 0744255f664..d85d785da54 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 af9ce93ba5c..32f2ae87642 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 ee8ef0d5cec..5fcc70aa536 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 ac5e8303c35..1df02e48090 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -89,7 +89,7 @@ ref: "refs/heads/test" commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" event: "push" - trigger_event: "push" + trigger_event: "schedule" is_fork_pull_request: 0 status: 1 started: 1683636528 @@ -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 04799b73ca0..2a4e64285f4 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 ecb72140065..110ff627a2a 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 6520b7f6fbe..3af8a28c9c1 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 e1bc588dc59..13efe378c43 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 314e9f7115b..741b193b13d 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 570d4a27daf..0870895a715 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 new file mode 100644 index 00000000000..72550be79aa --- /dev/null +++ b/models/fixtures/badge.yml @@ -0,0 +1,7 @@ +- + id: 1 + 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 a17999091e0..e09022a6142 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 4c3ac367f6b..2de34488090 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 8fde386e226..6930dbb58e7 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 87c652e53ab..df3bc425057 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 f63343b042a..9157911bac2 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 ca780a73aa0..0d1b2f00980 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 0f6bd9ee6df..b3c78120af1 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 ca780a73aa0..0d1b2f00980 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 b8d35828bf1..f8de0e039dc 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 2d54313fdf1..3d2895dc1ca 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 ca780a73aa0..0d1b2f00980 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 6023719b1ee..01918b35eeb 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 ca5b1c6cd1d..6da3c9e279a 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 c40ecad6764..a0bf422dc95 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 5aabc08e388..51100684471 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 f4ecb1f9232..3754bd78286 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 14b7a72d847..dc3d1c60d99 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 64824316ea2..756cb7be4b4 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 4bc3ff1b8b9..edc1041abc3 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 acfac749686..064f790a771 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 ae5ae565425..0fe430f147b 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 ca780a73aa0..0d1b2f00980 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 87c30cc96c4..c4ed2aea780 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 97bc4ae60dd..1f690654cb3 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 af08f07bfa1..17e26d7634f 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 bd279d4bb28..dcfbeada39d 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 2f38cb58b61..3426ccffac4 100644 --- a/models/fixtures/oauth2_application.yml +++ b/models/fixtures/oauth2_application.yml @@ -4,7 +4,7 @@ name: "Test" client_id: "da7da3ba-9a13-4167-856f-3899de0b0138" client_secret: "$2a$10$UYRgUSgekzBp6hYe8pAdc.cgB4Gn06QRKsORUnIYTYQADs.YR/uvi" # bcrypt of "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA= - redirect_uris: '["a", "https://example.com/xyzzy"]' + redirect_uris: '["https://example.com"]' created_unix: 1546869730 updated_unix: 1546869730 confidential_client: true @@ -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 d29502164e6..01918b35eeb 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 e63286878b2..54f4e45e62e 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 73a3e9dba9b..dc35701182a 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 44d87bce046..e61781fd7f1 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 3293dea6edf..91d21981714 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 b1af05908aa..7d9d5118820 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 ca780a73aa0..0d1b2f00980 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 1944e7bd84c..cb83439645e 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 856b0e3fb29..756bca86b61 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 9a16316e5a2..b8da7fe0812 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 ee571a73a48..9effcc98f7c 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 372a79509f0..be44e331ec0 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 efa5130a2b9..9055080ff85 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 ca780a73aa0..0d1b2f00980 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 ca780a73aa0..0d1b2f00980 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 ca780a73aa0..0d1b2f00980 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 8850c8d780b..60459d638d6 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 f166faccc1d..3a4e7edaa96 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 b12e6b207f4..0a26eaec8ee 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 4c3e37500f0..69f083ccd75 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 dfa514db37f..d8eb7962072 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 5b8bbceca9e..abcc9d3bb27 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 39b51b3736f..96db493448b 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 b7919d6fbbd..bbb3852069c 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 dcad176c899..ae612fa9f93 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 b549d0589bc..3c2cb7802a3 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 a29078107ee..c91f74467a8 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 110019eee30..bb950870094 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 6b2d153278a..4cceffee6a9 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 055addf510e..97ac821fc11 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 768af38d9e2..7c2145a6d86 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 d8cb85274b6..13b421b7b49 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 976a236011c..1a33947e047 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 2ec9d99df52..c1714e40c82 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 d3a367b99df..72fe7e34b8c 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 c668cb6c3b7..1b0f7a94942 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 18bcd2ed2b4..b7ee121f24d 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 bc43127fcd4..d188a4d76a8 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 ec282914b81..01918b35eeb 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 698c43f1472..db53ca0ac7e 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 25e84526d29..1445f3a5a02 100644 --- a/models/git/branch_list.go +++ b/models/git/branch_list.go @@ -7,7 +7,6 @@ import ( "context" "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/optional" @@ -60,24 +59,6 @@ func (branches BranchList) LoadPusher(ctx context.Context) error { return nil } -func (branches BranchList) LoadRepo(ctx context.Context) error { - ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) { - return branch.RepoID, branch.RepoID > 0 && branch.Repo == nil - }) - - reposMap := make(map[int64]*repo_model.Repository, len(ids)) - if err := db.GetEngine(ctx).In("id", ids).Find(&reposMap); err != nil { - return err - } - for _, branch := range branches { - if branch.RepoID <= 0 || branch.Repo != nil { - continue - } - branch.Repo = reposMap[branch.RepoID] - } - return nil -} - type FindBranchOptions struct { db.ListOptions RepoID int64 diff --git a/models/git/branch_test.go b/models/git/branch_test.go index 3832df93509..be674392852 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 f565550c539..f18d3470b02 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 f4567a0aac9..f242f94f7b7 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -495,9 +495,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/issues/comment.go b/models/issues/comment.go index 25e74c01eab..acfc07ff220 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 0eaa47e3593..db8054e1618 100644 --- a/models/issues/dependency.go +++ b/models/issues/dependency.go @@ -89,12 +89,6 @@ type ErrUnknownDependencyType struct { Type DependencyType } -// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType -func IsErrUnknownDependencyType(err error) bool { - _, ok := err.(ErrUnknownDependencyType) - return ok -} - func (err ErrUnknownDependencyType) Error() string { return fmt.Sprintf("unknown dependency type [type: %d]", err.Type) } diff --git a/models/issues/issue.go b/models/issues/issue.go index 655cdebdfc6..345b36a82fa 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 151469a9b8b..697448cf5e5 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 26b93189b8b..da407094a74 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 e9dc412331d..842249bad23 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 ae6195b05dd..753c96ed180 100644 --- a/models/issues/issue_pin.go +++ b/models/issues/issue_pin.go @@ -165,27 +165,6 @@ func MovePin(ctx context.Context, issue *Issue, newPosition int) error { }) } -func GetPinnedIssueIDs(ctx context.Context, repoID int64, isPull bool) ([]int64, error) { - var issuePins []IssuePin - if err := db.GetEngine(ctx). - Table("issue_pin"). - Where("repo_id = ?", repoID). - And("is_pull = ?", isPull). - Find(&issuePins); err != nil { - return nil, err - } - - sort.Slice(issuePins, func(i, j int) bool { - return issuePins[i].PinOrder < issuePins[j].PinOrder - }) - - var ids []int64 - for _, pin := range issuePins { - ids = append(ids, pin.IssueID) - } - return ids, nil -} - func GetIssuePinsByRepoID(ctx context.Context, repoID int64, isPull bool) ([]*IssuePin, error) { var pins []*IssuePin if err := db.GetEngine(ctx).Where("repo_id = ? AND is_pull = ?", repoID, isPull).Find(&pins); err != nil { diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 01852447834..18f0f91c381 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 00000000000..b004f994b17 --- /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 049dcc7de8a..f905e629e38 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 adedaa3d3a0..ddf6613870e 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 55a90f50a19..99a09de0ec5 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 0a320ffc56f..c58a2f319dd 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -93,12 +93,6 @@ type ErrIssueIsOpen struct { Index int64 } -// IsErrIssueIsOpen checks if an error is a ErrIssueIsOpen. -func IsErrIssueIsOpen(err error) bool { - _, ok := err.(ErrIssueIsOpen) - return ok -} - func (err ErrIssueIsOpen) Error() string { return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already open", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index) } @@ -441,7 +435,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la LabelIDs: labelIDs, Attachments: uuids, }); err != nil { - if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { return err } return fmt.Errorf("newIssue: %w", err) @@ -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 560be17eb62..f384e086e56 100644 --- a/models/issues/issue_watch.go +++ b/models/issues/issue_watch.go @@ -67,7 +67,7 @@ func GetIssueWatch(ctx context.Context, userID, issueID int64) (iw *IssueWatch, return iw, exists, err } -// CheckIssueWatch check if an user is watching an issue +// CheckIssueWatch check if a user is watching an issue // it takes participants and repo watch into account func CheckIssueWatch(ctx context.Context, user *user_model.User, issue *Issue) (bool, error) { iw, exist, err := GetIssueWatch(ctx, user.ID, issue.ID) diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go index f8495929cf9..41a867c892a 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 9f180f9ac95..4c8d02a990e 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "regexp" "strings" "code.gitea.io/gitea/models/db" @@ -24,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + "github.com/dlclark/regexp2" "xorm.io/builder" ) @@ -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) @@ -861,7 +861,7 @@ func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRul } type CodeOwnerRule struct { - Rule *regexp.Regexp + Rule *regexp2.Regexp // it supports negative lookahead, does better for end users Negative bool Users []*user_model.User Teams []*org_model.Team @@ -877,7 +877,13 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, warnings := make([]string, 0) - rule.Rule, err = regexp.Compile(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)) return nil, warnings diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index 19d727ecbdb..6a6abca9709 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 437830701c6..302b2ca0ba7 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 25b27cbe9c9..79d1f8aa9b4 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 22e2e186d91..78ef0d20c23 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -67,7 +67,7 @@ func (err ErrNotValidReviewRequest) Unwrap() error { return util.ErrInvalidArgument } -// ErrReviewRequestOnClosedPR represents an error when an user tries to request a re-review on a closed or merged PR. +// ErrReviewRequestOnClosedPR represents an error when a user tries to request a re-review on a closed or merged PR. type ErrReviewRequestOnClosedPR struct{} // IsErrReviewRequestOnClosedPR checks if an error is an ErrReviewRequestOnClosedPR. @@ -176,15 +176,7 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) { if r.ReviewerID == 0 || r.Reviewer != nil { return err } - r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID) - if err != nil { - if !user_model.IsErrUserNotExist(err) { - return fmt.Errorf("GetPossibleUserByID [%d]: %w", r.ReviewerID, err) - } - r.ReviewerID = user_model.GhostUserID - r.Reviewer = user_model.NewGhostUser() - return nil - } + r.ReviewerID, r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID) return err } @@ -908,8 +900,8 @@ func MarkConversation(ctx context.Context, comment *Comment, doer *user_model.Us // CanMarkConversation Add or remove Conversation mark for a code comment permission check // the PR writer , official reviewer and poster can do it func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) { - if doer == nil || issue == nil { - return false, errors.New("issue or doer is nil") + if doer == nil { + return false, nil } if err = issue.LoadRepo(ctx); err != nil { @@ -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.go b/models/migrations/base/db.go index 3b8f0e00a04..16048c2073b 100644 --- a/models/migrations/base/db.go +++ b/models/migrations/base/db.go @@ -402,7 +402,7 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin cols += "DROP COLUMN `" + col + "` CASCADE" } if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil { - return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err) + return fmt.Errorf("drop table `%s` columns %v: %w", tableName, columnNames, err) } case setting.Database.Type.IsMySQL(): // Drop indexes on columns first @@ -430,7 +430,7 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin cols += "DROP COLUMN `" + col + "`" } if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil { - return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err) + return fmt.Errorf("drop table `%s` columns %v: %w", tableName, columnNames, err) } case setting.Database.Type.IsMSSQL(): cols := "" @@ -444,27 +444,27 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin tableName, strings.ReplaceAll(cols, "`", "'")) constraints := make([]string, 0) if err := sess.SQL(sql).Find(&constraints); err != nil { - return fmt.Errorf("Find constraints: %v", err) + return fmt.Errorf("find constraints: %w", err) } for _, constraint := range constraints { if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` DROP CONSTRAINT `%s`", tableName, constraint)); err != nil { - return fmt.Errorf("Drop table `%s` default constraint `%s`: %v", tableName, constraint, err) + return fmt.Errorf("drop table `%s` default constraint `%s`: %w", tableName, constraint, err) } } sql = fmt.Sprintf("SELECT DISTINCT Name FROM sys.indexes INNER JOIN sys.index_columns ON indexes.index_id = index_columns.index_id AND indexes.object_id = index_columns.object_id WHERE indexes.object_id = OBJECT_ID('%[1]s') AND index_columns.column_id IN (SELECT column_id FROM sys.columns WHERE LOWER(name) IN (%[2]s) AND object_id = OBJECT_ID('%[1]s'))", tableName, strings.ReplaceAll(cols, "`", "'")) constraints = make([]string, 0) if err := sess.SQL(sql).Find(&constraints); err != nil { - return fmt.Errorf("Find constraints: %v", err) + return fmt.Errorf("find constraints: %w", err) } for _, constraint := range constraints { if _, err := sess.Exec(fmt.Sprintf("DROP INDEX `%[2]s` ON `%[1]s`", tableName, constraint)); err != nil { - return fmt.Errorf("Drop index `%[2]s` on `%[1]s`: %v", tableName, constraint, err) + return fmt.Errorf("drop index `%[2]s` on `%[1]s`: %[3]w", tableName, constraint, err) } } if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` DROP COLUMN %s", tableName, cols)); err != nil { - return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err) + return fmt.Errorf("drop table `%s` columns %v: %w", tableName, columnNames, err) } default: log.Fatal("Unrecognized DB") diff --git a/models/migrations/base/db_test.go b/models/migrations/base/db_test.go index 00635ca72e2..ce6e1169d83 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 17ea951b5a6..00000000000 --- 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 342adb2a049..49f71ce3fc2 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 4f90a4495ca..addf5e06820 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 ceff4c99931..6be1c7ca48e 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 580b2a4f047..3dd846bb36e 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 86cfb926e44..46162e78033 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 be6933f1560..83f42821fb4 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,9 +401,15 @@ 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 database-backed user session tracking", v1_26.AddUserSessionTable), + 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 database-backed user session tracking", v1_27.AddUserSessionTable), } return preparedMigrations } diff --git a/models/migrations/migrationtest/tests.go b/models/migrations/migrationtest/tests.go new file mode 100644 index 00000000000..e0f7d04bb06 --- /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 978f88577c3..6ed240c4075 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 5c1db4db71c..aa57b5ad1d9 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 263f69f3380..a86fb98830c 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 d01585e9978..768bbd310b0 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 73b5c1f3d6c..e230c684eab 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 5fc6a0c467e..c0a1378534c 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 7f93d6e9e5e..c54424788d6 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 fb56ac8e116..44424dd3691 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 2e827f0550b..f68dd6d92d8 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 946e06e3997..bbfa5e162a8 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 3b4ac7aa4b1..7bff2572e11 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 571a4f55a34..86522018719 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 a2dc0fae554..6fda9b9980e 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 ebcfb45a941..b8641526f3d 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 5722dd35574..638983ad0ba 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 25b2f6525da..e5e28ea63fc 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 87e807be6e1..784ca0e46ed 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 9eb6d405099..44ced874b32 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 7436ff7483c..3f7900c58f0 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 2fd63a7118e..3ceb9a3c66d 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 0bf63719e5e..3864eecb78a 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 536a7ade088..daf98d40f4c 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 ac8facd6aa0..e02c8a53282 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 743f860466f..8e4c9410bd7 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 b4a50f6fcb4..1bd7fac2f18 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 2b42a33c389..21946a662ab 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 c7b643c7e08..bc3a33055cc 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 1cf03d61201..a711b5ec5f0 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 f7b2caed83d..ffccac0fd3d 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 b008b6fc03d..1832adf39ab 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 c3dfa5e7e7e..9af84cd257f 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 d2c4a4105d3..33c981edb97 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 3ef2c68aa33..0749a20e20e 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 78d890704c5..19646140351 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 5aa12d553c9..0b271b9bbcf 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 d4a66fee816..3fd658e01b9 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 1ec0af76a01..dcf548bec0e 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 ddc26401606..a0225eb774c 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 971707be4f9..98e948cf05c 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/v328.go b/models/migrations/v1_26/v328.go index 2eb6770b5c6..81047305289 100644 --- a/models/migrations/v1_26/v328.go +++ b/models/migrations/v1_26/v328.go @@ -3,25 +3,14 @@ package v1_26 -import "xorm.io/xorm" +import ( + "xorm.io/xorm" +) -func AddUserSessionTable(x *xorm.Engine) error { - type UserSession struct { - ID string `xorm:"pk VARCHAR(64)"` - UserID int64 `xorm:"INDEX NOT NULL"` - LoginIP string `xorm:"VARCHAR(45)"` - LastIP string `xorm:"VARCHAR(45)"` - PrevIP string `xorm:"VARCHAR(45)"` - UserAgent string `xorm:"TEXT"` - LoginMethod string `xorm:"VARCHAR(64)"` - AuthTokenID string `xorm:"VARCHAR(64)"` - CreatedUnix int64 `xorm:"INDEX NOT NULL"` - LastAccessUnix int64 `xorm:"INDEX NOT NULL"` - LogoutUnix int64 `xorm:"INDEX NOT NULL DEFAULT 0"` +func AddTokenPermissionsToActionRunJob(x *xorm.Engine) error { + type ActionRunJob struct { + TokenPermissions string `xorm:"JSON TEXT"` } - - _, err := x.SyncWithOptions(xorm.SyncOptions{ - IgnoreDropIndices: true, - }, new(UserSession)) + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionRunJob)) return err } diff --git a/models/migrations/v1_26/v329.go b/models/migrations/v1_26/v329.go new file mode 100644 index 00000000000..1afd155f05e --- /dev/null +++ b/models/migrations/v1_26/v329.go @@ -0,0 +1,62 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "fmt" + + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +type UserBadge struct { //revive:disable-line:exported + ID int64 `xorm:"pk autoincr"` + BadgeID int64 + UserID int64 +} + +// TableIndices implements xorm's TableIndices interface +func (n *UserBadge) TableIndices() []*schemas.Index { + indices := make([]*schemas.Index, 0, 1) + ubUnique := schemas.NewIndex("unique_user_badge", schemas.UniqueType) + ubUnique.AddColumn("user_id", "badge_id") + indices = append(indices, ubUnique) + return indices +} + +// AddUniqueIndexForUserBadge adds a compound unique indexes for user badge table +// and it replaces an old index on user_id +func AddUniqueIndexForUserBadge(x *xorm.Engine) error { + // remove possible duplicated records in table user_badge + type result struct { + UserID int64 + BadgeID int64 + Cnt int + } + var results []result + if err := x.Select("user_id, badge_id, count(*) as cnt"). + Table("user_badge"). + GroupBy("user_id, badge_id"). + Having("count(*) > 1"). + Find(&results); err != nil { + return err + } + for _, r := range results { + if x.Dialect().URI().DBType == schemas.MSSQL { + if _, err := x.Exec(fmt.Sprintf("delete from user_badge where id in (SELECT top %d id FROM user_badge WHERE user_id = ? and badge_id = ?)", r.Cnt-1), r.UserID, r.BadgeID); err != nil { + return err + } + } else { + var ids []int64 + if err := x.SQL("SELECT id FROM user_badge WHERE user_id = ? and badge_id = ? limit ?", r.UserID, r.BadgeID, r.Cnt-1).Find(&ids); err != nil { + return err + } + if _, err := x.Table("user_badge").In("id", ids).Delete(); err != nil { + return err + } + } + } + + return x.Sync(new(UserBadge)) +} diff --git a/models/migrations/v1_26/v329_test.go b/models/migrations/v1_26/v329_test.go new file mode 100644 index 00000000000..e4bebfb71dc --- /dev/null +++ b/models/migrations/v1_26/v329_test.go @@ -0,0 +1,85 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/migrationtest" + + "github.com/stretchr/testify/assert" +) + +type UserBadgeBefore struct { + ID int64 `xorm:"pk autoincr"` + BadgeID int64 + UserID int64 `xorm:"INDEX"` +} + +func (UserBadgeBefore) TableName() string { + return "user_badge" +} + +func Test_AddUniqueIndexForUserBadge(t *testing.T) { + x, deferable := migrationtest.PrepareTestEnv(t, 0, new(UserBadgeBefore)) + defer deferable() + if x == nil || t.Failed() { + return + } + + testData := []*UserBadgeBefore{ + {UserID: 1, BadgeID: 1}, + {UserID: 1, BadgeID: 1}, // duplicate + {UserID: 2, BadgeID: 1}, + {UserID: 1, BadgeID: 2}, + {UserID: 3, BadgeID: 3}, + {UserID: 3, BadgeID: 3}, // duplicate + } + + for _, data := range testData { + _, err := x.Insert(data) + assert.NoError(t, err) + } + + // check that we have duplicates + count, err := x.Where("user_id = ? AND badge_id = ?", 1, 1).Count(&UserBadgeBefore{}) + assert.NoError(t, err) + assert.Equal(t, int64(2), count) + + count, err = x.Where("user_id = ? AND badge_id = ?", 3, 3).Count(&UserBadgeBefore{}) + assert.NoError(t, err) + assert.Equal(t, int64(2), count) + + totalCount, err := x.Count(&UserBadgeBefore{}) + assert.NoError(t, err) + assert.Equal(t, int64(6), totalCount) + + // run the migration + if err := AddUniqueIndexForUserBadge(x); err != nil { + assert.NoError(t, err) + return + } + + // verify the duplicates were removed + count, err = x.Where("user_id = ? AND badge_id = ?", 1, 1).Count(&UserBadgeBefore{}) + assert.NoError(t, err) + assert.Equal(t, int64(1), count) + + count, err = x.Where("user_id = ? AND badge_id = ?", 3, 3).Count(&UserBadgeBefore{}) + assert.NoError(t, err) + assert.Equal(t, int64(1), count) + + // check total count + totalCount, err = x.Count(&UserBadgeBefore{}) + assert.NoError(t, err) + assert.Equal(t, int64(4), totalCount) + + // fail to insert a duplicate + _, err = x.Insert(&UserBadge{UserID: 1, BadgeID: 1}) + assert.Error(t, err) + + // succeed adding a non-duplicate + _, err = x.Insert(&UserBadge{UserID: 4, BadgeID: 1}) + assert.NoError(t, err) +} diff --git a/models/migrations/v1_26/v330.go b/models/migrations/v1_26/v330.go new file mode 100644 index 00000000000..9f77331090b --- /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 00000000000..0c6a6a24401 --- /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 00000000000..204b7b661e9 --- /dev/null +++ b/models/migrations/v1_27/v331.go @@ -0,0 +1,158 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import ( + "context" + "time" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +type actionRunAttempt struct { + ID int64 + RepoID int64 `xorm:"index(repo_concurrency_status)"` + RunID int64 `xorm:"UNIQUE(run_attempt)"` + Attempt int64 `xorm:"UNIQUE(run_attempt)"` + TriggerUserID int64 + ConcurrencyGroup string `xorm:"index(repo_concurrency_status) NOT NULL DEFAULT ''"` + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` + Status int `xorm:"index(repo_concurrency_status)"` + Started timeutil.TimeStamp + Stopped timeutil.TimeStamp + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` +} + +func (actionRunAttempt) TableName() string { + return "action_run_attempt" +} + +type actionArtifact struct { + ID int64 `xorm:"pk autoincr"` + RunID int64 `xorm:"index unique(runid_attempt_name_path)"` + RunAttemptID int64 `xorm:"index unique(runid_attempt_name_path) NOT NULL DEFAULT 0"` + RunnerID int64 + RepoID int64 `xorm:"index"` + OwnerID int64 + CommitSHA string + StoragePath string + FileSize int64 + FileCompressedSize int64 + ContentEncoding string `xorm:"content_encoding"` + ArtifactPath string `xorm:"index unique(runid_attempt_name_path)"` + ArtifactName string `xorm:"index unique(runid_attempt_name_path)"` + Status int `xorm:"index"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` + ExpiredUnix timeutil.TimeStamp `xorm:"index"` +} + +func (actionArtifact) TableName() string { + return "action_artifact" +} + +// actionRun mirrors the post-migration action_run schema. +type actionRun struct { + ID int64 + Title string + RepoID int64 `xorm:"unique(repo_index)"` + OwnerID int64 `xorm:"index"` + WorkflowID string `xorm:"index"` + Index int64 `xorm:"index unique(repo_index)"` + TriggerUserID int64 `xorm:"index"` + ScheduleID int64 + Ref string `xorm:"index"` + CommitSHA string + IsForkPullRequest bool + NeedApproval bool + ApprovedBy int64 `xorm:"index"` + Event string + EventPayload string `xorm:"LONGTEXT"` + TriggerEvent string + Status int `xorm:"index"` + Version int `xorm:"version default 0"` + RawConcurrency string + Started timeutil.TimeStamp + Stopped timeutil.TimeStamp + PreviousDuration time.Duration + LatestAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"` + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` +} + +func (actionRun) TableName() string { + return "action_run" +} + +// AddActionRunAttemptModel adds the ActionRunAttempt table and the supporting ActionRun/ActionRunJob fields. +func AddActionRunAttemptModel(x *xorm.Engine) error { + // add "action_run_attempt" + if _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(actionRunAttempt)); err != nil { + return err + } + + // update "action_run_job" + type ActionRunJob struct { + RunAttemptID int64 `xorm:"index NOT NULL DEFAULT 0"` + AttemptJobID int64 `xorm:"index NOT NULL DEFAULT 0"` + SourceTaskID int64 `xorm:"NOT NULL DEFAULT 0"` + } + if _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(ActionRunJob)); err != nil { + return err + } + + // update "action_artifact": let xorm sync add the new 4-column unique index (runid_attempt_name_path) and drop the old 3-column unique (runid_name_path) + if err := x.Sync(new(actionArtifact)); err != nil { + return err + } + + // update "action_run" + // + // This migration intentionally removes the legacy run-level concurrency columns after + // introducing attempt-level concurrency on action_run_attempt. + // + // Existing values from action_run.concurrency_group / action_run.concurrency_cancel are + // not backfilled into action_run_attempt: + // - the old fields are only meaningful while a run is actively participating in + // concurrency scheduling + // - for completed legacy runs, keeping or backfilling those values has no practical + // effect on future scheduling behavior + // - scanning and backfilling old runs would add significant migration cost for little value + // + // This means the schema change is destructive for those two legacy columns by design. + // + // Let xorm sync add the latest_attempt_id column and drop the now-orphan (repo_id, concurrency_group) index. + if err := x.Sync(new(actionRun)); err != nil { + return err + } + concurrencyColumns := make([]string, 0, 2) + for _, col := range []string{"concurrency_group", "concurrency_cancel"} { + exist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "action_run", col) + if err != nil { + return err + } + if exist { + concurrencyColumns = append(concurrencyColumns, col) + } + } + if len(concurrencyColumns) == 0 { + return nil + } + sess := x.NewSession() + defer sess.Close() + if err := base.DropTableColumns(sess, "action_run", concurrencyColumns...); err != nil { + return err + } + // DropTableColumns rebuilds the table on SQLite, which drops all existing indexes. + // Re-sync to restore the indexes defined on actionRun. + return x.Sync(new(actionRun)) +} diff --git a/models/migrations/v1_27/v331_test.go b/models/migrations/v1_27/v331_test.go new file mode 100644 index 00000000000..2302fee024f --- /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 00000000000..2eb6770b5c6 --- /dev/null +++ b/models/migrations/v1_27/v332.go @@ -0,0 +1,27 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import "xorm.io/xorm" + +func AddUserSessionTable(x *xorm.Engine) error { + type UserSession struct { + ID string `xorm:"pk VARCHAR(64)"` + UserID int64 `xorm:"INDEX NOT NULL"` + LoginIP string `xorm:"VARCHAR(45)"` + LastIP string `xorm:"VARCHAR(45)"` + PrevIP string `xorm:"VARCHAR(45)"` + UserAgent string `xorm:"TEXT"` + LoginMethod string `xorm:"VARCHAR(64)"` + AuthTokenID string `xorm:"VARCHAR(64)"` + CreatedUnix int64 `xorm:"INDEX NOT NULL"` + LastAccessUnix int64 `xorm:"INDEX NOT NULL"` + LogoutUnix int64 `xorm:"INDEX NOT NULL DEFAULT 0"` + } + + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(UserSession)) + return err +} diff --git a/models/migrations/v1_6/v71.go b/models/migrations/v1_6/v71.go index 2b11f57c92f..b4dcd87ebae 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 48e1cd5dc4e..0e95a71f929 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_user.go b/models/organization/org_user.go index 69cd9609446..627c1c2edf0 100644 --- a/models/organization/org_user.go +++ b/models/organization/org_user.go @@ -8,10 +8,7 @@ import ( "fmt" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "xorm.io/builder" @@ -129,49 +126,6 @@ func IsUserOrgOwner(ctx context.Context, users user_model.UserList, orgID int64) return results } -// GetOrgAssignees returns all users that have write access and can be assigned to issues -// of the any repository in the organization. -func GetOrgAssignees(ctx context.Context, orgID int64) (_ []*user_model.User, err error) { - e := db.GetEngine(ctx) - userIDs := make([]int64, 0, 10) - if err = e.Table("access"). - Join("INNER", "repository", "`repository`.id = `access`.repo_id"). - Where("`repository`.owner_id = ? AND `access`.mode >= ?", orgID, perm.AccessModeWrite). - Select("user_id"). - Find(&userIDs); err != nil { - return nil, err - } - - additionalUserIDs := make([]int64, 0, 10) - if err = e.Table("team_user"). - Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). - Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). - Join("INNER", "repository", "`repository`.id = `team_repo`.repo_id"). - Where("`repository`.owner_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))", - orgID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests). - Distinct("`team_user`.uid"). - Select("`team_user`.uid"). - Find(&additionalUserIDs); err != nil { - return nil, err - } - - uniqueUserIDs := make(container.Set[int64]) - uniqueUserIDs.AddMultiple(userIDs...) - uniqueUserIDs.AddMultiple(additionalUserIDs...) - - users := make([]*user_model.User, 0, len(uniqueUserIDs)) - if len(userIDs) > 0 { - if err = e.In("id", uniqueUserIDs.Values()). - Where(builder.Eq{"`user`.is_active": true}). - OrderBy(user_model.GetOrderByName()). - Find(&users); err != nil { - return nil, err - } - } - - return users, nil -} - func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) { if len(users) == 0 { return nil, nil //nolint:nilnil // return nil when there are no users diff --git a/models/organization/team_invite.go b/models/organization/team_invite.go index 17f6c596104..186ae5f6e82 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 0274f9c5ba4..5629cec3661 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 c6ec6b39b2c..b5237c2c587 100644 --- a/models/organization/team_unit.go +++ b/models/organization/team_unit.go @@ -28,19 +28,3 @@ func (t *TeamUnit) Unit() unit.Unit { func getUnitsByTeamID(ctx context.Context, teamID int64) (units []*TeamUnit, err error) { return units, db.GetEngine(ctx).Where("team_id = ?", teamID).Find(&units) } - -// UpdateTeamUnits updates a teams's units -func UpdateTeamUnits(ctx context.Context, team *Team, units []TeamUnit) (err error) { - return db.WithTx(ctx, func(ctx context.Context) error { - if _, err = db.GetEngine(ctx).Where("team_id = ?", team.ID).Delete(new(TeamUnit)); err != nil { - return err - } - - if len(units) > 0 { - if err = db.Insert(ctx, units); err != nil { - return err - } - } - return nil - }) -} diff --git a/models/organization/team_user.go b/models/organization/team_user.go index d6d0a5054dd..d24a4c51263 100644 --- a/models/organization/team_user.go +++ b/models/organization/team_user.go @@ -36,14 +36,6 @@ type SearchMembersOptions struct { TeamID int64 } -func (opts SearchMembersOptions) ToConds() builder.Cond { - cond := builder.NewCond() - if opts.TeamID > 0 { - cond = cond.And(builder.Eq{"": opts.TeamID}) - } - return cond -} - // GetTeamMembers returns all members in given team of organization. func GetTeamMembers(ctx context.Context, opts *SearchMembersOptions) ([]*user_model.User, error) { var members []*user_model.User diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index ea0e0d5e734..2ef27051eea 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 38d1cdcf660..17e5d4eee35 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 4b0e789221b..60a55805a8b 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 bf877485d62..64bd08f0b21 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 acc05d8d5ac..30794ad73c3 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 0a478c03234..3e0e1899ea4 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 6433c4675cf..acc34c434ea 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 15d18b368cb..148a02efa33 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/actions_repo_permission_test.go b/models/perm/access/actions_repo_permission_test.go new file mode 100644 index 00000000000..442f6cf2fc2 --- /dev/null +++ b/models/perm/access/actions_repo_permission_test.go @@ -0,0 +1,155 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package access + +import ( + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + perm_model "code.gitea.io/gitea/models/perm" + 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" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetActionsUserRepoPermission(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + // Use fixtures for repos and users + repo4 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) // Public, Owner 5, has Actions unit + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) // Private, Owner 2, no Actions unit in fixtures + repo15 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 15}) // Private, Owner 2, no Actions unit in fixtures + owner2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + actionsUser := user_model.NewActionsUser() + + // Ensure repo2 and repo15 have Actions units for testing configuration + for _, r := range []*repo_model.Repository{repo2, repo15} { + require.NoError(t, db.Insert(ctx, &repo_model.RepoUnit{ + RepoID: r.ID, + Type: unit.TypeActions, + Config: &repo_model.ActionsConfig{}, + })) + } + + t.Run("SameRepo_Public", func(t *testing.T) { + task47 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47}) + require.Equal(t, repo4.ID, task47.RepoID) + + perm, err := GetActionsUserRepoPermission(ctx, repo4, actionsUser, task47.ID) + require.NoError(t, err) + + // Public repo, bot should have Read access even if not collaborator + assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode) + assert.True(t, perm.CanRead(unit.TypeCode)) + }) + + t.Run("SameRepo_Private", func(t *testing.T) { + // Use Task 53 which is already in Repo 2 (Private) + task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53}) + require.Equal(t, repo2.ID, task53.RepoID) + + perm, err := GetActionsUserRepoPermission(ctx, repo2, actionsUser, task53.ID) + require.NoError(t, err) + + // Private repo, bot has no base access, but gets Write from effective tokens perms (Permissive by default) + assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode) + assert.True(t, perm.CanWrite(unit.TypeCode)) + }) + + t.Run("CrossRepo_Denied_None", func(t *testing.T) { + task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53}) + + // Set owner policy to nil allowed repos (None) + cfg := actions_model.OwnerActionsConfig{} + require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, cfg)) + + perm, err := GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID) + require.NoError(t, err) + + // Should NOT have access to the private repo. + assert.False(t, perm.CanRead(unit.TypeCode)) + }) + + t.Run("ForkPR_NoCrossRepo", func(t *testing.T) { + task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53}) + task53.IsForkPullRequest = true + require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request")) + + // Policy contains repo15 + cfg := actions_model.OwnerActionsConfig{ + AllowedCrossRepoIDs: []int64{repo15.ID}, + } + require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, cfg)) + + perm, err := GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID) + require.NoError(t, err) + + // Fork PR never gets cross-repo access to other private repos + assert.False(t, perm.CanRead(unit.TypeCode)) + }) + + t.Run("Inheritance_And_Clamping", func(t *testing.T) { + task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53}) + task53.IsForkPullRequest = false + require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request")) + + // Owner policy: Restricted mode (Read-only Code) + ownerCfg := actions_model.OwnerActionsConfig{ + TokenPermissionMode: repo_model.ActionsTokenPermissionModeRestricted, + MaxTokenPermissions: &repo_model.ActionsTokenPermissions{ + UnitAccessModes: map[unit.Type]perm_model.AccessMode{ + unit.TypeCode: perm_model.AccessModeRead, + }, + }, + } + require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, ownerCfg)) + + // Repo policy: OverrideOwnerConfig = false (should inherit owner's restricted mode) + repo2ActionsUnit := repo2.MustGetUnit(ctx, unit.TypeActions) + repo2ActionsCfg := repo2ActionsUnit.ActionsConfig() + repo2ActionsCfg.OverrideOwnerConfig = false + require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo2ActionsUnit)) + + perm, err := GetActionsUserRepoPermission(ctx, repo2, actionsUser, task53.ID) + require.NoError(t, err) + + // Should be clamped to Read-only + assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeCode)) + assert.False(t, perm.CanWrite(unit.TypeCode)) + }) + + t.Run("RepoOverride_Clamping", func(t *testing.T) { + task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53}) + + // Owner policy: Permissive (Write access) + ownerCfg := actions_model.OwnerActionsConfig{ + TokenPermissionMode: repo_model.ActionsTokenPermissionModePermissive, + } + require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, ownerCfg)) + + // Repo policy: OverrideOwnerConfig = true, MaxTokenPermissions = Read + repo2ActionsUnit := repo2.MustGetUnit(ctx, unit.TypeActions) + repo2ActionsCfg := repo2ActionsUnit.ActionsConfig() + repo2ActionsCfg.OverrideOwnerConfig = true + repo2ActionsCfg.TokenPermissionMode = repo_model.ActionsTokenPermissionModeRestricted + repo2ActionsCfg.MaxTokenPermissions = &repo_model.ActionsTokenPermissions{ + UnitAccessModes: map[unit.Type]perm_model.AccessMode{ + unit.TypeCode: perm_model.AccessModeRead, + }, + } + require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo2ActionsUnit)) + + perm, err := GetActionsUserRepoPermission(ctx, repo2, actionsUser, task53.ID) + require.NoError(t, err) + + // Should be clamped to Read-only + assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeCode)) + }) +} diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 3235d83203c..21821f17466 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "maps" "slices" "strings" @@ -258,6 +259,23 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) { } } +func checkSameOwnerCrossRepoAccess(ctx context.Context, taskRepo, targetRepo *repo_model.Repository, isForkPR bool) bool { + if isForkPR { + // Fork PRs are never allowed cross-repo access to other private repositories of the owner. + return false + } + if taskRepo.OwnerID != targetRepo.OwnerID { + return false + } + ownerCfg, err := actions_model.GetOwnerActionsConfig(ctx, targetRepo.OwnerID) + if err != nil { + log.Error("GetOwnerActionsConfig: %v", err) + return false + } + + return slices.Contains(ownerCfg.AllowedCrossRepoIDs, targetRepo.ID) +} + // GetActionsUserRepoPermission returns the actions user permissions to the repository func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Repository, actionsUser *user_model.User, taskID int64) (perm Permission, err error) { if actionsUser.ID != user_model.ActionsUserID { @@ -268,42 +286,112 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito return perm, err } - var accessMode perm_model.AccessMode + if err := task.LoadJob(ctx); err != nil { + return perm, err + } + + var taskRepo *repo_model.Repository if task.RepoID != repo.ID { - taskRepo, exist, err := db.GetByID[repo_model.Repository](ctx, task.RepoID) - if err != nil || !exist { + if err := task.Job.LoadRepo(ctx); err != nil { return perm, err } - actionsCfg := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() - if !actionsCfg.IsCollaborativeOwner(taskRepo.OwnerID) || !taskRepo.IsPrivate { - // The task repo can access the current repo only if the task repo is private and - // the owner of the task repo is a collaborative owner of the current repo. - // FIXME should owner's visibility also be considered here? + taskRepo = task.Job.Repo + } else { + taskRepo = repo + } - // check permission like simple user but limit to read-only - perm, err = GetUserRepoPermission(ctx, repo, user_model.NewActionsUser()) + // Compute effective permissions for this task against the target repo + effectivePerms, err := actions_model.ComputeTaskTokenPermissions(ctx, task, repo) + if err != nil { + return perm, err + } + if task.RepoID != repo.ID { + // Cross-repo access must also respect the target repo's permission ceiling. + targetRepoActionsCfg := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + if targetRepoActionsCfg.OverrideOwnerConfig { + effectivePerms = targetRepoActionsCfg.ClampPermissions(effectivePerms) + } else { + targetRepoOwnerActionsCfg, err := actions_model.GetOwnerActionsConfig(ctx, repo.OwnerID) if err != nil { return perm, err } - perm.AccessMode = min(perm.AccessMode, perm_model.AccessModeRead) - return perm, nil + effectivePerms = targetRepoOwnerActionsCfg.ClampPermissions(effectivePerms) } - accessMode = perm_model.AccessModeRead - } else if task.IsForkPullRequest { - accessMode = perm_model.AccessModeRead - } else { - accessMode = perm_model.AccessModeWrite } if err := repo.LoadUnits(ctx); err != nil { return perm, err } - perm.SetUnitsWithDefaultAccessMode(repo.Units, accessMode) + + var maxPerm Permission + + // Set up per-unit access modes based on configured permissions + maxPerm.units = repo.Units + maxPerm.unitsMode = maps.Clone(effectivePerms.UnitAccessModes) + + // 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 := GetIndividualUserRepoPermission(ctx, repo, user_model.NewActionsUser()) + if err != nil { + return perm, err + } + if botPerm.AccessMode >= perm_model.AccessModeRead { + // Public repo allows read access, increase permissions to at least read + // Otherwise you cannot access your own repository if your permissions are set to none but the repository is public + for _, u := range repo.Units { + if botPerm.CanRead(u.Type) { + maxPerm.unitsMode[u.Type] = max(maxPerm.unitsMode[u.Type], perm_model.AccessModeRead) + } + } + } + + if task.RepoID == repo.ID { + return maxPerm, nil + } + + if checkSameOwnerCrossRepoAccess(ctx, taskRepo, repo, task.IsForkPullRequest) { + // Access allowed by owner policy (grants access to private repos). + // Note: maxPerm has already been restricted to Read-Only in ComputeTaskTokenPermissions + // because isSameRepo is false. + return maxPerm, nil + } + + // Fall through to allow public repository read access via botPerm check below + + // Check if the repo is public or the Bot has explicit access + if botPerm.AccessMode >= perm_model.AccessModeRead { + return maxPerm, nil + } + + // Check Collaborative Owner and explicit Bot permissions + // We allow access if: + // 1. It's a collaborative owner relationship + // 2. The Actions Bot user has been explicitly granted access and repository is private + // 3. The repository is public (handled by botPerm above) + + if taskRepo.IsPrivate { + actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions) + if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) { + return maxPerm, nil + } + } + 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) @@ -462,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 } @@ -482,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 } @@ -491,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 @@ -500,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 } @@ -548,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 a36be213ece..29a8dac71d4 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 79f6dfe9114..9c9abb4599d 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 948e012c62d..6437a764ed3 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 47d1537ec73..c89f5243054 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 7646c3dd719..7fcef430f26 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 7f940a98492..d32dc847d2d 100644 --- a/models/pull/automerge.go +++ b/models/pull/automerge.go @@ -5,14 +5,12 @@ package pull import ( "context" - "errors" "fmt" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" ) // AutoMerge represents a pull request scheduled for merging when checks succeed @@ -78,16 +76,8 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe return false, nil, err } - doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID) - if errors.Is(err, util.ErrNotExist) { - doer, err = user_model.NewGhostUser(), nil - } - if err != nil { - return false, nil, err - } - - scheduledPRM.Doer = doer - return true, scheduledPRM, nil + scheduledPRM.DoerID, scheduledPRM.Doer, err = user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID) + return true, scheduledPRM, err } // DeleteScheduledAutoMerge delete a scheduled pull request diff --git a/models/renderhelper/repo_comment.go b/models/renderhelper/repo_comment.go index ae0fbf0abd4..d1c587671b7 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 3b13bff73c7..1443f8b3c02 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 f1df8e89e0e..d9aa71b7270 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 3b48efba3af..72d98efc66e 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 218b1e4a670..61e2b570e53 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 522debb9fe6..34e7b108033 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/org_repo.go b/models/repo/org_repo.go index 96f21ba2aca..d8c2c91fec5 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 1c4f585ed90..b1653f2f1ae 100644 --- a/models/repo/pull_request_default_test.go +++ b/models/repo/pull_request_default_test.go @@ -26,7 +26,7 @@ func TestDefaultTargetBranchSelection(t *testing.T) { prConfig := prUnit.PullRequestsConfig() prConfig.DefaultTargetBranch = "branch2" prUnit.Config = prConfig - assert.NoError(t, UpdateRepoUnit(ctx, prUnit)) + assert.NoError(t, UpdateRepoUnitConfig(ctx, prUnit)) repo.Units = nil assert.Equal(t, "branch2", repo.GetPullRequestTargetBranch(ctx)) } diff --git a/models/repo/repo.go b/models/repo/repo.go index 25207cc28b0..7814bb4876d 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_list.go b/models/repo/repo_list.go index 811f83c9997..e927174a55f 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -778,3 +778,11 @@ func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (Repositor repos := make(RepositoryList, 0, opts.PageSize) return repos, count, db.SetSessionPagination(sess, &opts).Find(&repos) } + +func GetOwnerRepositoriesByIDs(ctx context.Context, ownerID int64, repoIDs []int64) (RepositoryList, error) { + if len(repoIDs) == 0 { + return RepositoryList{}, nil + } + repos := make(RepositoryList, 0, len(repoIDs)) + return repos, db.GetEngine(ctx).Where(builder.Eq{"owner_id": ownerID}).In("id", repoIDs).Find(&repos) +} diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 491e96770c4..c32adbbcd43 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -5,8 +5,6 @@ package repo import ( "context" - "slices" - "strings" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" @@ -144,6 +142,7 @@ func DefaultPullRequestsConfig() *PullRequestsConfig { AllowRebaseUpdate: true, DefaultAllowMaintainerEdit: true, } + cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge cfg.DefaultMergeStyle = MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle) cfg.DefaultMergeStyle = util.IfZero(cfg.DefaultMergeStyle, MergeStyleMerge) return cfg @@ -175,57 +174,6 @@ func DefaultPullRequestsUnit(repoID int64) RepoUnit { return RepoUnit{RepoID: repoID, Type: unit.TypePullRequests, Config: DefaultPullRequestsConfig()} } -type ActionsConfig struct { - DisabledWorkflows []string - // CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos. - // Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions. - CollaborativeOwnerIDs []int64 -} - -func (cfg *ActionsConfig) EnableWorkflow(file string) { - cfg.DisabledWorkflows = util.SliceRemoveAll(cfg.DisabledWorkflows, file) -} - -func (cfg *ActionsConfig) ToString() string { - return strings.Join(cfg.DisabledWorkflows, ",") -} - -func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool { - return slices.Contains(cfg.DisabledWorkflows, file) -} - -func (cfg *ActionsConfig) DisableWorkflow(file string) { - if slices.Contains(cfg.DisabledWorkflows, file) { - return - } - - cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) -} - -func (cfg *ActionsConfig) AddCollaborativeOwner(ownerID int64) { - if !slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) { - cfg.CollaborativeOwnerIDs = append(cfg.CollaborativeOwnerIDs, ownerID) - } -} - -func (cfg *ActionsConfig) RemoveCollaborativeOwner(ownerID int64) { - cfg.CollaborativeOwnerIDs = util.SliceRemoveAll(cfg.CollaborativeOwnerIDs, ownerID) -} - -func (cfg *ActionsConfig) IsCollaborativeOwner(ownerID int64) bool { - return slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) -} - -// FromDB fills up a ActionsConfig from serialized format. -func (cfg *ActionsConfig) FromDB(bs []byte) error { - return json.UnmarshalHandleDoubleEncode(bs, &cfg) -} - -// ToDB exports a ActionsConfig to a serialized format. -func (cfg *ActionsConfig) ToDB() ([]byte, error) { - return json.Marshal(cfg) -} - // ProjectsMode represents the projects enabled for a repository type ProjectsMode string @@ -279,7 +227,12 @@ func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool { func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { switch colName { case "type": - switch unit.Type(db.Cell2Int64(val)) { + var err error + r.Type, _, err = db.CellToInt(val, unit.TypeInvalid) + if err != nil { + setting.PanicInDevOrTesting("Unable to convert repo unit (id=%d) type: %v", r.ID, err) + } + switch r.Type { case unit.TypeExternalWiki: r.Config = new(ExternalWikiConfig) case unit.TypeExternalTracker: @@ -297,6 +250,11 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { default: r.Config = new(UnitConfig) } + case "config": + if *val == nil { + // XROM doesn't call FromDB if the value is nil, but we need to set default values for the config fields + _ = r.Config.FromDB(nil) + } } } @@ -360,9 +318,9 @@ func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err return units, nil } -// UpdateRepoUnit updates the provided repo unit -func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error { - _, err := db.GetEngine(ctx).ID(unit.ID).Update(unit) +// UpdateRepoUnitConfig updates the config of the provided repo unit +func UpdateRepoUnitConfig(ctx context.Context, unit *RepoUnit) error { + _, err := db.GetEngine(ctx).ID(unit.ID).Cols("config").Update(unit) return err } diff --git a/models/repo/repo_unit_actions.go b/models/repo/repo_unit_actions.go new file mode 100644 index 00000000000..50e2925792a --- /dev/null +++ b/models/repo/repo_unit_actions.go @@ -0,0 +1,153 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "slices" + + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" +) + +// ActionsTokenPermissionMode defines the default permission mode for Actions tokens +type ActionsTokenPermissionMode string + +const ( + // ActionsTokenPermissionModePermissive - write access by default (current behavior, backwards compatible) + ActionsTokenPermissionModePermissive ActionsTokenPermissionMode = "permissive" + // ActionsTokenPermissionModeRestricted - read access by default + ActionsTokenPermissionModeRestricted ActionsTokenPermissionMode = "restricted" +) + +func (ActionsTokenPermissionMode) EnumValues() []ActionsTokenPermissionMode { + return []ActionsTokenPermissionMode{ActionsTokenPermissionModePermissive /* default */, ActionsTokenPermissionModeRestricted} +} + +// ActionsTokenPermissions defines the permissions for different repository units +type ActionsTokenPermissions struct { + UnitAccessModes map[unit.Type]perm.AccessMode `json:"unit_access_modes,omitempty"` +} + +var ActionsTokenUnitTypes = []unit.Type{ + unit.TypeCode, + unit.TypeIssues, + unit.TypePullRequests, + unit.TypePackages, + unit.TypeActions, + unit.TypeWiki, + unit.TypeReleases, + unit.TypeProjects, +} + +func MakeActionsTokenPermissions(unitAccessMode perm.AccessMode) (ret ActionsTokenPermissions) { + ret.UnitAccessModes = make(map[unit.Type]perm.AccessMode) + for _, u := range ActionsTokenUnitTypes { + ret.UnitAccessModes[u] = unitAccessMode + } + return ret +} + +// ClampActionsTokenPermissions ensures that the given permissions don't exceed the maximum +func ClampActionsTokenPermissions(p1, p2 ActionsTokenPermissions) (ret ActionsTokenPermissions) { + ret.UnitAccessModes = make(map[unit.Type]perm.AccessMode) + for _, ut := range ActionsTokenUnitTypes { + ret.UnitAccessModes[ut] = min(p1.UnitAccessModes[ut], p2.UnitAccessModes[ut]) + } + return ret +} + +// MakeRestrictedPermissions returns the restricted permissions +func MakeRestrictedPermissions() ActionsTokenPermissions { + ret := MakeActionsTokenPermissions(perm.AccessModeNone) + ret.UnitAccessModes[unit.TypeCode] = perm.AccessModeRead + ret.UnitAccessModes[unit.TypePackages] = perm.AccessModeRead + ret.UnitAccessModes[unit.TypeReleases] = perm.AccessModeRead + return ret +} + +type ActionsConfig struct { + DisabledWorkflows []string + // CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos. + // Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions. + CollaborativeOwnerIDs []int64 + // TokenPermissionMode defines the default permission mode (permissive, restricted, or custom) + TokenPermissionMode ActionsTokenPermissionMode `json:"token_permission_mode,omitempty"` + // MaxTokenPermissions defines the absolute maximum permissions any token can have in this context. + // Workflow YAML "permissions" keywords can reduce permissions but never exceed this ceiling. + MaxTokenPermissions *ActionsTokenPermissions `json:"max_token_permissions,omitempty"` + // OverrideOwnerConfig indicates if this repository should override the owner-level configuration (User or Org) + OverrideOwnerConfig bool `json:"override_owner_config,omitempty"` +} + +func (cfg *ActionsConfig) EnableWorkflow(file string) { + cfg.DisabledWorkflows = util.SliceRemoveAll(cfg.DisabledWorkflows, file) +} + +func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool { + return slices.Contains(cfg.DisabledWorkflows, file) +} + +func (cfg *ActionsConfig) DisableWorkflow(file string) { + if slices.Contains(cfg.DisabledWorkflows, file) { + return + } + + cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) +} + +func (cfg *ActionsConfig) AddCollaborativeOwner(ownerID int64) { + if !slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) { + cfg.CollaborativeOwnerIDs = append(cfg.CollaborativeOwnerIDs, ownerID) + } +} + +func (cfg *ActionsConfig) RemoveCollaborativeOwner(ownerID int64) { + cfg.CollaborativeOwnerIDs = util.SliceRemoveAll(cfg.CollaborativeOwnerIDs, ownerID) +} + +func (cfg *ActionsConfig) IsCollaborativeOwner(ownerID int64) bool { + return slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) +} + +// GetDefaultTokenPermissions returns the default token permissions by its TokenPermissionMode. +// It does not apply MaxTokenPermissions; callers must clamp if needed. +func (cfg *ActionsConfig) GetDefaultTokenPermissions() ActionsTokenPermissions { + switch cfg.TokenPermissionMode { + case ActionsTokenPermissionModeRestricted: + return MakeRestrictedPermissions() + case ActionsTokenPermissionModePermissive: + return MakeActionsTokenPermissions(perm.AccessModeWrite) + default: + return ActionsTokenPermissions{} + } +} + +// GetMaxTokenPermissions returns the maximum allowed permissions +func (cfg *ActionsConfig) GetMaxTokenPermissions() ActionsTokenPermissions { + if cfg.MaxTokenPermissions != nil { + return *cfg.MaxTokenPermissions + } + // Default max is write for everything + return MakeActionsTokenPermissions(perm.AccessModeWrite) +} + +// ClampPermissions ensures that the given permissions don't exceed the maximum +func (cfg *ActionsConfig) ClampPermissions(perms ActionsTokenPermissions) ActionsTokenPermissions { + maxPerms := cfg.GetMaxTokenPermissions() + return ClampActionsTokenPermissions(perms, maxPerms) +} + +// FromDB fills up a ActionsConfig from serialized format. +func (cfg *ActionsConfig) FromDB(bs []byte) error { + _ = json.UnmarshalHandleDoubleEncode(bs, &cfg) + cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode) + return nil +} + +// ToDB exports a ActionsConfig to a serialized format. +func (cfg *ActionsConfig) ToDB() ([]byte, error) { + return json.Marshal(cfg) +} diff --git a/models/repo/repo_unit_test.go b/models/repo/repo_unit_test.go index 56dda5672d4..08f9ac2cd4e 100644 --- a/models/repo/repo_unit_test.go +++ b/models/repo/repo_unit_test.go @@ -4,8 +4,12 @@ package repo import ( + "strings" "testing" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" + "github.com/stretchr/testify/assert" ) @@ -26,5 +30,75 @@ func TestActionsConfig(t *testing.T) { cfg.DisableWorkflow("test1.yaml") cfg.DisableWorkflow("test2.yaml") cfg.DisableWorkflow("test3.yaml") - assert.Equal(t, "test1.yaml,test2.yaml,test3.yaml", cfg.ToString()) + assert.Equal(t, "test1.yaml,test2.yaml,test3.yaml", strings.Join(cfg.DisabledWorkflows, ",")) +} + +func TestActionsConfigTokenPermissions(t *testing.T) { + t.Run("Default Permission Mode", func(t *testing.T) { + cfg := &ActionsConfig{TokenPermissionMode: "invalid-value"} + _ = cfg.FromDB(nil) + assert.Equal(t, ActionsTokenPermissionModePermissive, cfg.TokenPermissionMode) + assert.Equal(t, perm.AccessModeWrite, cfg.GetDefaultTokenPermissions().UnitAccessModes[unit.TypeCode]) + }) + + t.Run("Explicit Permission Mode", func(t *testing.T) { + cfg := &ActionsConfig{ + TokenPermissionMode: ActionsTokenPermissionModeRestricted, + } + assert.Equal(t, ActionsTokenPermissionModeRestricted, cfg.TokenPermissionMode) + }) + + t.Run("Effective Permissions - Permissive Mode", func(t *testing.T) { + cfg := &ActionsConfig{ + TokenPermissionMode: ActionsTokenPermissionModePermissive, + } + defaultPerms := cfg.GetDefaultTokenPermissions() + perms := cfg.ClampPermissions(defaultPerms) + assert.Equal(t, perm.AccessModeWrite, perms.UnitAccessModes[unit.TypeCode]) + assert.Equal(t, perm.AccessModeWrite, perms.UnitAccessModes[unit.TypeIssues]) + assert.Equal(t, perm.AccessModeWrite, perms.UnitAccessModes[unit.TypePackages]) + }) + + t.Run("Effective Permissions - Restricted Mode", func(t *testing.T) { + cfg := &ActionsConfig{ + TokenPermissionMode: ActionsTokenPermissionModeRestricted, + } + defaultPerms := cfg.GetDefaultTokenPermissions() + perms := cfg.ClampPermissions(defaultPerms) + assert.Equal(t, perm.AccessModeRead, perms.UnitAccessModes[unit.TypeCode]) + assert.Equal(t, perm.AccessModeNone, perms.UnitAccessModes[unit.TypeIssues]) + assert.Equal(t, perm.AccessModeRead, perms.UnitAccessModes[unit.TypePackages]) + }) + + t.Run("Clamp Permissions", func(t *testing.T) { + cfg := &ActionsConfig{ + MaxTokenPermissions: &ActionsTokenPermissions{ + UnitAccessModes: map[unit.Type]perm.AccessMode{ + unit.TypeCode: perm.AccessModeRead, + unit.TypeIssues: perm.AccessModeWrite, + unit.TypePullRequests: perm.AccessModeRead, + unit.TypePackages: perm.AccessModeRead, + unit.TypeActions: perm.AccessModeNone, + unit.TypeWiki: perm.AccessModeWrite, + }, + }, + } + input := ActionsTokenPermissions{ + UnitAccessModes: map[unit.Type]perm.AccessMode{ + unit.TypeCode: perm.AccessModeWrite, // Should be clamped to Read + unit.TypeIssues: perm.AccessModeWrite, // Should stay Write + unit.TypePullRequests: perm.AccessModeWrite, // Should be clamped to Read + unit.TypePackages: perm.AccessModeWrite, // Should be clamped to Read + unit.TypeActions: perm.AccessModeRead, // Should be clamped to None + unit.TypeWiki: perm.AccessModeRead, // Should stay Read + }, + } + clamped := cfg.ClampPermissions(input) + assert.Equal(t, perm.AccessModeRead, clamped.UnitAccessModes[unit.TypeCode]) + assert.Equal(t, perm.AccessModeWrite, clamped.UnitAccessModes[unit.TypeIssues]) + assert.Equal(t, perm.AccessModeRead, clamped.UnitAccessModes[unit.TypePullRequests]) + assert.Equal(t, perm.AccessModeRead, clamped.UnitAccessModes[unit.TypePackages]) + assert.Equal(t, perm.AccessModeNone, clamped.UnitAccessModes[unit.TypeActions]) + assert.Equal(t, perm.AccessModeRead, clamped.UnitAccessModes[unit.TypeWiki]) + }) } diff --git a/models/repo/star.go b/models/repo/star.go index bc865f8373f..e1672623c82 100644 --- a/models/repo/star.go +++ b/models/repo/star.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" ) -// Star represents a starred repo by an user. +// Star represents a starred repo by a user. type Star struct { ID int64 `xorm:"pk autoincr"` UID int64 `xorm:"UNIQUE(s)"` diff --git a/models/repo/topic.go b/models/repo/topic.go index 6d5209d8210..14017da2d0b 100644 --- a/models/repo/topic.go +++ b/models/repo/topic.go @@ -44,12 +44,6 @@ type ErrTopicNotExist struct { Name string } -// IsErrTopicNotExist checks if an error is an ErrTopicNotExist. -func IsErrTopicNotExist(err error) bool { - _, ok := err.(ErrTopicNotExist) - return ok -} - // Error implements error interface func (err ErrTopicNotExist) Error() string { return fmt.Sprintf("topic is not exist [name: %s]", err.Name) diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index e15a64b01e8..28ae83a095e 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -8,6 +8,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -94,8 +95,7 @@ func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Reposit return db.FindAndCount[Repository](ctx, opts) } -// GetRepoAssignees returns all users that have write access and can be assigned to issues -// of the repository, +// GetRepoAssignees returns all users that have write access and can be assigned to issues or pull-requests of the repository, func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.User, err error) { if err = repo.LoadOwner(ctx); err != nil { return nil, err @@ -114,15 +114,9 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us uniqueUserIDs.AddMultiple(userIDs...) if repo.Owner.IsOrganization() { - additionalUserIDs := make([]int64, 0, 10) - if err = e.Table("team_user"). - Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). - Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). - Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))", - repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests). - Distinct("`team_user`.uid"). - Select("`team_user`.uid"). - Find(&additionalUserIDs); err != nil { + // issues and pull requests both need "assignee list" + additionalUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeIssues, unit.TypePullRequests) + if err != nil { return nil, err } uniqueUserIDs.AddMultiple(additionalUserIDs...) diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go index cd8a0f1a1f7..cd45db61d08 100644 --- a/models/repo/user_repo_test.go +++ b/models/repo/user_repo_test.go @@ -6,7 +6,12 @@ package repo_test import ( "testing" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + perm_model "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -14,9 +19,14 @@ import ( "github.com/stretchr/testify/require" ) -func TestRepoAssignees(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func TestUserRepo(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + t.Run("GetIssuePostersWithSearch", testUserRepoGetIssuePostersWithSearch) + t.Run("Assignees", testUserRepoAssignees) + t.Run("AssigneesNoTeamUnit", testRepoAssigneesNoTeamUnit) +} +func testUserRepoAssignees(t *testing.T) { repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) users, err := repo_model.GetRepoAssignees(t.Context(), repo2) assert.NoError(t, err) @@ -39,9 +49,29 @@ func TestRepoAssignees(t *testing.T) { } } -func TestGetIssuePostersWithSearch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testRepoAssigneesNoTeamUnit(t *testing.T) { + ctx := t.Context() + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) + require.NoError(t, repo.LoadOwner(ctx)) + require.True(t, repo.Owner.IsOrganization()) + + require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{}, &access_model.Access{})) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + team := &organization.Team{OrgID: repo.OwnerID, LowerName: "admin-team", AccessMode: perm_model.AccessModeAdmin} + require.NoError(t, db.Insert(ctx, team)) + require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: repo.OwnerID, TeamID: team.ID, UID: user.ID})) + require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: repo.OwnerID, TeamID: team.ID, RepoID: repo.ID})) + require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: repo.OwnerID, TeamID: team.ID, Type: unit.TypePullRequests, AccessMode: perm_model.AccessModeNone})) + + users, err := repo_model.GetRepoAssignees(ctx, repo) + require.NoError(t, err) + require.Len(t, users, 1) + assert.ElementsMatch(t, []int64{4}, []int64{users[0].ID}) +} + +func testUserRepoGetIssuePostersWithSearch(t *testing.T) { repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER") diff --git a/models/system/notice.go b/models/system/notice.go index f39188f8fb3..4b919dffc92 100644 --- a/models/system/notice.go +++ b/models/system/notice.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" ) // NoticeType describes the notice type @@ -60,18 +59,6 @@ func CreateRepositoryNotice(desc string, args ...any) error { return CreateNotice(graceful.GetManager().ShutdownContext(), NoticeRepository, desc, args...) } -// RemoveAllWithNotice removes all directories in given path and -// creates a system notice when error occurs. -func RemoveAllWithNotice(ctx context.Context, title, path string) { - if err := util.RemoveAll(path); err != nil { - desc := fmt.Sprintf("%s [%s]: %v", title, path, err) - log.Warn(title+" [%s]: %v", path, err) - if err = CreateNotice(graceful.GetManager().ShutdownContext(), NoticeRepository, desc); err != nil { - log.Error("CreateRepositoryNotice: %v", err) - } - } -} - // RemoveStorageWithNotice removes a file from the storage and // creates a system notice when error occurs. func RemoveStorageWithNotice(ctx context.Context, bucket storage.ObjectStorage, title, path string) { diff --git a/models/unittest/fixtures.go b/models/unittest/fixtures.go index a9a01a32275..872bdffc6dc 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 5b79cb5643b..e2f2f5846fb 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 72944ec0dbe..d45ef338478 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 00000000000..dd05b263ede --- /dev/null +++ b/models/unittest/mock_http.go @@ -0,0 +1,142 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package unittest + +import ( + "fmt" + "io" + "maps" + "net/http" + "net/http/httptest" + "net/url" + "os" + "slices" + "strings" + "testing" + + "code.gitea.io/gitea/modules/log" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// MockServerOptions tweaks NewMockWebServer behavior. +type MockServerOptions struct { + // Routes installs extra handlers on the mux before the fixture fallback; + // more specific patterns win. + Routes func(mux *http.ServeMux) + // StripPrefix is trimmed from the request path before forwarding upstream, + // useful when the client prepends a prefix the real upstream does not use + // (e.g. go-github prepends "/api/v3"). + StripPrefix string +} + +// NewMockWebServer returns a test HTTP server that records upstream responses on demand +// and replays them from disk on subsequent runs. +// +// - liveMode=true: requests are forwarded to liveServerBaseURL and responses written as +// fixture files under testDataDir. +// - liveMode=false: responses come from existing fixture files. +// +// Fixture format: header lines ("Name: value"), a blank line, then the body. Before +// replay, occurrences of liveServerBaseURL in the body are swapped for the mock URL. +// +// The typical switch is an env var holding an API token; fixtures ship committed so the +// default run (no token) works offline. +// +// token := os.Getenv("GITEA_TOKEN") +// mock := NewMockWebServer(t, "https://gitea.com", fixtureDir, token != "") +func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveMode bool, opts ...MockServerOptions) *httptest.Server { + t.Helper() + + var opt MockServerOptions + if len(opts) > 0 { + opt = opts[0] + } + + ignoredHeaders := []string{"cf-ray", "server", "date", "report-to", "nel", "x-request-id", "set-cookie"} + + var mockURL string + + fallback := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reqPath := r.URL.EscapedPath() + if r.URL.RawQuery != "" { + reqPath += "?" + r.URL.RawQuery + } + log.Info("mock server: %s %s", r.Method, reqPath) + + fixturePath := fmt.Sprintf("%s/%s_%s", testDataDir, r.Method, url.QueryEscape(reqPath)) + if strings.Contains(r.URL.Path, ".git/") { + fixturePath = fmt.Sprintf("%s/%s_%s", testDataDir, r.Method, url.QueryEscape(r.URL.Path)) + } + + if liveMode { + require.NoError(t, os.MkdirAll(testDataDir, 0o755)) + + liveURL := liveServerBaseURL + strings.TrimPrefix(reqPath, opt.StripPrefix) + req, err := http.NewRequest(r.Method, liveURL, r.Body) + require.NoError(t, err, "building upstream request to %s", liveURL) + for name, values := range r.Header { + if strings.EqualFold(name, "accept-encoding") { + continue + } + for _, value := range values { + req.Header.Add(name, value) + } + } + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err, "upstream request to %s failed", liveURL) + defer resp.Body.Close() + assert.Less(t, resp.StatusCode, 400, "upstream %s returned status %d", liveURL, resp.StatusCode) + + out, err := os.Create(fixturePath) + require.NoError(t, err, "creating fixture %s", fixturePath) + defer out.Close() + + for _, name := range slices.Sorted(maps.Keys(resp.Header)) { + if slices.Contains(ignoredHeaders, strings.ToLower(name)) { + continue + } + for _, value := range resp.Header[name] { + _, err := fmt.Fprintf(out, "%s: %s\n", name, value) + require.NoError(t, err) + } + } + _, err = out.WriteString("\n") + require.NoError(t, err) + + _, err = io.Copy(out, resp.Body) + require.NoError(t, err, "writing fixture body for %s", liveURL) + require.NoError(t, out.Sync()) + } + + raw, err := os.ReadFile(fixturePath) + require.NoError(t, err, "missing fixture: %s", fixturePath) + + replayed := strings.ReplaceAll(string(raw), liveServerBaseURL, mockURL) + headers, body, _ := strings.Cut(replayed, "\n\n") + for line := range strings.SplitSeq(headers, "\n") { + name, value, ok := strings.Cut(line, ": ") + if !ok || strings.EqualFold(name, "Content-Length") { + continue + } + w.Header().Set(name, value) + } + w.WriteHeader(http.StatusOK) + _, err = w.Write([]byte(body)) + require.NoError(t, err) + }) + + mux := http.NewServeMux() + if opt.Routes != nil { + opt.Routes(mux) + } + mux.Handle("/", fallback) + + server := httptest.NewServer(mux) + mockURL = server.URL + t.Cleanup(server.Close) + return server +} diff --git a/models/unittest/reflection.go b/models/unittest/reflection.go index bc96a059733..47145d6c031 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 63c9a3a9994..2a87a02fcde 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 e475ceb7489..a4a465a9d54 100644 --- a/models/user/badge.go +++ b/models/user/badge.go @@ -5,9 +5,12 @@ package user import ( "context" - "fmt" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" + "xorm.io/xorm/schemas" ) // Badge represents a user badge @@ -22,7 +25,16 @@ type Badge struct { type UserBadge struct { //nolint:revive // export stutter ID int64 `xorm:"pk autoincr"` BadgeID int64 - UserID int64 `xorm:"INDEX"` + UserID int64 +} + +// TableIndices implements xorm's TableIndices interface +func (n *UserBadge) TableIndices() []*schemas.Index { + indices := make([]*schemas.Index, 0, 1) + ubUnique := schemas.NewIndex("unique_user_badge", schemas.UniqueType) + ubUnique.AddColumn("user_id", "badge_id") + indices = append(indices, ubUnique) + return indices } func init() { @@ -42,32 +54,85 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) { return badges, count, err } -// CreateBadge creates a new badge. -func CreateBadge(ctx context.Context, badge *Badge) error { - _, err := db.GetEngine(ctx).Insert(badge) - return err +// GetBadgeUsersOptions contains options for getting users with a specific badge +type GetBadgeUsersOptions struct { + db.ListOptions + BadgeSlug string } -// GetBadge returns a badge +// GetBadgeUsers returns the users that have a specific badge with pagination support. +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", "badge", "`user_badge`.badge_id=badge.id"). + Where("badge.slug=?", opts.BadgeSlug) + + if opts.Page > 0 { + sess = db.SetSessionPagination(sess, opts) + } + + users := make([]*User, 0, opts.PageSize) + count, err := sess.FindAndCount(&users) + return users, count, err +} + +// CreateBadge creates a new badge. +func CreateBadge(ctx context.Context, badge *Badge) error { + exists, err := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge)) + if err != nil { + return err + } + if exists { + return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug) + } + + if _, err := db.GetEngine(ctx).Insert(badge); err != nil { + // Handle race between existence check and insert. + exists, existErr := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge)) + if existErr == nil && exists { + return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug) + } + return err + } + return nil +} + +// GetBadge returns a specific badge func GetBadge(ctx context.Context, slug string) (*Badge, error) { badge := new(Badge) has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge) - if !has { + if err != nil { return nil, err } - return badge, err + if !has { + return nil, util.NewNotExistErrorf("badge does not exist [slug: %s]", slug) + } + return badge, nil } // UpdateBadge updates a badge based on its slug. func UpdateBadge(ctx context.Context, badge *Badge) error { - _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge) + _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Cols("description", "image_url").Update(badge) return err } -// DeleteBadge deletes a badge. +// DeleteBadge deletes a badge and all associated user_badge entries. func DeleteBadge(ctx context.Context, badge *Badge) error { - _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge) - return err + return db.WithTx(ctx, func(ctx context.Context) error { + // First delete all user_badge entries for this badge + if _, err := db.GetEngine(ctx). + Where("badge_id = ?", badge.ID). + Delete(&UserBadge{}); err != nil { + return err + } + + // Then delete the badge itself + if _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge); err != nil { + return err + } + return nil + }) } // AddUserBadge adds a badge to a user. @@ -84,12 +149,25 @@ func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error { if err != nil { return err } else if !has { - return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug) + return util.NewNotExistErrorf("badge does not exist [slug: %s]", badge.Slug) } + + exists, err := db.GetEngine(ctx).Where("badge_id = ? AND user_id = ?", badge.ID, u.ID).Exist(new(UserBadge)) + if err != nil { + return err + } + if exists { + return util.NewAlreadyExistErrorf("user badge already exists [user_id: %d, badge_id: %d]", u.ID, badge.ID) + } + if err := db.Insert(ctx, &UserBadge{ BadgeID: badge.ID, UserID: u.ID, }); err != nil { + exists, existErr := db.GetEngine(ctx).Where("badge_id = ? AND user_id = ?", badge.ID, u.ID).Exist(new(UserBadge)) + if existErr == nil && exists { + return util.NewAlreadyExistErrorf("user badge already exists [user_id: %d, badge_id: %d]", u.ID, badge.ID) + } return err } } @@ -102,23 +180,75 @@ func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error { return RemoveUserBadges(ctx, u, []*Badge{badge}) } -// RemoveUserBadges removes badges from a user. +// RemoveUserBadges removes specific badges from a user. func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error { return db.WithTx(ctx, func(ctx context.Context) error { + if len(badges) == 0 { + return nil + } + + badgeSlugs := make([]string, 0, len(badges)) for _, badge := range badges { - if _, err := db.GetEngine(ctx). - Join("INNER", "badge", "badge.id = `user_badge`.badge_id"). - Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug). - Delete(&UserBadge{}); err != nil { - return err - } + badgeSlugs = append(badgeSlugs, badge.Slug) + } + var userBadges []UserBadge + if err := db.GetEngine(ctx).Table("user_badge"). + Join("INNER", "badge", "badge.id = `user_badge`.badge_id"). + Where("`user_badge`.user_id = ?", u.ID).In("`badge`.slug", badgeSlugs). + Find(&userBadges); err != nil { + return err + } + userBadgeIDs := make([]int64, 0, len(userBadges)) + for _, ub := range userBadges { + userBadgeIDs = append(userBadgeIDs, ub.ID) + } + if len(userBadgeIDs) == 0 { + return nil + } + if _, err := db.GetEngine(ctx).Table("user_badge").In("id", userBadgeIDs).Delete(); err != nil { + return err } return nil }) } -// 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 + + Keyword string + Slug string + ID int64 + OrderBy db.SearchOrderBy +} + +func (opts *SearchBadgeOptions) ToConds() builder.Cond { + cond := builder.NewCond() + + if opts.Keyword != "" { + keywordCond := builder.Or( + db.BuildCaseInsensitiveLike("badge.slug", opts.Keyword), + db.BuildCaseInsensitiveLike("badge.description", opts.Keyword), + ) + cond = cond.And(keywordCond) + } + + if opts.ID > 0 { + cond = cond.And(builder.Eq{"badge.id": opts.ID}) + } + + if len(opts.Slug) > 0 { + cond = cond.And(builder.Eq{"badge.slug": opts.Slug}) + } + + return cond +} + +func (opts *SearchBadgeOptions) ToOrders() string { + return opts.OrderBy.String() +} + +// SearchBadges returns badges based on the provided SearchBadgeOptions options +func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) { + return db.FindAndCount[Badge](ctx, opts) } diff --git a/models/user/badge_test.go b/models/user/badge_test.go new file mode 100644 index 00000000000..04979a9cafe --- /dev/null +++ b/models/user/badge_test.go @@ -0,0 +1,185 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" +) + +func TestBadge(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("GetBadgeNotExist", testGetBadgeNotExist) + t.Run("CreateBadgeAlreadyExists", testCreateBadgeAlreadyExists) + t.Run("GetBadgeUsers", testGetBadgeUsers) + t.Run("AddAndRemoveUserBadges", testAddAndRemoveUserBadges) + t.Run("SearchBadgesOrderingAndKeyword", testSearchBadgesOrderingAndKeyword) +} + +func testGetBadgeNotExist(t *testing.T) { + badge, err := user_model.GetBadge(t.Context(), "does-not-exist") + assert.Nil(t, badge) + assert.Error(t, err) + assert.ErrorIs(t, err, util.ErrNotExist) +} + +func testCreateBadgeAlreadyExists(t *testing.T) { + badge := &user_model.Badge{ + Slug: "duplicate-badge-slug", + Description: "First", + } + assert.NoError(t, user_model.CreateBadge(t.Context(), badge)) + + err := user_model.CreateBadge(t.Context(), &user_model.Badge{ + Slug: "duplicate-badge-slug", + Description: "Second", + }) + assert.Error(t, err) + assert.ErrorIs(t, err, util.ErrAlreadyExist) +} + +func testGetBadgeUsers(t *testing.T) { + // Create a test badge + badge := &user_model.Badge{ + Slug: "test-badge-users", + Description: "Test Badge", + ImageURL: "test.png", + } + assert.NoError(t, user_model.CreateBadge(t.Context(), badge)) + + // Create test users and assign badges + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + assert.NoError(t, user_model.AddUserBadge(t.Context(), user1, badge)) + assert.NoError(t, user_model.AddUserBadge(t.Context(), user2, badge)) + defer func() { + assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user1, badge)) + assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user2, badge)) + }() + + // Test getting users with pagination + opts := &user_model.GetBadgeUsersOptions{ + BadgeSlug: badge.Slug, + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 1, + }, + } + + users, count, err := user_model.GetBadgeUsers(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 2, count) + assert.Len(t, users, 1) + + // Test second page + opts.Page = 2 + users, count, err = user_model.GetBadgeUsers(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 2, count) + assert.Len(t, users, 1) + + // Test with non-existent badge + opts.BadgeSlug = "non-existent" + users, count, err = user_model.GetBadgeUsers(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 0, count) + assert.Empty(t, users) +} + +func testAddAndRemoveUserBadges(t *testing.T) { + badge1 := unittest.AssertExistsAndLoadBean(t, &user_model.Badge{ID: 1}) + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + // Add a badge to user and verify that it is returned in the list + assert.NoError(t, user_model.AddUserBadge(t.Context(), user1, badge1)) + badges, count, err := user_model.GetUserBadges(t.Context(), user1) + assert.Equal(t, int64(1), count) + assert.Equal(t, badge1.Slug, badges[0].Slug) + assert.NoError(t, err) + + // Confirm that it is impossible to duplicate the same badge + err = user_model.AddUserBadge(t.Context(), user1, badge1) + assert.Error(t, err) + assert.ErrorIs(t, err, util.ErrAlreadyExist) + + // Nothing happened to the existing badge + badges, count, err = user_model.GetUserBadges(t.Context(), user1) + assert.Equal(t, int64(1), count) + assert.Equal(t, badge1.Slug, badges[0].Slug) + assert.NoError(t, err) + + // Remove a badge from user and verify that it is no longer in the list + assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user1, badge1)) + _, count, err = user_model.GetUserBadges(t.Context(), user1) + assert.Equal(t, int64(0), count) + assert.NoError(t, err) + + // Removing empty or missing badge selections should be a no-op. + assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, nil)) + assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, []*user_model.Badge{{Slug: "does-not-exist"}})) +} + +func testSearchBadgesOrderingAndKeyword(t *testing.T) { + createdBadges := []*user_model.Badge{ + {Slug: "badge-sort-b", Description: "Badge Sort B"}, + {Slug: "badge-sort-c", Description: "Badge Sort C"}, + {Slug: "badge-sort-a", Description: "Badge Sort A"}, + {Slug: "badge-sort-case", Description: "MiXeDCaSeKeyword"}, + } + for _, badge := range createdBadges { + assert.NoError(t, user_model.CreateBadge(t.Context(), badge)) + } + + opts := &user_model.SearchBadgeOptions{ + ListOptions: db.ListOptions{ListAll: true}, + Keyword: "badge-sort-", + OrderBy: db.SearchOrderBy("`badge`.id ASC"), + } + + oldestFirst, count, err := user_model.SearchBadges(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 4, count) + assert.Equal(t, []string{"badge-sort-b", "badge-sort-c", "badge-sort-a", "badge-sort-case"}, collectBadgeSlugs(oldestFirst)) + + opts.OrderBy = db.SearchOrderBy("`badge`.id DESC") + newestFirst, count, err := user_model.SearchBadges(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 4, count) + assert.Equal(t, []string{"badge-sort-case", "badge-sort-a", "badge-sort-c", "badge-sort-b"}, collectBadgeSlugs(newestFirst)) + + opts.OrderBy = db.SearchOrderBy("`badge`.slug ASC") + alpha, count, err := user_model.SearchBadges(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 4, count) + assert.Equal(t, []string{"badge-sort-a", "badge-sort-b", "badge-sort-c", "badge-sort-case"}, collectBadgeSlugs(alpha)) + + opts.OrderBy = db.SearchOrderBy("`badge`.slug DESC") + reverseAlpha, count, err := user_model.SearchBadges(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 4, count) + assert.Equal(t, []string{"badge-sort-case", "badge-sort-c", "badge-sort-b", "badge-sort-a"}, collectBadgeSlugs(reverseAlpha)) + + opts.Keyword = "mixedcasekeyword" + opts.OrderBy = db.SearchOrderBy("`badge`.slug ASC") + caseInsensitive, count, err := user_model.SearchBadges(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 1, count) + assert.Equal(t, []string{"badge-sort-case"}, collectBadgeSlugs(caseInsensitive)) +} + +func collectBadgeSlugs(badges []*user_model.Badge) []string { + slugs := make([]string, 0, len(badges)) + for _, badge := range badges { + slugs = append(slugs, badge.Slug) + } + return slugs +} diff --git a/models/user/block.go b/models/user/block.go index f4afd47d0f7..03f984a8fdf 100644 --- a/models/user/block.go +++ b/models/user/block.go @@ -90,7 +90,7 @@ func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, er return nil, err } if len(blocks) == 0 { - return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist + return nil, util.NewNotExistErrorf("blocking record doesn't exist") } return blocks[0], nil } diff --git a/models/user/email_address.go b/models/user/email_address.go index aa483d5f005..670d417f9e6 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -147,11 +147,6 @@ func InsertEmailAddress(ctx context.Context, email *EmailAddress) (*EmailAddress return email, nil } -func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error { - _, err := db.GetEngine(ctx).ID(email.ID).AllCols().Update(email) - return err -} - // ValidateEmail check if email is a valid & allowed address func ValidateEmail(email string) error { if err := validateEmailBasic(email); err != nil { diff --git a/models/user/error.go b/models/user/error.go index cbf19998d10..5a956a2afe1 100644 --- a/models/user/error.go +++ b/models/user/error.go @@ -71,27 +71,6 @@ func (err ErrUserProhibitLogin) Unwrap() error { return util.ErrPermissionDenied } -// ErrUserInactive represents a "ErrUserInactive" kind of error. -type ErrUserInactive struct { - UID int64 - Name string -} - -// IsErrUserInactive checks if an error is a ErrUserInactive -func IsErrUserInactive(err error) bool { - _, ok := err.(ErrUserInactive) - return ok -} - -func (err ErrUserInactive) Error() string { - return fmt.Sprintf("user is inactive [uid: %d, name: %s]", err.UID, err.Name) -} - -// Unwrap unwraps this error as a ErrPermission error -func (err ErrUserInactive) Unwrap() error { - return util.ErrPermissionDenied -} - // ErrUserIsNotLocal represents a "ErrUserIsNotLocal" kind of error. type ErrUserIsNotLocal struct { UID int64 diff --git a/models/user/external_login_user.go b/models/user/external_login_user.go index 0e764efb9fe..636a2007ee8 100644 --- a/models/user/external_login_user.go +++ b/models/user/external_login_user.go @@ -21,12 +21,6 @@ type ErrExternalLoginUserAlreadyExist struct { LoginSourceID int64 } -// IsErrExternalLoginUserAlreadyExist checks if an error is a ExternalLoginUserAlreadyExist. -func IsErrExternalLoginUserAlreadyExist(err error) bool { - _, ok := err.(ErrExternalLoginUserAlreadyExist) - return ok -} - func (err ErrExternalLoginUserAlreadyExist) Error() string { return fmt.Sprintf("external login user already exists [externalID: %s, userID: %d, loginSourceID: %d]", err.ExternalID, err.UserID, err.LoginSourceID) } @@ -41,12 +35,6 @@ type ErrExternalLoginUserNotExist struct { LoginSourceID int64 } -// IsErrExternalLoginUserNotExist checks if an error is a ExternalLoginUserNotExist. -func IsErrExternalLoginUserNotExist(err error) bool { - _, ok := err.(ErrExternalLoginUserNotExist) - return ok -} - func (err ErrExternalLoginUserNotExist) Error() string { return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID) } diff --git a/models/user/setting.go b/models/user/setting.go index c65afae76c8..67b45208fcf 100644 --- a/models/user/setting.go +++ b/models/user/setting.go @@ -11,10 +11,12 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/json" setting_module "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "xorm.io/builder" + "xorm.io/xorm/convert" ) // Setting is a key value store of user settings @@ -48,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) @@ -211,3 +207,44 @@ func upsertUserSettingValue(ctx context.Context, userID int64, key, value string return err }) } + +func GetUserSettingJSON[T any](ctx context.Context, userID int64, key string, def T) (ret T, _ error) { + ret = def + str, err := GetUserSetting(ctx, userID, key) + if err != nil { + return ret, err + } + + conv, ok := any(&ret).(convert.ConversionFrom) + if !ok { + conv, ok = any(ret).(convert.ConversionFrom) + } + if ok { + if err := conv.FromDB(util.UnsafeStringToBytes(str)); err != nil { + return ret, err + } + } else { + if str == "" { + return ret, nil + } + err = json.Unmarshal(util.UnsafeStringToBytes(str), &ret) + } + return ret, err +} + +func SetUserSettingJSON[T any](ctx context.Context, userID int64, key string, val T) (err error) { + conv, ok := any(&val).(convert.ConversionTo) + if !ok { + conv, ok = any(val).(convert.ConversionTo) + } + var bs []byte + if ok { + bs, err = conv.ToDB() + } else { + bs, err = json.Marshal(val) + } + if err != nil { + return err + } + return SetUserSetting(ctx, userID, key, util.UnsafeBytesToString(bs)) +} diff --git a/models/user/setting_options.go b/models/user/setting_options.go index 587a46e8dec..5867b908d1b 100644 --- a/models/user/setting_options.go +++ b/models/user/setting_options.go @@ -22,4 +22,6 @@ const ( SettingEmailNotificationGiteaActionsAll = "all" SettingEmailNotificationGiteaActionsFailureOnly = "failure-only" // Default for actions email preference SettingEmailNotificationGiteaActionsDisabled = "disabled" + + SettingsKeyActionsConfig = "actions.config" ) diff --git a/models/user/user.go b/models/user/user.go index a74662bb12a..8a39eca634e 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" @@ -524,10 +525,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 +615,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 +1015,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 +1051,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 +1066,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 +1117,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 +1317,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 +1463,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 +1489,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 70a900378f3..3ae9c6e3665 100644 --- a/models/user/user_system_test.go +++ b/models/user/user_system_test.go @@ -11,8 +11,9 @@ import ( ) func TestSystemUser(t *testing.T) { - u, err := GetPossibleUserByID(t.Context(), -1) + uid, u, err := GetPossibleUserByID(t.Context(), -1) require.NoError(t, err) + assert.Equal(t, int64(-1), uid) assert.Equal(t, "Ghost", u.Name) assert.Equal(t, "ghost", u.LowerName) assert.True(t, u.IsGhost()) @@ -21,8 +22,9 @@ func TestSystemUser(t *testing.T) { require.NotNil(t, u) assert.Equal(t, "Ghost", u.Name) - u, err = GetPossibleUserByID(t.Context(), -2) + uid, u, err = GetPossibleUserByID(t.Context(), -2) require.NoError(t, err) + assert.Equal(t, int64(-2), uid) assert.Equal(t, "gitea-actions", u.Name) assert.Equal(t, "gitea-actions", u.LowerName) assert.True(t, u.IsGiteaActions()) @@ -31,6 +33,8 @@ func TestSystemUser(t *testing.T) { require.NotNil(t, u) assert.Equal(t, "Gitea Actions", u.FullName) - _, err = GetPossibleUserByID(t.Context(), -3) - require.Error(t, err) + uid, u, err = GetPossibleUserByID(t.Context(), 999999) + require.NoError(t, err) + assert.Equal(t, int64(-1), uid) + assert.Equal(t, "Ghost", u.Name) } diff --git a/models/webhook/main_test.go b/models/webhook/main_test.go index f19465d5053..5f2d5081a1a 100644 --- a/models/webhook/main_test.go +++ b/models/webhook/main_test.go @@ -15,5 +15,6 @@ func TestMain(m *testing.M) { "webhook.yml", "hook_task.yml", }, + SetUp: prepareWebhookTestData, }) } diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 7d4b2e2237d..5144c0eca60 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 d0013c6873f..9e954d1e377 100644 --- a/models/webhook/webhook_system_test.go +++ b/models/webhook/webhook_system_test.go @@ -10,28 +10,28 @@ import ( "code.gitea.io/gitea/modules/optional" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestListSystemWebhookOptions(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + hookSystem := unittest.AssertExistsAndLoadBean(t, &Webhook{URL: "https://www.example.com/system"}) + hookDefault := unittest.AssertExistsAndLoadBean(t, &Webhook{URL: "https://www.example.com/default"}) opts := ListSystemWebhookOptions{IsSystem: optional.None[bool]()} hooks, _, err := GetGlobalWebhooks(t.Context(), &opts) - assert.NoError(t, err) - if assert.Len(t, hooks, 2) { - assert.Equal(t, int64(5), hooks[0].ID) - assert.Equal(t, int64(6), hooks[1].ID) - } + require.NoError(t, err) + require.Len(t, hooks, 2) + assert.Equal(t, hookSystem.ID, hooks[0].ID) + assert.Equal(t, hookDefault.ID, hooks[1].ID) + opts.IsSystem = optional.Some(true) hooks, _, err = GetGlobalWebhooks(t.Context(), &opts) - assert.NoError(t, err) - if assert.Len(t, hooks, 1) { - assert.Equal(t, int64(5), hooks[0].ID) - } + require.NoError(t, err) + require.Len(t, hooks, 1) + assert.Equal(t, hookSystem.ID, hooks[0].ID) opts.IsSystem = optional.Some(false) hooks, _, err = GetGlobalWebhooks(t.Context(), &opts) - assert.NoError(t, err) - if assert.Len(t, hooks, 1) { - assert.Equal(t, int64(6), hooks[0].ID) - } + require.NoError(t, err) + require.Len(t, hooks, 1) + assert.Equal(t, hookDefault.ID, hooks[0].ID) } diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index 71f50017c51..073af91de21 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -4,6 +4,7 @@ package webhook import ( + "context" "testing" "time" @@ -14,40 +15,105 @@ import ( "code.gitea.io/gitea/modules/timeutil" webhook_module "code.gitea.io/gitea/modules/webhook" + "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "xorm.io/builder" ) -func TestHookContentType_Name(t *testing.T) { - assert.Equal(t, "json", ContentTypeJSON.Name()) - assert.Equal(t, "form", ContentTypeForm.Name()) +func prepareWebhookTestData() error { + if err := unittest.PrepareTestDatabase(); err != nil { + return err + } + var hooks []*Webhook + hooks = append(hooks, &Webhook{ + RepoID: 1, + URL: "https://www.example.com/url1", + ContentType: ContentTypeJSON, + Events: `{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}`, + IsActive: true, + }) + hooks = append(hooks, &Webhook{ + RepoID: 1, + URL: "https://www.example.com/url2", + ContentType: ContentTypeJSON, + Events: `{}`, + IsActive: false, + }) + hooks = append(hooks, &Webhook{ + OwnerID: 3, + RepoID: 3, + URL: "https://www.example.com/url3", + ContentType: ContentTypeJSON, + Events: `{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}`, + IsActive: true, + }) + hooks = append(hooks, &Webhook{ + OwnerID: 3, + RepoID: 3, + URL: "https://www.example.com/url3", + ContentType: ContentTypeJSON, + Events: `{}`, + }) + hooks = append(hooks, &Webhook{ + RepoID: 2, + URL: "https://www.example.com/url4", + ContentType: ContentTypeJSON, + Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`, + IsActive: true, + }) + hooks = append(hooks, &Webhook{ + URL: "https://www.example.com/system", + ContentType: ContentTypeJSON, + Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`, + IsSystemWebhook: true, + }) + hooks = append(hooks, &Webhook{ + URL: "https://www.example.com/default", + ContentType: ContentTypeJSON, + Events: `{"push_only":true,"branch_filter":"{master,feature*}"}`, + }) + ctx := context.Background() + if err := db.TruncateBeans(ctx, &Webhook{}); err != nil { + return err + } + if err := db.Insert(ctx, hooks); err != nil { + return err + } + + hook, _, _ := db.Get[Webhook](ctx, builder.Eq{"repo_id": 1, "is_active": true}) + var tasks []*HookTask + tasks = append(tasks, &HookTask{HookID: hook.ID, UUID: uuid.New().String()}) + tasks = append(tasks, &HookTask{HookID: hook.ID, UUID: uuid.New().String()}) + tasks = append(tasks, &HookTask{HookID: hook.ID, UUID: uuid.New().String()}) + if err := db.TruncateBeans(ctx, &HookTask{}); err != nil { + return err + } + return db.Insert(ctx, tasks) } -func TestIsValidHookContentType(t *testing.T) { +func TestWebHookContentType(t *testing.T) { + assert.Equal(t, "json", ContentTypeJSON.Name()) + assert.Equal(t, "form", ContentTypeForm.Name()) assert.True(t, IsValidHookContentType("json")) assert.True(t, IsValidHookContentType("form")) assert.False(t, IsValidHookContentType("invalid")) } func TestWebhook_History(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1}) - tasks, err := webhook.History(t.Context(), 0) - assert.NoError(t, err) - if assert.Len(t, tasks, 3) { - assert.Equal(t, int64(3), tasks[0].ID) - assert.Equal(t, int64(2), tasks[1].ID) - assert.Equal(t, int64(1), tasks[2].ID) - } + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true}) + tasks, err := hook.History(t.Context(), 0) + require.NoError(t, err) + require.Len(t, tasks, 3) - webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2}) - tasks, err = webhook.History(t.Context(), 0) + hook = unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, Events: "{}"}) + tasks, err = hook.History(t.Context(), 0) assert.NoError(t, err) assert.Empty(t, tasks) } func TestWebhook_UpdateEvent(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1}) + webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true}) hookEvent := &webhook_module.HookEvent{ PushOnly: true, SendEverything: false, @@ -100,10 +166,10 @@ func TestCreateWebhook(t *testing.T) { } func TestGetWebhookByRepoID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - hook, err := GetWebhookByRepoID(t.Context(), 1, 1) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true}) + loaded, err := GetWebhookByRepoID(t.Context(), 1, hook.ID) assert.NoError(t, err) - assert.Equal(t, int64(1), hook.ID) + assert.Equal(t, hook.ID, loaded.ID) _, err = GetWebhookByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) assert.Error(t, err) @@ -111,10 +177,10 @@ func TestGetWebhookByRepoID(t *testing.T) { } func TestGetWebhookByOwnerID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - hook, err := GetWebhookByOwnerID(t.Context(), 3, 3) - assert.NoError(t, err) - assert.Equal(t, int64(3), hook.ID) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3}) + loaded, err := GetWebhookByOwnerID(t.Context(), 3, hook.ID) + require.NoError(t, err) + require.Equal(t, hook.ID, loaded.ID) _, err = GetWebhookByOwnerID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) assert.Error(t, err) @@ -122,48 +188,45 @@ func TestGetWebhookByOwnerID(t *testing.T) { } func TestGetActiveWebhooksByRepoID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true}) hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)}) - assert.NoError(t, err) - if assert.Len(t, hooks, 1) { - assert.Equal(t, int64(1), hooks[0].ID) - assert.True(t, hooks[0].IsActive) - } + require.NoError(t, err) + require.Len(t, hooks, 1) + assert.Equal(t, hook.ID, hooks[0].ID) + assert.True(t, hooks[0].IsActive) } func TestGetWebhooksByRepoID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{RepoID: 1}) - assert.NoError(t, err) - if assert.Len(t, hooks, 2) { - assert.Equal(t, int64(1), hooks[0].ID) - assert.Equal(t, int64(2), hooks[1].ID) - } + require.NoError(t, err) + require.Len(t, hooks, 2) + assert.Equal(t, int64(1), hooks[0].RepoID) + assert.True(t, hooks[0].IsActive) + assert.Equal(t, int64(1), hooks[1].RepoID) + assert.False(t, hooks[1].IsActive) } func TestGetActiveWebhooksByOwnerID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)}) - assert.NoError(t, err) - if assert.Len(t, hooks, 1) { - assert.Equal(t, int64(3), hooks[0].ID) - assert.True(t, hooks[0].IsActive) - } + require.NoError(t, err) + require.Len(t, hooks, 1) + assert.Equal(t, int64(3), hooks[0].OwnerID) + assert.True(t, hooks[0].IsActive) } func TestGetWebhooksByOwnerID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) hooks, err := db.Find[Webhook](t.Context(), ListWebhookOptions{OwnerID: 3}) - assert.NoError(t, err) - if assert.Len(t, hooks, 1) { - assert.Equal(t, int64(3), hooks[0].ID) - assert.True(t, hooks[0].IsActive) - } + require.NoError(t, err) + require.Len(t, hooks, 2) + assert.Equal(t, int64(3), hooks[0].OwnerID) + assert.True(t, hooks[0].IsActive) + assert.Equal(t, int64(3), hooks[1].OwnerID) + assert.False(t, hooks[1].IsActive) } func TestUpdateWebhook(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - hook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2}) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, Events: `{}`}) + require.False(t, hook.IsActive) hook.IsActive = true hook.ContentType = ContentTypeForm unittest.AssertNotExistsBean(t, hook) @@ -172,48 +235,36 @@ func TestUpdateWebhook(t *testing.T) { } func TestDeleteWebhookByRepoID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2, RepoID: 1}) - assert.NoError(t, DeleteWebhookByRepoID(t.Context(), 1, 2)) - unittest.AssertNotExistsBean(t, &Webhook{ID: 2, RepoID: 1}) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, Events: `{}`}) + assert.NoError(t, DeleteWebhookByRepoID(t.Context(), 1, hook.ID)) + unittest.AssertNotExistsBean(t, &Webhook{ID: hook.ID, RepoID: 1}) err := DeleteWebhookByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) - assert.Error(t, err) assert.True(t, IsErrWebhookNotExist(err)) } func TestDeleteWebhookByOwnerID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OwnerID: 3}) - assert.NoError(t, DeleteWebhookByOwnerID(t.Context(), 3, 3)) - unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OwnerID: 3}) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3, Events: `{}`}) + assert.NoError(t, DeleteWebhookByOwnerID(t.Context(), 3, hook.ID)) + unittest.AssertNotExistsBean(t, &Webhook{ID: hook.ID, OwnerID: 3}) err := DeleteWebhookByOwnerID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) - assert.Error(t, err) assert.True(t, IsErrWebhookNotExist(err)) } func TestHookTasks(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - hookTasks, err := HookTasks(t.Context(), 1, 1) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{RepoID: 1, IsActive: true}) + hookTasks, err := HookTasks(t.Context(), hook.ID, 1) assert.NoError(t, err) - if assert.Len(t, hookTasks, 3) { - assert.Equal(t, int64(3), hookTasks[0].ID) - assert.Equal(t, int64(2), hookTasks[1].ID) - assert.Equal(t, int64(1), hookTasks[2].ID) - } - + assert.Len(t, hookTasks, 3) hookTasks, err = HookTasks(t.Context(), unittest.NonexistentID, 1) assert.NoError(t, err) assert.Empty(t, hookTasks) } func TestCreateHookTask(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - hookTask := &HookTask{ - HookID: 3, - PayloadVersion: 2, - } + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3, IsActive: true}) + hookTask := &HookTask{HookID: hook.ID, PayloadVersion: 2} unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(t.Context(), hookTask) assert.NoError(t, err) @@ -221,20 +272,23 @@ func TestCreateHookTask(t *testing.T) { } func TestUpdateHookTask(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + hook := unittest.AssertExistsAndLoadBean(t, &Webhook{OwnerID: 3, IsActive: true}) + hookTask := &HookTask{HookID: hook.ID, PayloadVersion: 2} + _, err := CreateHookTask(t.Context(), hookTask) + assert.NoError(t, err) - hook := unittest.AssertExistsAndLoadBean(t, &HookTask{ID: 1}) - hook.PayloadContent = "new payload content" - hook.IsDelivered = true - unittest.AssertNotExistsBean(t, hook) - assert.NoError(t, UpdateHookTask(t.Context(), hook)) - unittest.AssertExistsAndLoadBean(t, hook) + hookTask.PayloadContent = "new payload content" + hookTask.IsDelivered = true + unittest.AssertNotExistsBean(t, hookTask) + assert.NoError(t, UpdateHookTask(t.Context(), hookTask)) + unittest.AssertExistsAndLoadBean(t, hookTask) } func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup1", ContentType: ContentTypeJSON, Events: `{"push_only":true}`} + require.NoError(t, db.Insert(t.Context(), hook)) hookTask := &HookTask{ - HookID: 3, + HookID: hook.ID, IsDelivered: true, Delivered: timeutil.TimeStampNanoNow(), PayloadVersion: 2, @@ -249,9 +303,10 @@ func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) { } func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup2", ContentType: ContentTypeJSON, Events: `{"push_only":true}`} + require.NoError(t, db.Insert(t.Context(), hook)) hookTask := &HookTask{ - HookID: 4, + HookID: hook.ID, IsDelivered: false, PayloadVersion: 2, } @@ -265,9 +320,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) { } func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup3", ContentType: ContentTypeJSON, Events: `{"push_only":true}`} + require.NoError(t, db.Insert(t.Context(), hook)) hookTask := &HookTask{ - HookID: 4, + HookID: hook.ID, IsDelivered: true, Delivered: timeutil.TimeStampNanoNow(), PayloadVersion: 2, @@ -282,9 +338,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) { } func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup4", ContentType: ContentTypeJSON, Events: `{"push_only":true}`} + require.NoError(t, db.Insert(t.Context(), hook)) hookTask := &HookTask{ - HookID: 3, + HookID: hook.ID, IsDelivered: true, Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()), PayloadVersion: 2, @@ -299,9 +356,10 @@ func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) { } func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup5", ContentType: ContentTypeJSON, Events: `{"push_only":true}`} + require.NoError(t, db.Insert(t.Context(), hook)) hookTask := &HookTask{ - HookID: 4, + HookID: hook.ID, IsDelivered: false, PayloadVersion: 2, } @@ -315,9 +373,10 @@ func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) { } func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + hook := &Webhook{RepoID: 3, URL: "https://www.example.com/cleanup6", ContentType: ContentTypeJSON, Events: `{"push_only":true}`} + require.NoError(t, db.Insert(t.Context(), hook)) hookTask := &HookTask{ - HookID: 4, + HookID: hook.ID, IsDelivered: true, Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()), PayloadVersion: 2, diff --git a/modules/actions/artifacts.go b/modules/actions/artifacts.go index d28726e8993..4884eb42e80 100644 --- a/modules/actions/artifacts.go +++ b/modules/actions/artifacts.go @@ -5,44 +5,61 @@ 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" ) -// 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 +// 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 art.ArtifactName+".zip" == art.ArtifactPath && art.ContentEncoding == "application/zip" + return strings.Contains(art.ContentEncodingOrType, "/") } -func DownloadArtifactV4ServeDirectOnly(ctx *context.Base, art *actions_model.ActionArtifact) (bool, error) { - if setting.Actions.ArtifactStorage.ServeDirect() { - u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath, ctx.Req.Method, nil) - if u != nil && err == nil { - ctx.Redirect(u.String(), http.StatusFound) - return true, nil - } +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 false, nil + return u.String(), nil } -func DownloadArtifactV4Fallback(ctx *context.Base, art *actions_model.ActionArtifact) error { +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() - http.ServeContent(ctx.Resp, ctx.Req, art.ArtifactName+".zip", art.CreatedUnix.AsLocalTime(), f) + 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 { - ok, err := DownloadArtifactV4ServeDirectOnly(ctx, art) - if ok || err != nil { - return err + if DownloadArtifactV4ServeDirect(ctx, art) { + return nil } - return DownloadArtifactV4Fallback(ctx, art) + return DownloadArtifactV4ReadStorage(ctx, art) } diff --git a/modules/actions/github.go b/modules/actions/github.go index 68116ec83a5..b7f475aa91b 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 363cb2c18bc..3e314afb62b 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 512b6f02ab6..37defeff7a0 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 1d4c4c1756a..e7a2b484981 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 51ba70fc2ad..e74f0644f82 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 7132c278e95..2c4bd1f93a8 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 d0e82041618..bd29cb94254 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 00000000000..6731507a327 --- /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 00000000000..26591aadcd7 --- /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/workflows.go b/modules/actions/workflows.go index 4ac06def4d5..0269e8b0bb5 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -17,8 +17,8 @@ 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" + "gitea.com/gitea/runner/act/workflowpattern" "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, diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index ea027366f7e..cda2de13e28 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 41e4ca7376d..380c3ac4551 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 cca1482b1d0..a8b608e6be4 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/avatar/avatar.go b/modules/avatar/avatar.go index 3b622b99af0..44c61e1ff67 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 96e0561e155..c87d3cfa5af 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 e3fda5be982..00000000000 --- 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 c88ffd5aa5c..669a46c91aa 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 d3be0b1a133..81d2e8065b0 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 a87e8464666..00000000000 --- 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 5eeeedc4e29..00000000000 --- 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 b1566549733..e5df55c9b64 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 167683a298b..8f25e7876d4 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 37b6ad86d49..fb9ebbb2281 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 22e7f14f39f..11d09083fcb 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 9d796a0c183..4e1ff0fcf40 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 bd57dd6c4dd..16ea53fda1a 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 61f29120a65..00000000000 --- 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 812f0e34b3a..ddda875a9fd 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 a0ab4e71862..9c6994249b2 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 10d8f20aa48..dc8f9ce1a5e 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 02829d6a1ed..2f160707041 100644 --- a/modules/dump/dumper.go +++ b/modules/dump/dumper.go @@ -4,6 +4,7 @@ package dump import ( + "archive/zip" "context" "errors" "fmt" @@ -85,7 +86,7 @@ func NewDumper(ctx context.Context, format string, output io.Writer) (*Dumper, e var comp archives.ArchiverAsync switch format { case "zip": - comp = archives.Zip{} + comp = archives.Zip{Compression: zip.Deflate} case "tar": comp = archives.Tar{} case "tar.sz": diff --git a/modules/eventsource/event.go b/modules/eventsource/event.go index ebcca509034..c72cc466a03 100644 --- a/modules/eventsource/event.go +++ b/modules/eventsource/event.go @@ -7,7 +7,6 @@ import ( "bytes" "fmt" "io" - "strings" "time" "code.gitea.io/gitea/modules/json" @@ -110,9 +109,3 @@ func (e *Event) WriteTo(w io.Writer) (int64, error) { return sum, err } - -func (e *Event) String() string { - buf := new(strings.Builder) - _, _ = e.WriteTo(buf) - return buf.String() -} diff --git a/modules/generate/generate.go b/modules/generate/generate.go index 2d9a3dd9022..9baa057b17e 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 af640a60c1e..f9dd20cc7fe 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 710561f0451..4e18282bf38 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 795fc4ce3dd..595043d1d2e 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 8a0b3420795..4d77fb03c78 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 69662ffc1a7..782d34d2493 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 dfecfe60575..b576451db87 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 d7b945ed6b8..92ad1d21fda 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_reader.go b/modules/git/commit_reader.go index eb8f4c63227..ce48d6f3198 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 0aefb30c95e..9fb1539dc99 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 de7b7455ebb..a7668e4debe 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 91868076b42..874b3744c30 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 7a37ced3565..fc1c8e7cf6f 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 2df83f9843a..69eb07d1f0b 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -192,13 +192,13 @@ func RunGitTests(m interface{ Run() int }) { func runGitTests(m interface{ Run() int }) int { gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home") if err != nil { - testlogger.Panicf("unable to create temp dir: %s", err.Error()) + return testlogger.MainErrorf("unable to create temp dir: %v", err) } defer cleanup() setting.Git.HomePath = gitHomePath if err = InitFull(); err != nil { - testlogger.Panicf("failed to call Init: %s", err.Error()) + return testlogger.MainErrorf("failed to call Init: %v", err) } return m.Run() } diff --git a/modules/git/gitcmd/command.go b/modules/git/gitcmd/command.go index e9b51802feb..ee447dfd032 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 6e4214d9953..1a9bfe7d752 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 b674068c400..436f4e18ae4 100644 --- a/modules/git/gitcmd/error.go +++ b/modules/git/gitcmd/error.go @@ -56,6 +56,14 @@ func StderrHasPrefix(err error, prefix string) bool { return strings.HasPrefix(stderr, prefix) } +func StderrContains(err error, sub string) bool { + stderr, ok := ErrorAsStderr(err) + if !ok { + return false + } + return strings.Contains(stderr, sub) +} + func IsErrorExitCode(err error, code int) bool { var exitError *exec.ExitError if errors.As(err, &exitError) { diff --git a/modules/git/object_id_test.go b/modules/git/object_id_test.go index 03d0c85d878..213a0cd341d 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 d46cd3344dc..23d59c19233 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. @@ -47,14 +44,11 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { } 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 +58,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 4223cbb3d76..5b81b49edda 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 91bda0d0e5e..b4a4aff4ed3 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 c10f73690cf..8ba3f833868 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 3f7883ab14d..ea56a1375de 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_tag.go b/modules/git/repo_tag.go index 2599236ae03..862b3708906 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 878ab55bf20..a3e25f3899e 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 a9ac0408216..053c633662c 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 e6f8e75a0eb..50c6ab50361 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 8bf3658d62f..232dda7fd9e 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 ba02c28946d..6a65e359a25 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_mode.go b/modules/git/tree_entry_mode.go index 2ceba113740..f80f6bdc750 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 3df6eeab68d..bd6a5783b26 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 0ab17862fee..0a8cb0544cb 100644 --- a/modules/gitrepo/commit.go +++ b/modules/gitrepo/commit.go @@ -44,23 +44,6 @@ func CommitsCount(ctx context.Context, repo Repository, opts CommitsCountOptions return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) } -// CommitsCountBetween return numbers of commits between two commits -func CommitsCountBetween(ctx context.Context, repo Repository, start, end string) (int64, error) { - count, err := CommitsCount(ctx, repo, CommitsCountOptions{ - Revision: []string{start + ".." + end}, - }) - - if err != nil && strings.Contains(err.Error(), "no merge base") { - // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. - // previously it would return the results of git rev-list before last so let's try that... - return CommitsCount(ctx, repo, CommitsCountOptions{ - Revision: []string{start, end}, - }) - } - - return count, err -} - // FileCommitsCount return the number of files at a revision func FileCommitsCount(ctx context.Context, repo Repository, revision, file string) (int64, error) { return CommitsCount(ctx, repo, diff --git a/modules/gitrepo/compare.go b/modules/gitrepo/compare.go index 06cf880d993..7e38d33e6f2 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 f8661d94121..91ee32bed54 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 9be3ef94aeb..4940f1c0788 100644 --- a/modules/gitrepo/config.go +++ b/modules/gitrepo/config.go @@ -5,21 +5,11 @@ package gitrepo import ( "context" - "strings" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/globallock" ) -func GitConfigGet(ctx context.Context, repo Repository, key string) (string, error) { - result, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("config", "--get"). - AddDynamicArguments(key)) - if err != nil { - return "", err - } - return strings.TrimSpace(result), nil -} - func getRepoConfigLockKey(repoStoragePath string) string { return "repo-config:" + repoStoragePath } diff --git a/modules/gitrepo/merge.go b/modules/gitrepo/merge.go index 8d58e21c8db..51983929495 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/graceful/manager_windows.go b/modules/graceful/manager_windows.go index 457768d6ca0..9592dd6b39c 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 7c855ac64ec..77a2c3b6f83 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 c7416c7a10c..dca28588e47 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 d026210475c..211132b255f 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 5d98578f353..5b3348761c2 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 15c63714222..044dba679a1 100644 --- a/modules/hostmatcher/hostmatcher.go +++ b/modules/hostmatcher/hostmatcher.go @@ -78,11 +78,6 @@ func (hl *HostMatchList) AppendBuiltin(builtin string) { hl.builtins = append(hl.builtins, builtin) } -// AppendPattern appends more pattern to match -func (hl *HostMatchList) AppendPattern(pattern string) { - hl.patterns = append(hl.patterns, pattern) -} - // IsEmpty checks if the checklist is empty func (hl *HostMatchList) IsEmpty() bool { return hl == nil || (len(hl.builtins) == 0 && len(hl.patterns) == 0 && len(hl.ipNets) == 0) diff --git a/modules/httplib/content_disposition.go b/modules/httplib/content_disposition.go new file mode 100644 index 00000000000..da23dae2210 --- /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 00000000000..bf5040e1075 --- /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 2d66a86a8b0..d51c938bf07 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,50 +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) + } } - isSVG := sniffedType.IsSvgImage() - - // serve types that can present a security risk with CSP - if isSVG { - w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox") - } else 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'") - } - - opts.Disposition = "inline" - if isSVG && !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) @@ -135,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 { @@ -145,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) @@ -199,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 78b88c9b5f1..419085237c1 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 9d170528ad3..f9a3d4156dd 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 a884ab733a8..a8baf44edce 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 009d659d761..b5eb5116b06 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 265ce26585b..eaeb89efaa1 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 00000000000..774d6fbd6c5 --- /dev/null +++ b/modules/indexer/internal/elasticsearch/indexer_test.go @@ -0,0 +1,44 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package elasticsearch + +import ( + "os" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func newRealIndexer(t *testing.T) *Indexer { + t.Helper() + url := "http://elasticsearch:9200" + if os.Getenv("CI") == "" { + url = os.Getenv("TEST_ELASTICSEARCH_URL") + if url == "" { + t.Skip("TEST_ELASTICSEARCH_URL not set and not running in CI") + } + } + indexName := "gitea_test_" + strings.ReplaceAll(strings.ToLower(t.Name()), "/", "_") + ix := NewIndexer(url, 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 00000000000..8f8cf743032 --- /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 00000000000..106f2faa8e6 --- /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 9e034bd5530..2e96d4220aa 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 39d96cab985..219d4163d6a 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 380a25dc23d..7a66efe7914 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 f17724664d0..f4582d38dd8 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 9d627466ef4..8286e72f492 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 cb9ed3889d4..9b0eaebef60 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch_test.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch_test.go @@ -6,6 +6,7 @@ package elasticsearch import ( "fmt" "net/http" + "net/url" "os" "testing" "time" @@ -17,19 +18,36 @@ import ( 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 := "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 == "" { + rawURL = os.Getenv("TEST_ELASTICSEARCH_URL") + if rawURL == "" { 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 +55,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 3e38ac49b71..a67e96c0a28 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 0d4f0f727d5..84979a8e645 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 7aebbbcd58e..20e64a59554 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 5715cf47947..6ac6d239c80 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/util.go b/modules/indexer/issues/util.go index 7647be58e89..fafe9b8bbd8 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 77ea047fa71..00000000000 --- 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 156e4560418..81d644d4f4e 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 0bba2783bcb..c4afc9513b7 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 0d9c0c98acc..be1e6c8e90c 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 2bc8c4f4497..6dacf583a7f 100644 --- a/modules/log/logger_global.go +++ b/modules/log/logger_global.go @@ -75,12 +75,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/markup/common/linkify.go b/modules/markup/common/linkify.go index aff1813803e..54e40a02d63 100644 --- a/modules/markup/common/linkify.go +++ b/modules/markup/common/linkify.go @@ -96,6 +96,7 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont m[1] -= closing } } else if lastChar == ';' { + // exclude HTML entity reference, e.g.: exclude " " from "http://example.com?foo=1 " i := m[1] - 2 for ; i >= m[0]; i-- { if util.IsAlphaNumeric(line[i]) { @@ -105,7 +106,7 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont } if i != m[1]-2 { if line[i] == '&' { - m[1] -= m[1] - i + m[1] = i } } } diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go index 4d447e301ab..4b3c96fd33d 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 00000000000..7327503d28a --- /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 ac5eae53ffe..00000000000 --- 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 1c2ae6918de..0fe37ae3052 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 e62747c7241..4fa9466d19a 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -317,7 +317,7 @@ func TestRender_email(t *testing.T) { func TestRender_emoji(t *testing.T) { setting.AppURL = markup.TestAppURL - setting.StaticURLPrefix = markup.TestAppURL + setting.StaticURLPrefix = strings.TrimSuffix(markup.TestAppURL, "/") test := func(input, expected string) { expected = strings.ReplaceAll(expected, "&", "&") @@ -500,7 +500,7 @@ func Test_ParseClusterFuzz(t *testing.T) { } func TestPostProcess(t *testing.T) { - setting.StaticURLPrefix = markup.TestAppURL // can't run standalone + setting.StaticURLPrefix = strings.TrimSuffix(markup.TestAppURL, "/") // can't run standalone defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() test := func(input, expected string) { diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 555a171685b..4a560517f22 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 cca44a87744..1cc75d763d1 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 261c4e780c9..2f14a0fae98 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 e76b253ecd2..6ddd892110b 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 00000000000..de9264c4c49 --- /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 c89ad2f2cf3..6cafa8ff78e 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/render.go b/modules/markup/render.go index 5785dc5ad54..6e8838d49fe 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 77ba8bf4f4c..447cf4807ec 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 e5ba018e1be..e66f00c02f6 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 e8b6891ca11..fd6ac45a210 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 da997f645b2..fd7db9074b8 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/repo.go b/modules/migration/repo.go index 22c2cf6fb32..8f5364fefa3 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 695c2c11352..d7c70815962 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 dca109d6afc..46db8792858 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/options/options_bindata.go b/modules/options/options_bindata.go index b2321d7eb50..f85f30065e2 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/content_store.go b/modules/packages/content_store.go index 57974515e2c..4c61233b5e7 100644 --- a/modules/packages/content_store.go +++ b/modules/packages/content_store.go @@ -36,8 +36,8 @@ func (s *ContentStore) ShouldServeDirect() bool { return setting.Packages.Storage.ServeDirect() } -func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename, method string, reqParams url.Values) (*url.URL, error) { - return s.store.URL(KeyToRelativePath(key), filename, method, reqParams) +func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename, method string, reqParams *storage.ServeDirectOptions) (*url.URL, error) { + return s.store.ServeDirectURL(KeyToRelativePath(key), filename, method, reqParams) } // FIXME: Workaround to be removed in v1.20 diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 51246273957..ae15e4ec831 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 2eadee5463a..5e398151e8c 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 f4f78c2cab5..d8ac7ea75f5 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 1505221acc7..bf7b7e2c53a 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 78925c6e6d9..d0137f8dfef 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 461773cbfce..440bcb9fac2 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 00000000000..3c326c04e9b --- /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 00000000000..57631286991 --- /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/private/hook.go b/modules/private/hook.go index 215996b9b99..ce87ccd8019 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -37,7 +37,7 @@ type HookOptions struct { PushTrigger repository.PushTrigger DeployKeyID int64 // if the pusher is a DeployKey, then UserID is the repo's org user. IsWiki bool - ActionPerm int + ActionsTaskID int64 // if the pusher is an Actions user, the task ID } // SSHLogOption ssh log options diff --git a/modules/public/manifest.go b/modules/public/manifest.go new file mode 100644 index 00000000000..f807244c893 --- /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 00000000000..acfeaa6dbeb --- /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 32bdf3bfa25..fa4691c6a96 100644 --- a/modules/public/mime_types.go +++ b/modules/public/mime_types.go @@ -3,38 +3,48 @@ package public -import "strings" +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 +// 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 // mime.TypeByExtension would use OS's mime-type config to overwrite the well-known types (see its document). // If the user's OS has incorrect mime-type config, it would make Gitea can not respond a correct Content-Type to browsers. // For example, if Gitea returns `text/plain` for a `.js` file, the browser couldn't run the JS due to security reasons. -// detectWellKnownMimeType makes the Content-Type for well-known files stable. -func detectWellKnownMimeType(ext 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 3a5a76637e7..bb4721a48d3 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() @@ -51,9 +62,9 @@ func parseAcceptEncoding(val string) container.Set[string] { } // setWellKnownContentType will set the Content-Type if the file is a well-known type. -// See the comments of detectWellKnownMimeType +// See the comments of DetectWellKnownMimeType func setWellKnownContentType(w http.ResponseWriter, file string) { - mimeType := detectWellKnownMimeType(path.Ext(file)) + mimeType := DetectWellKnownMimeType(path.Ext(file)) if mimeType != "" { w.Header().Set("Content-Type", mimeType) } diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go new file mode 100644 index 00000000000..e6be460599a --- /dev/null +++ b/modules/public/vitedev.go @@ -0,0 +1,204 @@ +// 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/emoji.json: it is an exception for the frontend assets, it is imported by JS code, but: + // - 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/emoji.json" just happens to have the dir name "assets", it is not related to frontend assets + // - BAD DESIGN: indeed it is a "conflicted and polluted name" sample + if path == "/assets/emoji.json" { + return true + } + return false +} diff --git a/modules/queue/manager_test.go b/modules/queue/manager_test.go index fda498cc841..f9f9b7310be 100644 --- a/modules/queue/manager_test.go +++ b/modules/queue/manager_test.go @@ -13,11 +13,7 @@ import ( ) func TestManager(t *testing.T) { - oldAppDataPath := setting.AppDataPath setting.AppDataPath = t.TempDir() - defer func() { - setting.AppDataPath = oldAppDataPath - }() newQueueFromConfig := func(name, cfg string) (*WorkerPoolQueue[int], error) { cfgProvider, err := setting.NewConfigProviderFromData(cfg) diff --git a/modules/repository/branch.go b/modules/repository/branch.go index 0a8f7cc464d..8dda3f82d7a 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 a3e253e998e..32550a9f03e 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 04c07118283..46db7b028b3 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/repository/env.go b/modules/repository/env.go index 55a81f006e2..ed2c6fef81a 100644 --- a/modules/repository/env.go +++ b/modules/repository/env.go @@ -11,25 +11,26 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) // env keys for git hooks need const ( - EnvRepoName = "GITEA_REPO_NAME" - EnvRepoUsername = "GITEA_REPO_USER_NAME" - EnvRepoID = "GITEA_REPO_ID" - EnvRepoIsWiki = "GITEA_REPO_IS_WIKI" - EnvPusherName = "GITEA_PUSHER_NAME" - EnvPusherEmail = "GITEA_PUSHER_EMAIL" - EnvPusherID = "GITEA_PUSHER_ID" - EnvKeyID = "GITEA_KEY_ID" // public key ID - EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID" - EnvPRID = "GITEA_PR_ID" - EnvPRIndex = "GITEA_PR_INDEX" // not used by Gitea at the moment, it is for custom git hooks - EnvPushTrigger = "GITEA_PUSH_TRIGGER" - EnvIsInternal = "GITEA_INTERNAL_PUSH" - EnvAppURL = "GITEA_ROOT_URL" - EnvActionPerm = "GITEA_ACTION_PERM" + EnvRepoName = "GITEA_REPO_NAME" + EnvRepoUsername = "GITEA_REPO_USER_NAME" + EnvRepoID = "GITEA_REPO_ID" + EnvRepoIsWiki = "GITEA_REPO_IS_WIKI" + EnvPusherName = "GITEA_PUSHER_NAME" + EnvPusherEmail = "GITEA_PUSHER_EMAIL" + EnvPusherID = "GITEA_PUSHER_ID" + EnvKeyID = "GITEA_KEY_ID" // public key ID + EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID" + EnvPRID = "GITEA_PR_ID" + EnvPRIndex = "GITEA_PR_INDEX" // not used by Gitea at the moment, it is for custom git hooks + EnvPushTrigger = "GITEA_PUSH_TRIGGER" + EnvIsInternal = "GITEA_INTERNAL_PUSH" + EnvAppURL = "GITEA_ROOT_URL" + EnvActionsTaskID = "GITEA_ACTIONS_TASK_ID" ) type PushTrigger string @@ -54,36 +55,39 @@ func PushingEnvironment(doer *user_model.User, repo *repo_model.Repository) []st return FullPushingEnvironment(doer, doer, repo, repo.Name, 0, 0) } +func DoerPushingEnvironment(doer *user_model.User, repo *repo_model.Repository, isWiki bool) []string { + env := []string{ + EnvAppURL + "=" + setting.AppURL, + EnvRepoName + "=" + repo.Name + util.Iif(isWiki, ".wiki", ""), + EnvRepoUsername + "=" + repo.OwnerName, + EnvRepoID + "=" + strconv.FormatInt(repo.ID, 10), + EnvRepoIsWiki + "=" + strconv.FormatBool(isWiki), + EnvPusherName + "=" + doer.Name, + EnvPusherID + "=" + strconv.FormatInt(doer.ID, 10), + } + if !doer.KeepEmailPrivate { + env = append(env, EnvPusherEmail+"="+doer.Email) + } + if taskID, isActionsUser := user_model.GetActionsUserTaskID(doer); isActionsUser { + env = append(env, EnvActionsTaskID+"="+strconv.FormatInt(taskID, 10)) + } + return env +} + // FullPushingEnvironment returns an os environment to allow hooks to work on push func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model.Repository, repoName string, prID, prIndex int64) []string { - isWiki := "false" - if strings.HasSuffix(repoName, ".wiki") { - isWiki = "true" - } - + isWiki := strings.HasSuffix(repoName, ".wiki") authorSig := author.NewGitSig() committerSig := committer.NewGitSig() - environ := append(os.Environ(), "GIT_AUTHOR_NAME="+authorSig.Name, "GIT_AUTHOR_EMAIL="+authorSig.Email, "GIT_COMMITTER_NAME="+committerSig.Name, "GIT_COMMITTER_EMAIL="+committerSig.Email, - EnvRepoName+"="+repoName, - EnvRepoUsername+"="+repo.OwnerName, - EnvRepoIsWiki+"="+isWiki, - EnvPusherName+"="+committer.Name, - EnvPusherID+"="+strconv.FormatInt(committer.ID, 10), - EnvRepoID+"="+strconv.FormatInt(repo.ID, 10), EnvPRID+"="+strconv.FormatInt(prID, 10), EnvPRIndex+"="+strconv.FormatInt(prIndex, 10), - EnvAppURL+"="+setting.AppURL, "SSH_ORIGINAL_COMMAND=gitea-internal", ) - - if !committer.KeepEmailPrivate { - environ = append(environ, EnvPusherEmail+"="+committer.Email) - } - + environ = append(environ, DoerPushingEnvironment(committer, repo, isWiki)...) return environ } diff --git a/modules/session/redis.go b/modules/session/redis.go index 083869f4e1e..f5cac8e636c 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 7a91ecb5930..0d1bdadc8ec 100644 --- a/modules/setting/actions.go +++ b/modules/setting/actions.go @@ -12,6 +12,8 @@ import ( "code.gitea.io/gitea/modules/log" ) +const defaultMaxRerunAttempts = 50 + // Actions settings var ( Actions = struct { @@ -27,11 +29,13 @@ var ( AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"` SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"` WorkflowDirs []string `ini:"WORKFLOW_DIRS"` + MaxRerunAttempts int64 `ini:"MAX_RERUN_ATTEMPTS"` }{ Enabled: true, DefaultActionsURL: defaultActionsURLGitHub, SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"}, WorkflowDirs: []string{".gitea/workflows", ".github/workflows"}, + MaxRerunAttempts: defaultMaxRerunAttempts, } ) @@ -118,6 +122,10 @@ func loadActionsFrom(rootCfg ConfigProvider) error { Actions.EndlessTaskTimeout = sec.Key("ENDLESS_TASK_TIMEOUT").MustDuration(3 * time.Hour) Actions.AbandonedJobTimeout = sec.Key("ABANDONED_JOB_TIMEOUT").MustDuration(24 * time.Hour) + if Actions.MaxRerunAttempts <= 0 { + Actions.MaxRerunAttempts = defaultMaxRerunAttempts + } + if !Actions.LogCompression.IsValid() { return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression) } diff --git a/modules/setting/database.go b/modules/setting/database.go index 1a4bf648058..edaaf32c14d 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 c1037cfb27e..00000000000 --- 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/lfs.go b/modules/setting/lfs.go index 7f2d0ae1590..3ec21860ae6 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 59866c76055..764c2a82f4f 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 f281715973e..7e1c988568c 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 2dfe77dda9a..83891387a77 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -99,6 +99,7 @@ var OAuth2 = struct { JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"` MaxTokenLength int DefaultApplications []string + CustomSchemes []string }{ Enabled: true, AccessTokenExpirationTime: 3600, @@ -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 b5984240648..38ee2ad55eb 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 f51457a620e..45c6759a73e 100644 --- a/modules/setting/path.go +++ b/modules/setting/path.go @@ -198,6 +198,12 @@ func InitWorkPathAndCfgProvider(getEnvFn func(name string) string, args ArgWorkP CustomConf = tmpCustomConf.Value } +func MockBuiltinPaths(workPath, customPath, customConf string) func() { + oldApp, oldCustom, oldConf := appWorkPathBuiltin, customPathBuiltin, customConfBuiltin + appWorkPathBuiltin, customPathBuiltin, customConfBuiltin = workPath, customPath, customConf + return func() { appWorkPathBuiltin, customPathBuiltin, customConfBuiltin = oldApp, oldCustom, oldConf } +} + // AppDataTempDir returns a managed temporary directory for the application data. // Using empty sub will get the managed base temp directory, and it's safe to delete it. // Gitea only creates subdirectories under it, but not the APP_TEMP_PATH directory itself. diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 662e03598b5..a8bc91c0895 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 @@ -86,9 +92,10 @@ var ( DefaultMergeMessageOfficialApproversOnly bool PopulateSquashCommentWithCommitMessages bool AddCoCommitterTrailers bool - TestConflictingPatchesWithGitApply bool RetargetChildrenOnMerge bool DelayCheckForInactiveDays int + DefaultDeleteBranchAfterMerge bool + DefaultTitleSource string } `ini:"repository.pull-request"` // Issue Setting @@ -210,9 +217,10 @@ var ( DefaultMergeMessageOfficialApproversOnly bool PopulateSquashCommentWithCommitMessages bool AddCoCommitterTrailers bool - TestConflictingPatchesWithGitApply bool 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 a1fd0bce2e5..8b7664baba3 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -16,9 +16,11 @@ import ( // Security settings var Security = struct { // TODO: move more settings to this struct in future - XFrameOptions string + XFrameOptions string + XContentTypeOptions string }{ - XFrameOptions: "SAMEORIGIN", + XFrameOptions: "SAMEORIGIN", + XContentTypeOptions: "nosniff", } var ( @@ -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 7e7611b802d..dc58e43c435 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -4,7 +4,6 @@ package setting import ( - "encoding/base64" "net" "net/url" "os" @@ -13,7 +12,6 @@ import ( "strings" "time" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" ) @@ -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") @@ -286,7 +218,7 @@ func loadServerFrom(rootCfg ConfigProvider) { defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL) - PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLLegacy) + PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLAuto) if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy && PublicURLDetection != PublicURLNever { log.Fatal("Invalid PUBLIC_URL_DETECTION value: %s", PublicURLDetection) } @@ -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 f2b6274edca..5bfe68697af 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -28,8 +28,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 +41,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 +59,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. @@ -195,7 +201,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 +232,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 13575f52a6e..00000000000 --- a/modules/setting/setting_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package setting - -import ( - "net/url" - "testing" - - "code.gitea.io/gitea/modules/json" - - "github.com/stretchr/testify/assert" -) - -func TestMakeAbsoluteAssetURL(t *testing.T) { - appURL1, _ := url.Parse("https://localhost:1234") - appURL2, _ := url.Parse("https://localhost:1234/") - appURLSub1, _ := url.Parse("https://localhost:1234/foo") - appURLSub2, _ := url.Parse("https://localhost:1234/foo/") - - // static URL is an absolute URL, so should be used - assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL(appURL1, "https://localhost:2345")) - assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL(appURL1, "https://localhost:2345/")) - - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL1, "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL2, "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL1, "/foo/")) - - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub1, "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub2, "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub1, "/foo/")) - - assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub1, "/bar")) - assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub2, "/bar")) - assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub1, "/bar/")) -} - -func TestMakeManifestData(t *testing.T) { - jsonBytes := MakeManifestData(`Example App '\"`, "https://example.com", "https://example.com/foo/bar") - assert.True(t, json.Valid(jsonBytes)) -} diff --git a/modules/setting/testenv.go b/modules/setting/testenv.go index 853521c328a..24d489d77c2 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/storage/azureblob.go b/modules/storage/azureblob.go index e7297cec77a..a273a7770d0 100644 --- a/modules/storage/azureblob.go +++ b/modules/storage/azureblob.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "net/http" "net/url" "os" "path" @@ -246,16 +247,53 @@ func (a *AzureBlobStorage) Delete(path string) error { return convertAzureBlobErr(err) } -// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. -func (a *AzureBlobStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) { - blobClient := a.getBlobClient(path) +func (a *AzureBlobStorage) getSasURL(b *blob.Client, template sas.BlobSignatureValues) (string, error) { + urlParts, err := blob.ParseURL(b.URL()) + if err != nil { + return "", err + } - // TODO: OBJECT-STORAGE-CONTENT-TYPE: "browser inline rendering images/PDF" needs proper Content-Type header from storage - startTime := time.Now() - u, err := blobClient.GetSASURL(sas.BlobPermissions{ - Read: true, - }, time.Now().Add(5*time.Minute), &blob.GetSASURLOptions{ - StartTime: &startTime, + var t time.Time + if urlParts.Snapshot == "" { + t = time.Time{} + } else { + t, err = time.Parse(blob.SnapshotTimeFormat, urlParts.Snapshot) + if err != nil { + return "", err + } + } + + template.ContainerName = urlParts.ContainerName + template.BlobName = urlParts.BlobName + template.SnapshotTime = t + template.Version = sas.Version + + qps, err := template.SignWithSharedKey(a.credential) + if err != nil { + return "", err + } + + endpoint := b.URL() + "?" + qps.Encode() + + return endpoint, nil +} + +func (a *AzureBlobStorage) ServeDirectURL(storePath, name, method string, reqParams *ServeDirectOptions) (*url.URL, error) { + blobClient := a.getBlobClient(storePath) + + startTime := time.Now().UTC() + + param := prepareServeDirectOptions(reqParams, name) + + u, err := a.getSasURL(blobClient, sas.BlobSignatureValues{ + Permissions: (&sas.BlobPermissions{ + Read: method == http.MethodGet || method == http.MethodHead, + Write: method == http.MethodPut, + }).String(), + StartTime: startTime, + ExpiryTime: startTime.Add(5 * time.Minute), + ContentDisposition: param.ContentDisposition, + ContentType: param.ContentType, }) if err != nil { return nil, convertAzureBlobErr(err) diff --git a/modules/storage/azureblob_test.go b/modules/storage/azureblob_test.go index b3791b49164..b5d5d0fecc0 100644 --- a/modules/storage/azureblob_test.go +++ b/modules/storage/azureblob_test.go @@ -14,12 +14,13 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAzureBlobStorageIterator(t *testing.T) { +func TestAzureBlobStorage(t *testing.T) { if os.Getenv("CI") == "" { t.Skip("azureBlobStorage not present outside of CI") return } - testStorageIterator(t, setting.AzureBlobStorageType, &setting.Storage{ + 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", @@ -28,7 +29,25 @@ func TestAzureBlobStorageIterator(t *testing.T) { AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==", Container: "test", }, - }) + } + table := []struct { + name string + test func(t *testing.T, typStr Type, cfg *setting.Storage) + }{ + { + name: "iterator", + test: testStorageIterator, + }, + { + name: "testBlobStorageURLContentTypeAndDisposition", + test: testBlobStorageURLContentTypeAndDisposition, + }, + } + for _, entry := range table { + t.Run(entry.name, func(t *testing.T) { + entry.test(t, storageType, config) + }) + } } func TestAzureBlobStoragePath(t *testing.T) { diff --git a/modules/storage/helper.go b/modules/storage/helper.go index f6c3d5eebbc..30d96a33add 100644 --- a/modules/storage/helper.go +++ b/modules/storage/helper.go @@ -30,7 +30,7 @@ func (s discardStorage) Delete(_ string) error { return fmt.Errorf("%s", s) } -func (s discardStorage) URL(_, _, _ string, _ url.Values) (*url.URL, error) { +func (s discardStorage) ServeDirectURL(_, _, _ string, _ *ServeDirectOptions) (*url.URL, error) { return nil, fmt.Errorf("%s", s) } diff --git a/modules/storage/helper_test.go b/modules/storage/helper_test.go index 3cba1e13c01..ae64c8aca49 100644 --- a/modules/storage/helper_test.go +++ b/modules/storage/helper_test.go @@ -37,7 +37,7 @@ func Test_discardStorage(t *testing.T) { assert.Error(t, err, string(tt)) } { - got, err := tt.URL("path", "name", "GET", nil) + got, err := tt.ServeDirectURL("path", "name", "GET", nil) assert.Nil(t, got) assert.Errorf(t, err, string(tt)) } diff --git a/modules/storage/local.go b/modules/storage/local.go index 5ea6f055ce7..04e3e05f950 100644 --- a/modules/storage/local.go +++ b/modules/storage/local.go @@ -133,8 +133,7 @@ func (l *LocalStorage) Delete(path string) error { return err } -// URL gets the redirect URL to a file -func (l *LocalStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) { +func (l *LocalStorage) ServeDirectURL(path, name, _ string, reqParams *ServeDirectOptions) (*url.URL, error) { return nil, ErrURLNotSupported } diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 6993ac2d922..ace78bb6105 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 @@ -278,37 +274,16 @@ func (m *MinioStorage) Delete(path string) error { return convertMinioErr(err) } -// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. -func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams url.Values) (*url.URL, error) { - // copy serveDirectReqParams - reqParams, err := url.ParseQuery(serveDirectReqParams.Encode()) - if err != nil { - return nil, err - } +func (m *MinioStorage) ServeDirectURL(storePath, name, method string, opt *ServeDirectOptions) (*url.URL, error) { + reqParams := url.Values{} - // Here we might not know the real filename, and it's quite inefficient to detect the mine type by pre-fetching the object head. - // So we just do a quick detection by extension name, at least if works for the "View Raw File" for an LFS file on the Web UI. - // Detect content type by extension name, only support the well-known safe types for inline rendering. - // TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future - ext := path.Ext(name) - inlineExtMimeTypes := map[string]string{ - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".webp": "image/webp", - ".avif": "image/avif", - // ATTENTION! Don't support unsafe types like HTML/SVG due to security concerns: they can contain JS code, and maybe they need proper Content-Security-Policy - // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline - ".pdf": "application/pdf", - - // TODO: refactor with "modules/public/mime_types.go", for example: "DetectWellKnownSafeInlineMimeType" + param := prepareServeDirectOptions(opt, name) + // minio does not ignore empty params + if param.ContentType != "" { + reqParams.Set("response-content-type", param.ContentType) } - if mimeType, ok := inlineExtMimeTypes[ext]; ok { - reqParams.Set("response-content-type", mimeType) - reqParams.Set("response-content-disposition", "inline") - } else { - reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name))) + if param.ContentDisposition != "" { + reqParams.Set("response-content-disposition", param.ContentDisposition) } expires := 5 * time.Minute @@ -323,6 +298,7 @@ func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams // IterateObjects iterates across the objects in the miniostorage func (m *MinioStorage) IterateObjects(dirName string, fn func(path string, obj Object) error) error { opts := minio.GetObjectOptions{} + // FIXME: this loop is not right and causes resource leaking, see the comment of ListObjects for mObjInfo := range m.client.ListObjects(m.ctx, m.bucket, minio.ListObjectsOptions{ Prefix: m.buildMinioDirPrefix(dirName), Recursive: true, diff --git a/modules/storage/minio_test.go b/modules/storage/minio_test.go index 2726d765ddf..5c15ee1ed6d 100644 --- a/modules/storage/minio_test.go +++ b/modules/storage/minio_test.go @@ -16,12 +16,13 @@ import ( "github.com/stretchr/testify/assert" ) -func TestMinioStorageIterator(t *testing.T) { +func TestMinioStorage(t *testing.T) { if os.Getenv("CI") == "" { t.Skip("minioStorage not present outside of CI") return } - testStorageIterator(t, setting.MinioStorageType, &setting.Storage{ + storageType := setting.MinioStorageType + config := &setting.Storage{ MinioConfig: setting.MinioStorageConfig{ Endpoint: "minio:9000", AccessKeyID: "123456", @@ -29,7 +30,25 @@ func TestMinioStorageIterator(t *testing.T) { Bucket: "gitea", Location: "us-east-1", }, - }) + } + table := []struct { + name string + test func(t *testing.T, typStr Type, cfg *setting.Storage) + }{ + { + name: "iterator", + test: testStorageIterator, + }, + { + name: "testBlobStorageURLContentTypeAndDisposition", + test: testBlobStorageURLContentTypeAndDisposition, + }, + } + for _, entry := range table { + t.Run(entry.name, func(t *testing.T) { + entry.test(t, storageType, config) + }) + } } func TestMinioStoragePath(t *testing.T) { diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 74d0cd47c87..1271440e5a8 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -10,33 +10,17 @@ import ( "io" "net/url" "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" ) // 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 @@ -56,6 +40,37 @@ type Object interface { Stat() (os.FileInfo, error) } +// ServeDirectOptions customizes HTTP headers for a generated signed URL. +type ServeDirectOptions struct { + // Overrides the automatically detected MIME type. + ContentType string +} + +// Safe defaults are applied only when not explicitly overridden by the caller. +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.ContentType = optsOptional.ContentType + } + name = path.Base(name) + if ret.ContentType == "" { + ext := path.Ext(name) + ret.ContentType = public.DetectWellKnownMimeType(ext) + } + // 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 +} + // ObjectStorage represents an object storage to handle a bucket and files type ObjectStorage interface { Open(path string) (Object, error) @@ -67,7 +82,15 @@ type ObjectStorage interface { Stat(path string) (os.FileInfo, error) Delete(path string) error - URL(path, name, method string, reqParams url.Values) (*url.URL, error) + + // ServeDirectURL generates a "serve-direct" URL for the specified blob storage file, + // end user (browser) will use this URL to access the file directly from the object storage, bypassing Gitea server. + // Usually the link is time-limited (a few minutes) and contains a signature to ensure security. + // The generated URL must NOT use the same origin as Gitea server, otherwise it will cause security issues. + // * method defines which HTTP method is permitted for certain storage providers (e.g., MinIO). + // * opt allows customizing the Content-Type and Content-Disposition headers. + // TODO: need to merge "ServeDirect()" check into this function, avoid duplicate code and potential inconsistency. + ServeDirectURL(path, name, method string, opt *ServeDirectOptions) (*url.URL, error) // IterateObjects calls the iterator function for each object in the storage with the given path as prefix // The "fullPath" argument in callback is the full path in this storage. @@ -136,7 +159,7 @@ var ( // Actions represents actions storage Actions ObjectStorage = uninitializedStorage - // Actions Artifacts represents actions artifacts storage + // ActionsArtifacts Artifacts represents actions artifacts storage ActionsArtifacts ObjectStorage = uninitializedStorage ) diff --git a/modules/storage/storage_test.go b/modules/storage/storage_test.go index 08f274e74b4..83ee2ef7934 100644 --- a/modules/storage/storage_test.go +++ b/modules/storage/storage_test.go @@ -4,12 +4,14 @@ package storage import ( + "net/http" "strings" "testing" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) { @@ -50,3 +52,53 @@ func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) { assert.Len(t, expected, count) } } + +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()) + require.NoError(t, err) + defer resp.Body.Close() + if expected.ContentType != "" { + assert.Equal(t, expected.ContentType, resp.Header.Get("Content-Type")) + } + if expected.ContentDisposition != "" { + assert.Equal(t, expected.ContentDisposition, resp.Header.Get("Content-Disposition")) + } +} + +func testBlobStorageURLContentTypeAndDisposition(t *testing.T, typStr Type, cfg *setting.Storage) { + s, err := NewStorage(typStr, cfg) + assert.NoError(t, err) + + testFilename := "test.txt" + _, err = s.Save(testFilename, strings.NewReader("dummy-content"), -1) + assert.NoError(t, err) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.txt", expectedServeDirectHeaders{ + ContentType: "text/plain; charset=utf-8", + ContentDisposition: `inline; filename=test.txt`, + }, nil) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.pdf", expectedServeDirectHeaders{ + ContentType: "application/pdf", + ContentDisposition: `inline; filename=test.pdf`, + }, nil) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{ + ContentDisposition: `inline; filename=test.wasm`, + }, nil) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{ + ContentType: "application/wasm", + ContentDisposition: `inline; filename=test.wasm`, + }, &ServeDirectOptions{ + ContentType: "application/wasm", + }) + assert.NoError(t, s.Delete(testFilename)) +} diff --git a/modules/structs/activity.go b/modules/structs/activity.go index 9085495593a..b896adfed52 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 d158a5fd316..ee65e016bcd 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 57af38464a2..99c1535155e 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 2540481d0ff..f108cf3d0a6 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 226c613d47b..dd8bdc6cda7 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 cee5da6624d..b94e02aee35 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 c3d70ebf000..b93f68b6009 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,29 +60,25 @@ 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"` } -// TODO: make EditOrgOption fields optional after https://gitea.com/go-chi/binding/pulls/5 got merged - // EditOrgOption options for editing an organization type EditOrgOption struct { // The full display name of the organization - FullName string `json:"full_name" binding:"MaxSize(100)"` - // The email address of the organization - Email string `json:"email" binding:"MaxSize(255)"` + FullName *string `json:"full_name" binding:"MaxSize(100)"` + // The email address of the organization; use empty string to clear + Email *string `json:"email" binding:"MaxSize(255)"` // The description of the organization - Description string `json:"description" binding:"MaxSize(255)"` + Description *string `json:"description" binding:"MaxSize(255)"` // The website URL of the organization - Website string `json:"website" binding:"ValidUrl;MaxSize(255)"` + Website *string `json:"website" binding:"ValidUrl;MaxSize(255)"` // The location of the organization - Location string `json:"location" binding:"MaxSize(50)"` + 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 d34de5b6d2e..20959931d33 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 00000000000..5feb122767b --- /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 3ad2f78bd34..cd2ffbe7195 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 f44d2f84f5a..82c86b5b4ac 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 3507cc410a1..0c3a0ab44e3 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. @@ -114,8 +123,7 @@ type Repository struct { 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"` @@ -150,11 +158,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 @@ -229,6 +236,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 +391,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 92ca9bcccef..4592c18ed69 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -105,12 +105,18 @@ type ActionArtifact struct { // ActionWorkflowRun represents a WorkflowRun type ActionWorkflowRun struct { - ID int64 `json:"id"` - URL string `json:"url"` - HTMLURL string `json:"html_url"` - DisplayTitle string `json:"display_title"` - Path string `json:"path"` - Event string `json:"event"` + ID int64 `json:"id"` + URL string `json:"url"` + // PreviousAttemptURL is the API URL of the previous attempt of this run, e.g. ".../actions/runs/{run_id}/attempts/{attempt-1}". + // It is set only when the current attempt is > 1 (i.e. a rerun). For the first attempt, or for legacy runs that pre-date ActionRunAttempt, it is null. + PreviousAttemptURL *string `json:"previous_attempt_url"` + HTMLURL string `json:"html_url"` + DisplayTitle string `json:"display_title"` + Path string `json:"path"` + Event string `json:"event"` + // RunAttempt is 1-based for runs created after ActionRunAttempt was introduced. + // A value of 0 is a legacy-only sentinel for runs created before attempts existed + // and indicates no corresponding /attempts/{n} resource is available. RunAttempt int64 `json:"run_attempt"` RunNumber int64 `json:"run_number"` RepositoryID int64 `json:"repository_id,omitempty"` diff --git a/modules/structs/repo_collaborator.go b/modules/structs/repo_collaborator.go index 9ede7f075a6..fb36ecf4858 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 59665062b77..53ce5aeae28 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 90dbcff25cb..a25c0c5a1e7 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 b5ff353b876..f537884963d 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 d2d4d364df0..aebbfc74074 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 f90818c0ad9..c21c20efff9 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -5,6 +5,7 @@ package templates import ( "html/template" + "net/url" "strings" "testing" @@ -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 ca13626468d..f81073902f3 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 8f6c82fc4ba..475cb0d678e 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 a919591ecfd..0399b2c49ac 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_misc.go b/modules/templates/util_misc.go index fb523fd53ac..ac179e71783 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 7ff0b204f04..da186f87f21 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 a3318cc11a4..e74b308471a 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 a6448a6ff23..88b05485af0 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 829f735c7cf..387b4f3fc5d 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 4996f5a13b1..331dc4a959b 100644 --- a/modules/test/utils.go +++ b/modules/test/utils.go @@ -5,6 +5,7 @@ package test import ( "archive/tar" + "archive/zip" "bytes" "compress/gzip" "io" @@ -45,6 +46,14 @@ func ParseJSONError(buf []byte) (ret struct { return ret } +func ParseJSONRedirect(buf []byte) (ret struct { + Redirect *string `json:"redirect"` +}, +) { + _ = json.Unmarshal(buf, &ret) + return ret +} + func IsNormalPageCompleted(s string) bool { return strings.Contains(s, `